Connect the nearest points in segment and label segment - python
I using Open CV and skimage for document analysis of datasheets.
I am trying to segment out the shade region separately .
I am currently able to segment out the part and number as different clusters.
Using felzenszwalb() from skimage I segment the parts:
import matplotlib.pyplot as plt
import numpy as np
from skimage.segmentation import felzenszwalb
from skimage.io import imread
img = imread('test.jpg')
segments_fz = felzenszwalb(img, scale=100, sigma=0.2, min_size=50)
print("Felzenszwalb number of segments {}".format(len(np.unique(segments_fz))))
plt.imshow(segments_fz)
plt.tight_layout()
plt.show()
But not able to connect them. Any idea to connect methodically and label out the corresponding segment with part and part number would of great help .
Thanks in advance for your time – if I’ve missed out anything, over- or under-emphasised a specific point let me know in the comments.
Preliminaries
Some preliminary code:
%matplotlib inline
%load_ext Cython
import numpy as np
import cv2
from matplotlib import pyplot as plt
import skimage as sk
import skimage.morphology as skm
import itertools
def ShowImage(title,img,ctype):
plt.figure(figsize=(20, 20))
if ctype=='bgr':
b,g,r = cv2.split(img) # get b,g,r
rgb_img = cv2.merge([r,g,b]) # switch it to rgb
plt.imshow(rgb_img)
elif ctype=='hsv':
rgb = cv2.cvtColor(img,cv2.COLOR_HSV2RGB)
plt.imshow(rgb)
elif ctype=='gray':
plt.imshow(img,cmap='gray')
elif ctype=='rgb':
plt.imshow(img)
else:
raise Exception("Unknown colour type")
plt.axis('off')
plt.title(title)
plt.show()
For reference, here's your original image:
#Read in image
img = cv2.imread('part.jpg')
ShowImage('Original',img,'bgr')
Identifying Numbers
To simplify things, we'll want to classify pixels as being either on or off. We can do so with thresholding. Since our image contains two clear classes of pixels (black and white), we can use Otsu's method. We'll invert the colour scheme since the libraries we're using consider black pixels boring and white pixels interesting.
#Convert image to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#Apply Otsu's method to eliminate pixels of intermediate colour
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
ShowImage('Applying Otsu',thresh,'gray')
#Verify that pixels are either black or white and nothing in between
np.unique(thresh)
Our strategy will be to locate numbers and then follow the line(s) near them to parts and then to label those parts. Since, conveniently, all of the Arabic numerals are formed from contiguous pixels, we can start by finding the connected components.
ret, components = cv2.connectedComponents(thresh)
#Each component is a different colour
ShowImage('Connected Components', components, 'rgb')
We can then filter the connected components to find the numbers by filtering for dimension. Note that this is not a super robust method of doing this. A better option would be to use character recognition, but this is left as an exercise to the reader :-)
class Box:
def __init__(self,x0,x1,y0,y1):
self.x0, self.x1, self.y0, self.y1 = x0,x1,y0,y1
def overlaps(self,box2,tol):
if self.x0 is None or box2.x0 is None:
return False
return not (self.x1+tol<=box2.x0 or self.x0-tol>=box2.x1 or self.y1+tol<=box2.y0 or self.y0-tol>=box2.y1)
def merge(self,box2):
self.x0 = min(self.x0,box2.x0)
self.x1 = max(self.x1,box2.x1)
self.y0 = min(self.y0,box2.y0)
self.y1 = max(self.y1,box2.y1)
box2.x0 = None #Used to mark `box2` as being no longer valid. It can be removed later
def dist(self,x,y):
#Get center point
ax = (self.x0+self.x1)/2
ay = (self.y0+self.y1)/2
#Get distance to center point
return np.sqrt((ax-x)**2+(ay-y)**2)
def good(self):
return not (self.x0 is None)
def ExtractComponent(original_image, component_matrix, component_number):
"""Extracts a component from a ConnectedComponents matrix"""
#Create a true-false matrix indicating if a pixel is part of a particular component
is_component = component_matrix==component_number
#Find the coordinates of those pixels
coords = np.argwhere(is_component)
# Bounding box of non-black pixels.
y0, x0 = coords.min(axis=0)
y1, x1 = coords.max(axis=0) + 1 # slices are exclusive at the top
# Get the contents of the bounding box.
return x0,x1,y0,y1,original_image[y0:y1, x0:x1]
numbers_img = thresh.copy() #This is used purely to show that we can identify numbers
numbers = []
for component in range(components.max()):
tx0,tx1,ty0,ty1,this_component = ExtractComponent(thresh, components, component)
#ShowImage('Component #{0}'.format(component), this_component, 'gray')
cheight, cwidth = this_component.shape
#print(cwidth,cheight) #Enable this to see dimensions
#Identify numbers based on aspect ratio
if (abs(cwidth-14)<3 or abs(cwidth-7)<3) and abs(cheight-24)<3:
numbers_img[ty0:ty1,tx0:tx1] = 128
numbers.append(Box(tx0,tx1,ty0,ty1))
ShowImage('Numbers', numbers_img, 'gray')
We now connect the numbers into contiguous blocks by expanding their bounding boxes slightly and looking for overlaps.
#This is kind of a silly way to do this, but it will work find for small quantities (hundreds)
merged=True #If true, then a merge happened this round
while merged: #Continue until there are no more mergers
merged=False #Reset merge indicator
for a,b in itertools.combinations(numbers,2): #Consider all pairs of numbers
if a.overlaps(b,10): #If this pair overlaps
a.merge(b) #Merge it
merged=True #Make a note that we've merged
numbers = [x for x in numbers if x.good()] #Eliminate those boxes that were gobbled by the mergers
#This is used purely to show that we can identify numbers
numbers_img = thresh.copy()
for n in numbers:
numbers_img[n.y0:n.y1,n.x0:n.x1] = 128
thresh[n.y0:n.y1,n.x0:n.x1] = 0 #Drop numbers from thresholded image
ShowImage('Numbers', numbers_img, 'gray')
Okay, so now we've identified the numbers! We'll use these later to identify parts.
Identifying Arrows
Next, we'll want to figure out what parts the numbers are pointing to. To do so, we want to detect lines. The Hough transform is good for this. To reduce the number of false positives, we skeletonize the data, which transforms it into a representation which is at most one pixel wide.
skel = sk.img_as_ubyte(skm.skeletonize(thresh>0))
ShowImage('Skeleton', skel, 'gray')
Now we perform the Hough transform. We're looking for one that identifies all of the lines going from the numbers to the parts. Getting this right may take some fiddling with the parameters.
lines = cv2.HoughLinesP(
skel,
1, #Resolution of r in pixels
np.pi / 180, #Resolution of theta in radians
30, #Minimum number of intersections to detect a line
None,
80, #Min line length
10 #Max line gap
)
lines = [x[0] for x in lines]
line_img = thresh.copy()
line_img = cv2.cvtColor(line_img, cv2.COLOR_GRAY2BGR)
for l in lines:
color = tuple(map(int, np.random.randint(low=0, high=255, size=3)))
cv2.line(line_img, (l[0], l[1]), (l[2], l[3]), color, 3, cv2.LINE_AA)
ShowImage('Lines', line_img, 'bgr')
We now want to find the line or lines which are closest to each number and retain only these. We're essentially filtering out all of the lines which are not arrows. To do so, we compare the end points of each line to the center point of each number box.
comp_labels = np.zeros(img.shape[0:2], dtype=np.uint8)
for n_idx,n in enumerate(numbers):
distvals = []
for i,l in enumerate(lines):
#Distances from each point of line to midpoint of rectangle
dists = [n.dist(l[0],l[1]),n.dist(l[2],l[3])]
#Minimum distance and the end point (0 or 1) of the line associated with that point
#Tuples of (Line Number, Line Point, Dist to Line Point) are produced
distvals.append( (i,np.argmin(dists),np.min(dists)) )
#Sort by distance between the number box and the line
distvals = sorted(distvals, key=lambda x: x[2])
#Include nearby lines, not just the closest one. This accounts for forking.
distvals = [x for x in distvals if x[2]<1.5*distvals[0][2]]
#Draw a white rectangle where the number box was
cv2.rectangle(comp_labels, (n.x0,n.y0), (n.x1,n.y1), 1, cv2.FILLED)
#Draw white lines where the arrows are
for dv in distvals:
l = lines[dv[0]]
lp = (l[0],l[1]) if dv[1]==0 else (l[2],l[3])
cv2.line(comp_labels, (l[0], l[1]), (l[2], l[3]), 1, 3, cv2.LINE_AA)
cv2.line(comp_labels, (lp[0], lp[1]), ((n.x0+n.x1)//2, (n.y0+n.y1)//2), 1, 3, cv2.LINE_AA)
ShowImage('Lines', comp_labels, 'gray')
Finding Parts
This part was hard! We now want to segment the parts in the image. If there was some way to disconnect the lines linking subparts together, this would be easy. Unfortunately, the lines connecting the subparts are the same width as many of the lines which constitute the parts.
To work around this, we could use a lot of logic. It would be painful and error-prone.
Alternatively, we could assume you have an expert-in-the-loop. This expert's sole job is to cut the lines connecting the subparts. This should be both easy and fast for them. Labeling everything would be slow and sad for humans, but is fast for computers. Separating things is easy for humans, but hard for computers. So we let both do what they do best.
In this case, you could probably train someone to do this job in a few minutes, so a true "expert" isn't really necessary. Just a mildly competent human.
If you pursue this, you'll need to write the expert in the loop tool. To do so, save the skeleton images, have your expert modify them, and read the skeletonized images back in. Like so.
#Save the image, or display it on a GUI
#cv2.imwrite("/z/skel.png", skel);
#EXPERT DOES THEIR THING HERE
#Read the expert-mediated image back in
skelhuman = cv2.imread('/z/skel.png')
#Convert back to the form we need
skelhuman = cv2.cvtColor(skelhuman,cv2.COLOR_BGR2GRAY)
ret, skelhuman = cv2.threshold(skelhuman,0,255,cv2.THRESH_OTSU)
ShowImage('SkelHuman', skelhuman, 'gray')
Now that we have the parts separated, we'll eliminate as much of the arrows as possible. We've already extracted these above, so we can add them back later if we need to.
To eliminate the arrows, we'll find all of the lines that terminate in locations other than by another line. That is, we'll locate pixels which have only one neighbouring pixel. We'll then eliminate the pixel and look at its neighbour. Doing this iteratively eliminates the arrows. Since I don't know another term for it, I'll call this a Fuse Transform. Since this will require manipulating individual pixels, which would be super slow in Python, we'll write the transform in Cython.
%%cython -a --cplus
import cython
from libcpp.queue cimport queue
import numpy as np
cimport numpy as np
#cython.boundscheck(False)
#cython.wraparound(False)
#cython.nonecheck(False)
#cython.cdivision(True)
cpdef void FuseTransform(unsigned char [:, :] image):
# set the variable extension types
cdef int c, x, y, nx, ny, width, height, neighbours
cdef queue[int] q
# grab the image dimensions
height = image.shape[0]
width = image.shape[1]
cdef int dx[8]
cdef int dy[8]
#Offsets to neighbouring cells
dx[:] = [-1,-1,0,1,1,1,0,-1]
dy[:] = [0,-1,-1,-1,0,1,1,1]
#Find seed cells: those with only one neighbour
for y in range(1, height-1):
for x in range(1, width-1):
if image[y,x]==0: #Seed cells cannot be blank cells
continue
neighbours = 0
for n in range(0,8): #Looks at all neighbours
nx = x+dx[n]
ny = y+dy[n]
if image[ny,nx]>0: #This neighbour has a value
neighbours += 1
if neighbours==1: #Was there only one neighbour?
q.push(y*width+x) #If so, this is a seed cell
#Starting with the seed cells, gobble up the lines
while not q.empty():
c = q.front()
q.pop()
y = c//width #Convert flat index into 2D x-y index
x = c%width
image[y,x] = 0 #Gobble up this part of the fuse
neighbour = -1 #No neighbours yet
for n in range(0,8): #Look at all neighbours
nx = x+dx[n] #Find coordinates of neighbour cells
ny = y+dy[n]
#If the neighbour would be off the side of the matrix, ignore it
if nx<0 or ny<0 or nx==width or ny==height:
continue
if image[ny,nx]>0: #Is the neighbouring cell active?
if neighbour!=-1: #If we've already found an active neighbour
neighbour=-1 #Then pretend we found no neighbours
break #And stop looking. This is the end of the fuse.
else: #Otherwise, make a note of the neighbour's index.
neighbour = ny*width+nx
if neighbour!=-1: #If there was only one neighbour
q.push(neighbour) #Continue burning the fuse
Back in standard Python:
#Apply the Fuse Transform
skh_dilated=skelhuman.copy()
FuseTransform(skh_dilated)
ShowImage('Fuse Transform', skh_dilated, 'gray')
Now that we've eliminated all of the arrows and lines connecting the parts, we dilate the remaining pixels a lot.
kernel = np.ones((3,3),np.uint8)
dilated = cv2.dilate(skh_dilated, kernel, iterations=6)
ShowImage('Dilation', dilated, 'gray')
Putting It All Together
And overlay the labels and arrows we segmented out earlier...
comp_labels_dilated = cv2.dilate(comp_labels, kernel, iterations=5)
labels_combined = np.uint8(np.logical_or(comp_labels_dilated,dilated))
ShowImage('Comp Labels', labels_combined, 'gray')
Finally, we take the merged number boxes, component arrows, and parts and color each of them using pretty colors from Color Brewer. We then overlay this on the original image to obtain the desired highlighting.
ret, labels = cv2.connectedComponents(labels_combined)
colormask = np.zeros(img.shape, dtype=np.uint8)
#Colors from Color Brewer
colors = [(228,26,28),(55,126,184),(77,175,74),(152,78,163),(255,127,0),(255,255,51),(166,86,40),(247,129,191),(153,153,153)]
for l in range(labels.max()):
if l==0: #Background component
colormask[labels==0] = (255,255,255)
else:
colormask[labels==l] = colors[l]
ShowImage('Comp Labels', colormask, 'bgr')
blended = cv2.addWeighted(img,0.7,colormask,0.3,0)
ShowImage('Blended', blended, 'bgr')
The final image
So, to recap, we identified numbers, arrows, and parts. In some cases, we were able to separate them automatically. In other cases, we used expert in the loop. Where we had to manipulate pixels individually, we used Cython for speed.
Of course, the danger with this sort of thing is that some other image will break the (many) assumptions I've made here. But that's a risk that you take when you try to use a single image to present a problem.
Related
Extract street network from a raster image
I have a 512x512 image of a street grid: I'd like to extract polylines for each of the streets in this image (large blue dots = intersections, small blue dots = points along polylines): I've tried a few techniques! One idea was to start with skeletonize to compress the streets down to 1px wide lines: from skimage import morphology morphology.skeletonize(streets_data)) Unfortunately this has some gaps that break the connectivity of the street network; I'm not entirely sure why, but my guess is that this is because some of the streets are 1px narrower in some places and 1px wider in others. (update: the gaps aren't real; they're entirely artifacts of how I was displaying the skeleton. See this comment for the sad tale. The skeleton is well-connected.) I can patch these using a binary_dilation, at the cost of making the streets somewhat variable width again: out = morphology.skeletonize(streets_data) out = morphology.binary_dilation(out, morphology.selem.disk(1)) With a re-connected grid, I can run the Hough transform to find line segments: import cv2 rho = 1 # distance resolution in pixels of the Hough grid theta = np.pi / 180 # angular resolution in radians of the Hough grid threshold = 8 # minimum number of votes (intersections in Hough grid cell) min_line_length = 10 # minimum number of pixels making up a line max_line_gap = 2 # maximum gap in pixels between connectable line segments # Run Hough on edge detected image # Output "lines" is an array containing endpoints of detected line segments lines = cv2.HoughLinesP( out, rho, theta, threshold, np.array([]), min_line_length, max_line_gap ) line_image = streets_data.copy() for i, line in enumerate(lines): for x1,y1,x2,y2 in line: cv2.line(line_image,(x1,y1),(x2,y2), 2, 1) This produces a whole jumble of overlapping line segments, along with some gaps (look at the T intersection on the right side): At this point I could try to de-dupe overlapping line segments, but it's not really clear to me that this is a path towards a solution, especially given that gap. Are there more direct methods available to get at the network of polylines I'm looking for? In particular, what are some methods for: Finding the intersections (both four-way and T intersections). Shrinking the streets to all be 1px wide, allowing that there may be some variable width. Finding the polylines between intersections.
If you want to improve your "skeletonization", you could try the following algorithm to obtain the "1-px wide streets": import imageio import numpy as np from matplotlib import pyplot as plt from scipy.ndimage import distance_transform_edt from skimage.segmentation import watershed # read image image_rgb = imageio.imread('1mYBD.png') # convert to binary image_bin = np.max(image_rgb, axis=2) > 0 # compute the distance transform (only > 0) distance = distance_transform_edt(image_bin) # segment the image into "cells" (i.e. the reciprocal of the network) cells = watershed(distance) # compute the image gradients grad_v = np.pad(cells[1:, :] - cells[:-1, :], ((0, 1), (0, 0))) grad_h = np.pad(cells[:, 1:] - cells[:, :-1], ((0, 0), (0, 1))) # given that the cells have a constant value, # only the edges will have non-zero gradient edges = (abs(grad_v) > 0) + (abs(grad_h) > 0) # extract points into (x, y) coordinate pairs pos_v, pos_h = np.nonzero(edges) # display points on top of image plt.imshow(image_bin, cmap='gray_r') plt.scatter(pos_h, pos_v, 1, np.arange(pos_h.size), cmap='Spectral') The algorithm works on the "blocks" rather than the "streets", take a look into the cells image:
I was on the right track with skeleton; it does produce a connected, 1px wide version of the street grid. It's just that there was a bug in my display code (see this comment). Here's what the skeleton actually looks like: from skimage import morphology morphology.skeletonize(streets_data)) From here the reference to NEFI2 in RJ Adriaansen's comment was extremely helpful. I wasn't able to get an extraction pipeline to run on my image using their GUI, but I was able to cobble together something using their code and approach. Here's the general procedure I wound up with: Find candidate nodes. These are pixels in the skeleton with either one neighbor (end of a line) or 3+ neighbors (an intersection). NEFI2 calls this the Zhang-Suen algorithm. For four-way intersections, it produces multiple nodes so we need to merge them. Repeat until no two nodes are too close together: Use breadth-first search (flood fill) to connect nodes. If two connected nodes are within D of each other, merge them. Run shapely's simplify on the paths between nodes to get polylines. I put this all together in a repo here: extract-raster-network. This works pretty well on my test images! Here are some sample images:
Find minimal number of rectangles in the image
I have binary images where rectangles are placed randomly and I want to get the positions and sizes of those rectangles. If possible I want the minimal number of rectangles necessary to exactly recreate the image. On the left is my original image and on the right the image I get after applying scipys.find_objects() (like suggested for this question). import scipy # image = scipy.ndimage.zoom(image, 9, order=0) labels, n = scipy.ndimage.measurements.label(image, np.ones((3, 3))) bboxes = scipy.ndimage.measurements.find_objects(labels) img_new = np.zeros_like(image) for bb in bboxes: img_new[bb[0], bb[1]] = 1 This works fine if the rectangles are far apart, but if they overlap and build more complex structures this algorithm just gives me the largest bounding box (upsampling the image made no difference). I have the feeling that there should already exist a scipy or opencv method which does this. I would be glad to know if somebody has an idea on how to tackle this problem or even better knows of an existing solution. As result I want a list of rectangles (ie. lower-left-corner : upper-righ-corner) in the image. The condition is that when I redraw those filled rectangles I want to get exactly the same image as before. If possible the number of rectangles should be minimal. Here is the code for generating sample images (and a more complex example original vs scipy) import numpy as np def random_rectangle_image(grid_size, n_obstacles, rectangle_limits): n_dim = 2 rect_pos = np.random.randint(low=0, high=grid_size-rectangle_limits[0]+1, size=(n_obstacles, n_dim)) rect_size = np.random.randint(low=rectangle_limits[0], high=rectangle_limits[1]+1, size=(n_obstacles, n_dim)) # Crop rectangle size if it goes over the boundaries of the world diff = rect_pos + rect_size ex = np.where(diff > grid_size, True, False) rect_size[ex] -= (diff - grid_size)[ex].astype(int) img = np.zeros((grid_size,)*n_dim, dtype=bool) for i in range(n_obstacles): p_i = np.array(rect_pos[i]) ps_i = p_i + np.array(rect_size[i]) img[tuple(map(slice, p_i, ps_i))] = True return img img = random_rectangle_image(grid_size=64, n_obstacles=30, rectangle_limits=[4, 10])
Here is something to get you started: a naïve algorithm that walks your image and creates rectangles as large as possible. As it is now, it only marks the rectangles but does not report back coordinates or counts. This is to visualize the algorithm alone. It does not need any external libraries except for PIL, to load and access the left side image when saved as a PNG. I'm assuming a border of 15 pixels all around can be ignored. from PIL import Image def fill_rect (pixels,xp,yp,w,h): for y in range(h): for x in range(w): pixels[xp+x,yp+y] = (255,0,0,255) for y in range(h): pixels[xp,yp+y] = (255,192,0,255) pixels[xp+w-1,yp+y] = (255,192,0,255) for x in range(w): pixels[xp+x,yp] = (255,192,0,255) pixels[xp+x,yp+h-1] = (255,192,0,255) def find_rect (pixels,x,y,maxx,maxy): # assume we're at the top left # get max horizontal span width = 0 height = 1 while x+width < maxx and pixels[x+width,y] == (0,0,0,255): width += 1 # now walk down, adjusting max width while y+height < maxy: for w in range(x,x+width,1): if pixels[x,y+height] != (0,0,0,255): break if pixels[x,y+height] != (0,0,0,255): break height += 1 # fill rectangle fill_rect (pixels,x,y,width,height) image = Image.open('A.png') pixels = image.load() width, height = image.size print (width,height) for y in range(16,height-15,1): for x in range(16,width-15,1): if pixels[x,y] == (0,0,0,255): find_rect (pixels,x,y,width,height) image.show() From the output you can observe the detection algorithm can be improved, as, for example, the "obvious" two top left rectangles are split up into 3. Similar, the larger structure in the center also contains one rectangle more than absolutely needed. Possible improvements are either to adjust the find_rect routine to locate a best fit¹, or store the coordinates and use math (beyond my ken) to find which rectangles may be joined. ¹ A further idea on this. Currently all found rectangles are immediately filled with the "found" color. You could try to detect obviously multiple rectangles, and then, after marking the first, the other rectangle(s) to check may then either be black or red. Off the cuff I'd say you'd need to try different scan orders (top-to-bottom or reverse, left-to-right or reverse) to actually find the minimally needed number of rectangles in any combination.
Save individual segments from image segmentation
I've been using skimage.segmentation modules to find contiguous segments within an image. For example, segments quite nicely to I want to be able to view the distinct regions of the original image in isolation (such that the above image would result in 6 roughly rectangular sub-images). I have obtained some degree of success in doing this, but it's been difficult. Is there any pre-existing module I can use to accomplish this? If not, high-level algorthim advice would be appreciated. Approach thus far: image_slic = seg.slic(image, n_segments=6) borders = seg.find_boundaries(image_slic) sub_images = [] new_seg = [] for every row of borders: new_seg.append([]) for every pixel in every row: if (pixel is not a border and is not already processed): new_seg[-1].append(pixel) Mark pixel as processed elif (pixel is a border and is not already processed): break if (on the first pixel of a row OR the first unprocessed pixel): sub_images.append(new_seg) new_seg = [] With this approach, I can generate the four regions from the example image that border the left side without error. While it's not shown in the above pseudo-code, I'm also padding segments with transparent pixels to preserve their shape. This additional consideration makes finding right-side sub-images more difficult.
This can be readily accomplished through NumPy's boolean indexing: import numpy as np from skimage import io, segmentation import matplotlib.pyplot as plt n_segments = 6 fig_width = 2.5*n_segments img = io.imread('https://i.imgur.com/G44JEG7.png') segments = segmentation.slic(img, n_segments=n_segments) fig, ax = plt.subplots(1, n_segments) fig.set_figwidth(fig_width) for index in np.unique(segments): segment = img.copy() segment[segments!=index] = 0 ax[index].imshow(segment) ax[index].set(title=f'Segment {index}') ax[index].set_axis_off() plt.show(fig) You could obtain the same result using NumPy's where function like this: for index in np.unique(segments): segment = np.where(np.expand_dims(segments, axis=-1)==index, img, [0, 0, 0])
Correctly closing of polygons generated using skimage.measure.find_contours()
I'm currently using skimage.measure.find_contours() to find contours on a surface. Now that I've found the contours I need to able to find the area enclosed within them. When all of the vertices are within the data set this is fine as a have a fully enclosed polygon. However, how do I ensure the polygon is fully enclosed if the contour breaches the edge of the surface, either at an edge or at a corner? When this happens I would like to use the edge of the surface as additional vertices to close off the polygon. For example in the following image, with contours shown, you can see that the contours end at the edge of the image, how do I close them up? Also in the example of the brown contour, which is just a single line, I don't think I want an area returned, how would I single out this case? I know I can check for enclosed contours/polygons by checking if the last vertices of the polygon is the same as the first. I have code for calculating the area inside a polygon, taken from here def find_area(array): a = 0 ox,oy = array[0] for x,y in array[1:]: a += (x*oy-y*ox) ox,oy = x,y return -a/2 I just need help in closing off the polygons. And checking for the different cases that might occur. Thanks Update: After applying the solution suggested by #soupault I have this code: import numpy as np import matplotlib.pyplot as plt from skimage import measure # Construct some test data x, y = np.ogrid[-np.pi:np.pi:100j, -np.pi:np.pi:100j] r = np.sin(np.exp((np.sin(x)**3 + np.cos(y)**2))) # Coordinates of point of interest pt = [(49,75)] # Apply thresholding to the surface threshold = 0.8 blobs = r > threshold # Make a labelled image based on the thresholding regions blobs_labels = measure.label(blobs, background = 0) # Show the thresholded regions plt.figure() plt.imshow(blobs_labels, cmap='spectral') # Apply regionprops to charactersie each of the regions props = measure.regionprops(blobs_labels, intensity_image = r) # Loop through each region in regionprops, identify if the point of interest is # in that region. If so, plot the region and print it's area. plt.figure() plt.imshow(r, cmap='Greys') plt.plot(pt[0][0], pt[0][1],'rx') for prop in props: coords = prop.coords if np.sum(np.all(coords[:,[1,0]] == pt[0], axis=1)): plt.plot(coords[:,1],coords[:,0],'r.') print(prop.area) This solution assumes that each pixel is 1x1 in size. In my real data solution this isn't the case so I have also applied the following function to apply linear interpolation to the data. I believe you can also apply a similar function to make the area of each pixel smaller and increase the resolution of the data. import numpy as np from scipy import interpolate def interpolate_patch(x,y,patch): x_interp = np.arange(np.ceil(x[0]), x[-1], 1) y_interp = np.arange(np.ceil(y[0]), y[-1], 1) f = interpolate.interp2d(x, y, patch, kind='linear') patch_interp = f(x_interp, y_interp) return x_interp, y_interp, patch_interp
If you need to measure the properties of different regions, it is natural to start with finding the regions (not contours). The algorithm will be the following, in this case: Prepare a labeled image: 1.a Either fill the areas between different contour lines with the different colors; 1.b Or apply some image thresholding function, and then run skimage.measure.label (http://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.label); Execute regionprops using the very labeled image as an input (http://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.regionprops); Iterate over regions in regionprops and calculate the desired parameters (area, perimeter, etc). Once you identified the regions in your image via regionprops, you can call .coords for each of them to get the enclosed contour.
If someone will need close open contours by image edges (and make a polygon) here is: import shapely.geometry as sgeo import shapely.ops as sops def close_contour_with_image_edge(contour, image_shape): """ this function uses shapely because its easiest way to do that :param contour: contour generated by skimage.measure.find_contours() :param image_shape: tuple (row, cols), standard return of numpy shape() :return: """ # make contour linestring contour_line = sgeo.LineString(contour) # make image box linestring box_rows, box_cols = image_shape[0], image_shape[1] img_box = sgeo.LineString(coordinates=( (0, 0), (0, box_cols-1), (box_rows-1, box_cols-1), (box_rows-1, 0), (0, 0) )) # intersect box with non-closed contour and get shortest line which touch both of contour ends edge_points = img_box.intersection(contour_line) edge_parts = sops.split(img_box, edge_points) edge_parts = list(part for part in edge_parts.geoms if part.touches(edge_points.geoms[0]) and part.touches(edge_points.geoms[1])) edge_parts.sort(reverse=False, key=lambda x: x.length) contour_edge = edge_parts[0] # weld it contour_line = contour_line.union(contour_edge) contour_line = sops.linemerge(contour_line) contour_polygon = sgeo.Polygon(contour_line.coords) return contour_polygon
Get cells indexes of raster file inside a rasterized border line in python
I have a closed line made of raster cells of which I know the indexes (col and raw of each cell stored in a list). List is like - I would like to get the indexes of the cells within this closed line and store them in a separate list. I want to do this in python. Here is an image to be more clear: Raster boundary line
One way to approach this is to implement your own (naive) algorithm, which was my first idea. One the other hand, why reinvent the wheel: One can easily see that the problem can be interpreted as a black and white (raster/pixel) image. Then the outer and inner area form the background (black) while the border is a closed (white) loop. (Obviously the colors could also switched, but I will use white on black for now.) As it happens there are some fairly sophisticated image processing libraries for python, namely skimage, ndimage and mahotas. I'm no expert but I think skimage.draw.polygon, skimage.draw.polygon_perimiter are the easiest way to solve your problem. My experimentation yielded the following: import matplotlib.pyplot as plt import numpy as np from skimage.draw import polygon, polygon_perimeter from skimage.measure import label, regionprops # some test data # I used the format that your input data is in # These are 4+99*4 points describing the border of a 99*99 square border_points = ( [[100,100]] + [[100,100+i] for i in range(1,100)] + [[100,200]] + [[100+i,200] for i in range(1,100)] + [[200,200]] + [[200,200-i] for i in range(1,100)] + [[200,100]] + [[200-i,100] for i in range(1,100)] ) # convert to numpy arrays which hold the x/y coords for all points # repeat first point at the end to close polygon. border_points_x = np.array( [p[0] for p in border_points] + [border_points[0][0]] ) border_points_y = np.array( [p[1] for p in border_points] + [border_points[0][1]] ) # empty (=black) 300x300 black-and-white image image = np.zeros((300, 300)) # polygon() calculates the indices of a filled polygon # one would expect this to be inner+border but apparently it is inner+border/2 # probably some kind of "include only the left/top half" filled_rr, filled_cc = polygon(border_points_y, border_points_x) # set the image to white at these points image[filled_rr, filled_cc] = 1 # polygon_perimeter() calculates the indices of a polygon perimiter (i.e. border) border_rr, border_cc = polygon_perimeter(border_points_y, border_points_x) # exclude border, by setting it to black image[border_rr, border_cc] = 0 # label() detects connected patches of the same color and enumerates them # the resulting image has each of those regions filled with its index label_img, num_regions = label(image, background=0, return_num=True) # regionprops() takes a labeled image and computes some measures for each region regions = regionprops(label_img) inner_region = regions[0] print("area", inner_region.area) # expecting 9801 = 99*99 for inner # this is what you want, the coords of all inner points inner_region.coords # print it fig, ax = plt.subplots() ax.imshow(image, cmap=plt.cm.gray)