Shortest distance between a specific object to other objects - image

Following these two posts that deals with finding the distance between objects in binary image, how can I only output/calculate only the shortest distance between a specific object to the rest (for examples, {1->3}, {2->5}, {3->1}, {4->7)?
https://www.mathworks.com/matlabcentral/answers/164955-distance-between-several-objects-in-binary-image
Pairwise distance between all centroid coordinate combinations - Matlab
Script:
clc;
clear all;
I = rgb2gray(imread('E:/NCircles.png'));
imshow(I);
BW = imbinarize(I,'adaptive');
BW = imfill(BW, 'holes');
BW = bwlabel(BW);
s = regionprops(BW,'Area', 'BoundingBox', 'Eccentricity', 'MajorAxisLength', 'MinorAxisLength', 'Orientation', 'Perimeter','Centroid');
imshow(BW)
hold on
for k = 1:numel(s)
c = s(k).Centroid;
text(c(1), c(2), sprintf('%d', k), 'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle');
end
boundaries = bwboundaries(BW);
numberOfBoundaries = size(boundaries, 1);
for k = 1 : numberOfBoundaries
thisBoundary = boundaries{k};
plot(thisBoundary(:,2), thisBoundary(:,1), 'r', 'LineWidth', 3);
end
hold off;
% Define object boundaries
numberOfBoundaries = size(boundaries, 1)
for b1 = 1 : numberOfBoundaries
for b2 = 1 : numberOfBoundaries
if b1 == b2
% Can't find distance between the region and itself
continue;
end
boundary1 = boundaries{b1};
boundary2 = boundaries{b2};
boundary1x = boundary1(:, 2);
boundary1y = boundary1(:, 1);
x1=1;
y1=1;
x2=1;
y2=1;
overallMinDistance = inf; % Initialize.
% For every point in boundary 2, find the distance to every point in boundary 1.
for k = 1 : size(boundary2, 1)
% Pick the next point on boundary 2.
boundary2x = boundary2(k, 2);
boundary2y = boundary2(k, 1);
% For this point, compute distances from it to all points in boundary 1.
allDistances = sqrt((boundary1x - boundary2x).^2 + (boundary1y - boundary2y).^2);
% Find closest point, min distance.
[minDistance(k), indexOfMin] = min(allDistances);
if minDistance(k) < overallMinDistance
x1 = boundary1x(indexOfMin);
y1 = boundary1y(indexOfMin);
x2 = boundary2x;
y2 = boundary2y;
overallMinDistance = minDistance(k);
end
end
% Find the overall min distance
minDistance = min(minDistance);
% Report to command window.
fprintf('The minimum distance from region %d to region %d is %.3f pixels\n', b1, b2, minDistance);
% Draw a line between point 1 and 2
line([x1, x2], [y1, y2], 'Color', 'y', 'LineWidth', 3);
end
end

Given BW and boundaries as defined above, and a source object from which to calculate distances to all other objects:
source_object = 1; % label of source object in BW
Construct a distance image such that the value of each pixel is its distance from the source object using bwdist:
% anonymous function to convert cell array of subsripts
% into cell array of indices
indsfun = #(a) sub2ind(size(BW), a(:,1), a(:,2));
% use function on all of the cell's boundary objects
object_inds = cellfun(indsfun, boundaries, 'UniformOutput', false);
source_image = zeros(size(BW)); % create image containing only source object
source_image(object_inds{source_object}) = 1;
% compute distance from source to all other pixels in image
dist_image = bwdist(source_image, 'euclidean'); % replace with desired metric
imagesc(dist_image); % not necessary, but gives a cool image
Now, for each object in the original image, find the minimum distance of its boundary to the source object boundary:
min_dist = zeros(1,numel(boundaries)); % hold minimum distances
for target_object = 1:numel(boundaries)
% get the distance values at the indices of the target object
% and store the minimum.
min_dist(target_object) = min(dist_image(object_inds{target_object}));
end
In the end, min_dist will contain the minimum (boundary) distance from the source object to all other objects. A sample run on your image gives the following Euclidean distances:
min_dist =
Columns 1 through 7:
0.00000 67.54258 60.00000 207.23416 154.48625 168.79869 319.01410
Columns 8 through 13:
236.05296 324.71063 344.05814 367.00000 469.07996 509.00000

Related

Interp2 of image with transformed coordinates

I have 2 greyscale images that i am trying to align using scalar scaling 1 , rotation matrix [2,2] and translation vector [2,1]. I can calculate image1's transformed coordinates as
y = s*R*x + t;
Below the resulting images are shown.
The first image is image1 before transformation,
the second image is image1 (red) with attempted interpolation using interp2 shown on top of image2 (green)
The third image is when i manually insert the pixel values from image1 into an empty array (that has the same size as image2) using the transformed coordinates.
From this we can see that the coordinate transformation must have been successful, as the images are aligned although not perfectly (which is to be expected since only 2 coordinates were used in calculating s, R and t) .
How come interp2 is not producing a result more similar to when i manually insert pixel values?
Below the code for doing this is included:
Interpolation code
function [transformed_image] = interpolate_image(im_r,im_t,s,R,t)
[m,n] = size(im_t);
% doesn't help if i use get_grid that the other function is using here
[~, grid_xr, grid_yr] = get_ipgrid(im_r);
[x_t, grid_xt, grid_yt] = get_ipgrid(im_t);
y = s*R*x_t + t;
yx = reshape(y(1,:), m,n);
yy = reshape(y(2,:), m,n);
transformed_image = interp2(grid_xr, grid_yr, im_r, yx, yy, 'nearest');
end
function [x, grid_x, grid_y] = get_ipgrid(image)
[m,n] = size(image);
[grid_x,grid_y] = meshgrid(1:n,1:m);
x = [reshape(grid_x, 1, []); reshape(grid_y, 1, [])]; % X is [2xM*N] coordinate pairs
end
The manual code
function [transformed_image] = transform_image(im_r,im_t,s,R,t)
[m,n] = size(im_t);
[x_t, grid_xt, grid_yt] = get_grid(im_t);
y = s*R*x_t + t;
ymat = reshape(y',m,n,2);
yx = ymat(:,:,1);
yy = ymat(:,:,2);
transformed_image = zeros(m,n);
for i = 1:m
for j = 1:n
% make sure coordinates are inside
if (yx(i,j) < m & yy(i,j) < n & yx(i,j) > 0.5 & yy(i,j) > 0.5)
transformed_image(round(yx(i,j)),round(yy(i,j))) = im_r(i,j);
end
end
end
end
function [x, grid_x, grid_y] = get_grid(image)
[m,n] = size(image);
[grid_y,grid_x] = meshgrid(1:n,1:m);
x = [grid_x(:) grid_y(:)]'; % X is [2xM*N] coordinate pairs
end
Can anyone see what i'm doing wrong with interp2? I feel like i have tried everything
Turns out i got interpolation all wrong.
In my question i calculate the coordinates of im1 in im2.
However the way interpolation works is that i need to calculate the coordinates of im2 in im1 such that i can map the image as shown below.
This means that i also calculated the wrong s,R and t since they were used to transform im1 -> im2, where as i needed im2 -> im1. (this is also called the inverse transform). Below is the manual code, that is basically the same as interp2 with nearest neighbour interpolation
function [transformed_image] = transform_image(im_r,im_t,s,R,t)
[m,n] = size(im_t);
[x_t, grid_xt, grid_yt] = get_grid(im_t);
y = s*R*x_t + t;
ymat = reshape(y',m,n,2);
yx = ymat(:,:,1);
yy = ymat(:,:,2);
transformed_image = zeros(m,n);
for i = 1:m
for j = 1:n
% make sure coordinates are inside
if (yx(i,j) < m & yy(i,j) < n & yx(i,j) > 0.5 & yy(i,j) > 0.5)
transformed_image(i,j) = im_r(round(yx(i,j)),round(yy(i,j)));
end
end
end
end

How to rotate a 2D theoretical path to fit or be overlaid on a path from videoprocessing

I track the motion of an object from a video file in MATLAB and save the locations from each frame in a numberOfFrames x 2 array.
I know the theoretical path or intended path. When recording the movie the camera is at some unknown angle in space. Therefore, the path is skewed. The only information I have is the scaling between the pixels and millimeters by using the object diameter.
Now I would like to rotate the intended path, and move it around until it is overlaid on the tracked motion path.
I start with my theoretical path (Pth) then rotate it in 3-dimensions to "Pthr". After that I loop over each point in "M". And for each point in "M", I look for the closest point from "Pthr". Then, I repeat for the next point in "M". This probably has a problem of choosing the same point in "Pthr" for multiple points in "M".
I noticed this is sensitive to my initial guess and it gives terrible results.
Also, M is not a perfect path, since it is experimental measurements it is no where near perfect. measured vs. theoretical unrotated path
% M = [Mx,My], is location in x and y, from motion tracking.
% scale = 20; % pixels/mm, using the size of object
% I build the theoretical path (Pth) goes from to (0,0,0) to (0,3,0) to
% (3,3,0) to (3,0,0) to be approximately the same length as M
Pthup = linspace(0,3,num)';
Pthdwn = linspace(3,0,num)';
Pth0 = zeros(size(Pthup));
Pth3 = 3*ones(size(Pthup));
% Pth is approximately same length as M
Pth = scale*[Pth0 Pthup Pth0;Pthup Pth3 Pth0;Pth3 Pthdwn Pth0];
% using fmincon in matlab to minimize the sum of the square
lb = [145 0 -45 min(min(M)) min(min(M))]; %upper bound
ub = [180 90 45 max(max(M)) max(max(M))]; %lower bound
coro = [180 0 0 mean(Mx) mean(My)]; %initial guess
% initial guess (theta(x),theta(y),theta(z), shift in x, shift in y)
cnt = 0; er = 1;
while (abs(er)>0.1)
[const,fval] = fmincon(#(cor)findOrientation(cor,Pth,M),coro,[],[],[],[],lb,ub);
er = sum(const-coro);
coro = const;
cnt = 1+cnt;
if (cnt>50)
cnt = cnt;
break
end
end
%% function findOrientation keeps rotating Pth until it is closest to M
function [Eo] = findOrientation(cor,Pth,M)
% cor = [angle of rotations, center coordinate];(degrees, non-dimensiolaized in pixels)
% M = measured coordinates from movie in pixel
% coor: is output of the form [x-coordiante,y-coordinate, absolute distance from Center(i,:)]
% F = sum of least square, sum(coor(:,3))
%% Rotation of theoretical path about z,y,x and shifting in it in xy
thx = cor(1);
thy = cor(2);
thz = cor(3);
xy = cor([4:5]);
% T = [cosd(thn) -sind(thn);
% sind(thn) cosd(thn)]; %rotation matrix in 3D
Tz = [cosd(thz) -sind(thz) 0;
sind(thz) cosd(thz) 0;0 0 1]; %rotation matrix
Ty = [cosd(thy) 0 -sind(thy);0 1 0;
sind(thy) 0 cosd(thy)]; %rotation matrix
Tx = [1 0 0;0 cosd(thx) -sind(thx);
0 sind(thx) cosd(thx)]; %rotation matrix
Pthr = zeros(size(Pth));
for i = 1:size(Pth,1)
xp = Tz*Pth(i,:)';
xp = Ty*xp;
xp = Tx*xp;
Pthr(i,:) = xp.';
end
Pthr = Pthr(:,[1,2]); % omit third value because it is 2D
Pthr = Pthr + [cor([4:5])];
rin = sqrt(Pthr(:,1).^2+Pthr(:,2).^2); %theoretical radius
Centern = sqrt(M(:,1).^2 + M(:,2).^2);%measured radius
for i = 1:size(M,1) %loop over each point in tracked motion
sub = Pthr-M(i,:); %subtracting M(i,:) from all Pthr
for j = 1:length(sub)
dist(j,1) = norm(sub(j,:));% distance from M(i,:) to all ri
end
%index is based on the min absolute distance between Pthr and M(i,:). It chooses the closest Pthr to a specific M(i,:)
[mn, index] = min(dist);
erri = abs(rin(index)-Centern(i))./rin(index);
coor(i,:) = erri;
end
Eo = sum(coor);

Check for pixel values in a neighborhood

I'm trying to write a MATLAB script that does the following:
Given: pixel coordinates(x,y) for a .jpg image
Goal: Check, within a 5 pixel radius of given coordinates, if there is a pixel of a certain value.
For example, let's say I'm given the coordinates (100,100), then I want to check the neighborhood of (100,100) within my image for any pixels that are black (0,0,0). So perhaps, pixel (103, 100) and (104,100) might have the value (0,0,0).
Current code:
x_coord = uint32(coord(:,1));
y_coord = uint32(coord(:,2));
count = 0;
for i = 1:length(x_coord)
%(img(x,y) returns pixel value at that (x,y)
%Note 0 = black. Indicating that, at that position, the image is just
% black
if img(x_coord(i),y_coord(i)) == 0
count = count + 1;
end
end
It currently only checks at an exact location. Not in a local neighborhood. How to could I extend this?
EDIT: Also note, as long as there as at least one pixel in the neighborhood with the value, I increment count. I'm not trying to enumerate how many pixels in the neighborhood have that value, just trying to find evidence of at least one pixel that has that value.
EDIT:
Even though I am unable to identify an error with the code, I am not able to get the exact results I want. Here is the code I am using.
val = 0; %pixel value to check
N = 50; % neighbourhood radius
%2D grid of coordinates surrounding center coordinate
[R, C] = ndgrid(1 : size(img, 1), 1 : size(img, 2));
for kk = 1 : size(coord, 1)
r = coord(kk, 1); c = coord(kk, 2); % Get pixel locations
% mask of valid locations within the neighbourhood (avoid boundary problems)
mask = (R - r).^2 + (C - c).^2 <= N*N;
pix = img(mask); % Get the valid pixels
valid = any(pix(:) ~= val);
% Add either 0 or 1 depending if we have found any matching pixels
if(valid == 1)
img = insertMarker(img, [r c], 'x', 'color', 'red', 'size', 10);
imwrite(img, images(i).name,'tiff');
end
count = count + valid;
end
An easier way to do this would be to use indexing to grab a neighbourhood, then to check to see if any of the pixels in the neighbourhood have the value that you're looking for, use any on a flattened version of this neighbourhood. The trick with grabbing the right neighbourhood is to first generate a 2D grid of coordinates that span the entire dimensions of your image, then simply use the equation of a circle with the centre of it being each coordinate you are looking at and determine those locations that satisfy the following equation:
(x - a)^2 + (y - b)^2 <= N^2
N is the radius of the observation window, (a, b) is a coordinate of interest while (x, y) is a coordinate in the image. Use meshgrid to generate the coordinates.
You would use the above equation to create a logical mask, index into your image to pull the locations that are valid within the mask and check how many pixels match the one you want. Another added benefit with the above approach is that you are not subject to any out of bounds errors. Because you are pre-generating the list of all valid coordinates in your image, generating the mask will confine you within the boundaries of the image so you never have to check for out of boundaries conditions.... even when you specify coordinates to search that are out of bounds.
Specifically, assuming your image is stored in img, you would do:
count = 0; % Remembers total count of pixels matching a value
val = 0; % Value to match
N = 50; % Radius of neighbourhood
% Generate 2D grid of coordinates
[x, y] = meshgrid(1 : size(img, 2), 1 : size(img, 1));
% For each coordinate to check...
for kk = 1 : size(coord, 1)
a = coord(kk, 1); b = coord(kk, 2); % Get the pixel locations
mask = (x - a).^2 + (y - b).^2 <= N*N; % Get a mask of valid locations
% within the neighbourhood
pix = img(mask); % Get the valid pixels
count = count + any(pix(:) == val); % Add either 0 or 1 depending if
% we have found any matching pixels
end
The proposed solution:
fc = repmat(-5:5,11,1);
I = (fc.^2+fc'.^2)<=25;
fc_x = fc(I);
fc_y = fc'; fc_y = fc_y(I);
for i = 1:length(x_coord)
x_toCheck = fc_x + x_coord(i);
y_toCheck = fc_y + y_coord(i);
I = x_toCheck>0 & x_toCheck<=yourImageWidth;
I = I.*(y_toCheck>0 & y_toCheck<=yourImageHeight);
x_toCheck = x_toCheck(logical(I));
y_toCheck = y_toCheck(logical(I));
count = sum(img(x_toCheck(:),y_toCheck(:)) == 0);
end
If your img function can only check one pixel at a time, just add a for loop:
for i = 1:length(x_coord)
x_toCheck = fc_x + x_coord(i);
y_toCheck = fc_y + y_coord(i);
I = x_toCheck>0 & x_toCheck<=yourImageWidth;
I = I.*(y_toCheck>0 & y_toCheck<=yourImageHeight);
x_toCheck = x_toCheck(logical(I));
y_toCheck = y_toCheck(logical(I));
for j = 1:length(x_toCheck)
count = count + (img(x_toCheck(j),y_toCheck(j)) == 0);
end
end
Step-by-step:
You first need to get all the coordinates within 5 pixels range of the given coordinate.
We start by building a square of 11 pixels in length/width.
fc = repmat(-5:5,11,1);
fc_x = fc;
fc_y = fc';
plot(fc_x,fc_y,'.');
We now need to build a filter to get rid of those points outside the 5-pixel radius.
I = (fc.^2+fc'.^2)<=25;
Apply the filter, so we can get a circle of 5-pixel radius.
fc_x = fc_x(I);
fc_y = fc_y(I);
Next translate the centre of the circle to the given coordinate:
x_toCheck = fc_x + x_coord(i);
y_toCheck = fc_y + y_coord(i);
You need to check whether part of the circle is outside the range of your image:
I = x_toCheck>0 & x_toCheck<=yourImageWidth;
I = I.*(y_toCheck>0 & y_toCheck<=yourImageHeight);
x_toCheck = x_toCheck(logical(I));
y_toCheck = y_toCheck(logical(I));
Finally count the pixels:
count = sum(img(x_toCheck,y_toCheck) == 0);

matlab: efficient computation of local histograms within circular neighboorhoods

I've an image over which I would like to compute a local histogram within a circular neighborhood. The size of the neighborhood is given by a radius. Although the code below does the job, it's computationally expensive. I run the profiler and the way I'm accessing to the pixels within the circular neighborhoods is already expensive.
Is there any sort of improvement/optimization based maybe on vectorization? Or for instance, storing the neighborhoods as columns?
I found a similar question in this post and the proposed solution is quite in the spirit of the code below, however the solution is still not appropriate to my case. Any ideas are really welcomed :-) Imagine for the moment, the image is binary, but the method should also ideally work with gray-level images :-)
[rows,cols] = size(img);
hist_img = zeros(rows, cols, 2);
[XX, YY] = meshgrid(1:cols, 1:rows);
for rr=1:rows
for cc=1:cols
distance = sqrt( (YY-rr).^2 + (XX-cc).^2 );
mask_radii = (distance <= radius);
bwresponses = img(mask_radii);
[nelems, ~] = histc(double(bwresponses),0:255);
% do some processing over the histogram
...
end
end
EDIT 1 Given the received feedback, I tried to update the solution. However, it's not yet correct
radius = sqrt(2.0);
disk = diskfilter(radius);
fun = #(x) histc( x(disk>0), min(x(:)):max(x(:)) );
output = im2col(im, size(disk), fun);
function disk = diskfilter(radius)
height = 2*ceil(radius)+1;
width = 2*ceil(radius)+1;
[XX,YY] = meshgrid(1:width,1:height);
dist = sqrt((XX-ceil(width/2)).^2+(YY-ceil(height/2)).^2);
circfilter = (dist <= radius);
end
Following on the technique I described in my answer to a similar question you could try to do the following:
compute the index offsets from a particular voxel that get you to all the neighbors within a radius
Determine which voxels have all neighbors at least radius away from the edge
Compute the neighbors for all these voxels
Generate your histograms for each neighborhood
It is not hard to vectorize this, but note that
It will be slow when the neighborhood is large
It involves generating an intermediate matrix that is NxM (N = voxels in image, M = voxels in neighborhood) which could get very large
Here is the code:
% generate histograms for neighborhood within radius r
A = rand(200,200,200);
radius = 2.5;
tic
sz=size(A);
[xx yy zz] = meshgrid(1:sz(2), 1:sz(1), 1:sz(3));
center = round(sz/2);
centerPoints = find((xx - center(1)).^2 + (yy - center(2)).^2 + (zz - center(3)).^2 < radius.^2);
centerIndex = sub2ind(sz, center(1), center(2), center(3));
% limit to just the points that are "far enough on the inside":
inside = find(xx > radius+1 & xx < sz(2) - radius & ...
yy > radius + 1 & yy < sz(1) - radius & ...
zz > radius + 1 & zz < sz(3) - radius);
offsets = centerPoints - centerIndex;
allPoints = 1:prod(sz);
insidePoints = allPoints(inside);
indices = bsxfun(#plus, offsets, insidePoints);
hh = histc(A(indices), 0:0.1:1); % <<<< modify to give you the histogram you want
toc
A 2D version of the same code (which might be all you need, and is considerably faster):
% generate histograms for neighborhood within radius r
A = rand(200,200);
radius = 2.5;
tic
sz=size(A);
[xx yy] = meshgrid(1:sz(2), 1:sz(1));
center = round(sz/2);
centerPoints = find((xx - center(1)).^2 + (yy - center(2)).^2 < radius.^2);
centerIndex = sub2ind(sz, center(1), center(2));
% limit to just the points that are "far enough on the inside":
inside = find(xx > radius+1 & xx < sz(2) - radius & ...
yy > radius + 1 & yy < sz(1) - radius);
offsets = centerPoints - centerIndex;
allPoints = 1:prod(sz);
insidePoints = allPoints(inside);
indices = bsxfun(#plus, offsets, insidePoints);
hh = histc(A(indices), 0:0.1:1); % <<<< modify to give you the histogram you want
toc
You're right, I don't think that colfilt can be used as you're not applying a filter. You'll have to check the correctness, but here's my attempt using im2col and your diskfilter function (I did remove the conversion to double so it now output logicals):
function circhist
% Example data
im = randi(256,20)-1;
% Ranges - I do this globally for the whole image rather than for each neighborhood
mini = min(im(:));
maxi = max(im(:));
edges = linspace(mini,maxi,20);
% Disk filter
radius = sqrt(2.0);
disk = diskfilter(radius); % Returns logical matrix
% Pad array with -1
im_pad = padarray(im, (size(disk)-1)/2, -1);
% Convert sliding neighborhoods to columns
B = im2col(im_pad, size(disk), 'sliding');
% Get elements from each column that correspond to disk (logical indexing)
C = B(disk(:), :);
% Apply histogram across columns to count number of elements
out = histc(C, edges)
% Display output
figure
imagesc(out)
h = colorbar;
ylabel(h,'Counts');
xlabel('Neighborhood #')
ylabel('Bins')
axis xy
function disk = diskfilter(radius)
height = 2*ceil(radius)+1;
width = 2*ceil(radius)+1;
[XX,YY] = meshgrid(1:width,1:height);
dist = sqrt((XX-ceil(width/2)).^2+(YY-ceil(height/2)).^2);
disk = (dist <= radius);
If you want to set your ranges (edges) based on each neighborhood then you'll need to make sure that the vector is always the same length if you want to build a big matrix (and then the rows of that matrix won't correspond to each other).
You should note that the shape of the disk returned by fspecial is not as circular as what you were using. It's meant to be used a smoothing/averaging filter so the edges are fuzzy (anti-aliased). Thus when you use ~=0 it will grab more pixels. It'd stick with your own function, which is faster anyways.
You could try processing with an opposite logic (as briefly explained in the comment)
hist = zeros(W+2*R, H+2*R, Q);
for i = 1:R+1;
for j = 1:R+1;
if ((i-R-1)^2+(j-R-1)^2 < R*R)
for q = 0:1:Q-1;
hist(i:i+W-1,j:j+H-1,q+1) += (image == q);
end
end
end
end

Algorithm to subdivide a polygon in smaller polygons

I have a polygon made of successive edges on a plane, and would like to subdivide it in sub-polygons being triangles or rectangles.
Where can I find an algorithm to do this ?
Thanks !
In computational geometry, the problem you want to solve is called triangulation.
There are algorithms to solve this problem, giving triangulations with different properties. You will need to decide which one is the best fit.
I was looking for an answer for this myself but couldn't find one. Tried to stitch together several pieces and here's the result.
This is not necessarily the most optimal routine but it did the job for me. If you want to increase performance, try experimenting with the code.
A brief description of the algorithm:
Using the boundaries of the original geometry itself, and the boundaries of its convex hull, and its minimum rotated rectangle, derive all possible rectangles.
Divide all rectangles into smaller squares of specified side length.
Drop duplicates using a rounded off centroid. (r: round off param)
Retain either those squares 'within' the geometry, or those that 'intersect' the geometry, depending on whichever is closer to the total number of required squares.
EDITED
New Solution
#### Python script for dividing any shapely polygon into smaller equal sized polygons
import numpy as np
from shapely.ops import split
import geopandas
from shapely.geometry import MultiPolygon, Polygon
def rhombus(square):
"""
Naively transform the square into a Rhombus at a 45 degree angle
"""
coords = square.boundary.coords.xy
xx = list(coords[0])
yy = list(coords[1])
radians = 1
points = list(zip(xx, yy))
Rhombus = Polygon(
[
points[0],
points[1],
points[3],
((2 * points[3][0]) - points[2][0], (2 * points[3][1]) - points[2][1]),
points[4],
]
)
return Rhombus
def get_squares_from_rect(RectangularPolygon, side_length=0.0025):
"""
Divide a Rectangle (Shapely Polygon) into squares of equal area.
`side_length` : required side of square
"""
rect_coords = np.array(RectangularPolygon.boundary.coords.xy)
y_list = rect_coords[1]
x_list = rect_coords[0]
y1 = min(y_list)
y2 = max(y_list)
x1 = min(x_list)
x2 = max(x_list)
width = x2 - x1
height = y2 - y1
xcells = int(np.round(width / side_length))
ycells = int(np.round(height / side_length))
yindices = np.linspace(y1, y2, ycells + 1)
xindices = np.linspace(x1, x2, xcells + 1)
horizontal_splitters = [
LineString([(x, yindices[0]), (x, yindices[-1])]) for x in xindices
]
vertical_splitters = [
LineString([(xindices[0], y), (xindices[-1], y)]) for y in yindices
]
result = RectangularPolygon
for splitter in vertical_splitters:
result = MultiPolygon(split(result, splitter))
for splitter in horizontal_splitters:
result = MultiPolygon(split(result, splitter))
square_polygons = list(result)
return square_polygons
def split_polygon(G, side_length=0.025, shape="square", thresh=0.9):
"""
Using a rectangular envelope around `G`, creates a mesh of squares of required length.
Removes non-intersecting polygons.
Args:
- `thresh` : Range - [0,1]
This controls - the number of smaller polygons at the boundaries.
A thresh == 1 will only create (or retain) smaller polygons that are
completely enclosed (area of intersection=area of smaller polygon)
by the original Geometry - `G`.
A thresh == 0 will create (or retain) smaller polygons that
have a non-zero intersection (area of intersection>0) with the
original geometry - `G`
- `side_length` : Range - (0,infinity)
side_length must be such that the resultant geometries are smaller
than the original geometry - `G`, for a useful result.
side_length should be >0 (non-zero positive)
- `shape` : {square/rhombus}
Desired shape of subset geometries.
"""
assert side_length>0, "side_length must be a float>0"
Rectangle = G.envelope
squares = get_squares_from_rect(Rectangle, side_length=side_length)
SquareGeoDF = geopandas.GeoDataFrame(squares).rename(columns={0: "geometry"})
Geoms = SquareGeoDF[SquareGeoDF.intersects(G)].geometry.values
if shape == "rhombus":
Geoms = [rhombus(g) for g in Geoms]
geoms = [g for g in Geoms if ((g.intersection(G)).area / g.area) >= thresh]
elif shape == "square":
geoms = [g for g in Geoms if ((g.intersection(G)).area / g.area) >= thresh]
return geoms
# Reading geometric data
geo_filepath = "/data/geojson/pc_14.geojson"
GeoDF = geopandas.read_file(geo_filepath)
# Selecting random shapely-geometry
G = np.random.choice(GeoDF.geometry.values)
squares = split_polygon(G,shape='square',thresh=0.5,side_length=0.025)
rhombuses = split_polygon(G,shape='rhombus',thresh=0.5,side_length=0.025)
Previous Solution:
import numpy as np
import geopandas
from shapely.ops import split
from shapely.geometry import MultiPolygon, Polygon, Point, MultiPoint
def get_rect_from_geom(G, r=2):
"""
Get rectangles from a geometry.
r = rounding factor.
small r ==> more rounding off ==> more rectangles
"""
coordinate_arrays = G.exterior.coords.xy
coordinates = list(
zip(
[np.round(c, r) for c in coordinate_arrays[0]],
[np.round(c, r) for c in coordinate_arrays[1]],
)
)
Rectangles = []
for c1 in coordinates:
Coords1 = [a for a in coordinates if a != c1]
for c2 in Coords1:
Coords2 = [b for b in Coords1 if b != c2]
x1, y1 = c1[0], c1[1]
x2, y2 = c2[0], c2[1]
K1 = [k for k in Coords2 if k == (x1, y2)]
K2 = [k for k in Coords2 if k == (x2, y1)]
if (len(K1) > 0) & (len(K2) > 0):
rect = [list(c1), list(K1[0]), list(c2), list(K2[0])]
Rectangles.append(rect)
return Rectangles
def get_squares_from_rect(rect, side_length=0.0025):
"""
Divide a rectangle into equal area squares
side_length = required side of square
"""
y_list = [r[1] for r in rect]
x_list = [r[0] for r in rect]
y1 = min(y_list)
y2 = max(y_list)
x1 = min(x_list)
x2 = max(x_list)
width = x2 - x1
height = y2 - y1
xcells, ycells = int(np.round(width / side_length)), int(
np.round(height / side_length)
)
yindices = np.linspace(y1, y2, ycells + 1)
xindices = np.linspace(x1, x2, xcells + 1)
horizontal_splitters = [
LineString([(x, yindices[0]), (x, yindices[-1])]) for x in xindices
]
vertical_splitters = [
LineString([(xindices[0], y), (xindices[-1], y)]) for y in yindices
]
result = Polygon(rect)
for splitter in vertical_splitters:
result = MultiPolygon(split(result, splitter))
for splitter in horizontal_splitters:
result = MultiPolygon(split(result, splitter))
square_polygons = list(result)
return [np.stack(SQPOLY.exterior.coords.xy, axis=1) for SQPOLY in square_polygons]
def round_centroid(g, r=10):
"""
Get Centroids.
Round off centroid coordinates to `r` decimal points.
"""
C = g.centroid.coords.xy
return (np.round(C[0][0], r), np.round(C[1][0], r))
def subdivide_polygon(g, side_length=0.0025, r=10):
"""
1. Create all possible rectangles coordinates from the geometry, its minimum rotated rectangle, and its convex hull.
2. Divide all rectangles into smaller squares.
small r ==> more rounding off ==> fewer overlapping squares. (these are dropped as duplicates)
large r ==> may lead to a few overlapping squares.
"""
# Number of squares required.
num_squares_reqd = g.area // (side_length ** 2)
# Some of these combinations can be dropped to improve performance.
Rectangles = []
Rectangles.extend(get_rect_from_geom(g))
Rectangles.extend(get_rect_from_geom(g.minimum_rotated_rectangle))
Rectangles.extend(get_rect_from_geom(g.convex_hull))
Squares = []
for r in range(len(Rectangles)):
rect = Rectangles[r]
Squares.extend(get_squares_from_rect(rect, side_length=side_length))
SquarePolygons = [Polygon(square) for square in Squares]
GDF = geopandas.GeoDataFrame(SquarePolygons).rename(columns={0: "geometry"})
GDF.loc[:, "centroid"] = GDF.geometry.apply(round_centroid, r=r)
GDF = GDF.drop_duplicates(subset=["centroid"])
wgeoms = GDF[GDF.within(g)].geometry.values
igeoms = GDF[GDF.intersects(g)].geometry.values
w = abs(num_squares_reqd - len(wgeoms))
i = abs(num_squares_reqd - len(igeoms))
print(w, i)
if w <= i:
return wgeoms
else:
return igeoms
geoms = subdivide(g)
Stumbled across this after many searches.
Thanks #Aditya Chhabra for your submission, it works great but get_squares_from_rect is very slow for small side lengths due to iterative clips.
We can do this instantaneously if we combine all LineStrings into a single collection, then clip and polygonize in one step, which I found in in this question.
Previously side lengths of 0.0001 (EPSG:4326) took > 1 minute, now it takes no time.
from shapely.ops import unary_union, polygonize, linemerge
from shapely.geometry import LineString
import numpy as np
def get_squares_from_rect_faster(RectangularPolygon, side_length=0.0025):
rect_coords = np.array(RectangularPolygon.boundary.coords.xy)
y_list = rect_coords[1]
x_list = rect_coords[0]
y1 = min(y_list)
y2 = max(y_list)
x1 = min(x_list)
x2 = max(x_list)
width = x2 - x1
height = y2 - y1
xcells = int(np.round(width / side_length))
ycells = int(np.round(height / side_length))
yindices = np.linspace(y1, y2, ycells + 1)
xindices = np.linspace(x1, x2, xcells + 1)
horizontal_splitters = [
LineString([(x, yindices[0]), (x, yindices[-1])]) for x in xindices
]
vertical_splitters = [
LineString([(xindices[0], y), (xindices[-1], y)]) for y in yindices
]
lines = horizontal_splitters + vertical_splitters
lines.append(RectangularPolygon.boundary)
lines = unary_union(lines)
lines = linemerge(lines)
return list(polygonize(lines))

Resources