I have a noisy gray-scale image for which I want to segment/mask the large arc spanning the image from the rest. I intend to mask the arc and all of the pixels above the arc.
To do this, I have thresholded the image to create a binary image, and used cv2.findContours() to trace the outline of the arc.
Original Image:
Image after Otsu threshold:
Threshold + Closing:
Contours of closed image:
As you can see, the closed image does not create a solid arc. Closing further causes the arc to lose it's shape. The green line is a contour of the closed image. The blue line is created with approxpolyDP() but I can't get it to work. Are there better ways to mask the arc in the image perhaps?
Here is my code:
import cv2, matplotlib
import numpy as np
import matplotlib.pyplot as plt
# read an image
img = cv2.imread('oct.png')
# get gray image and apply Gaussian blur
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
# get binary image
ret, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# close image to "solidify" it
kernel = np.ones((3,3),np.uint8)
closing = cv2.morphologyEx(thresh,cv2.MORPH_CLOSE,kernel, iterations = 3)
# find contours
(_, contours, _) = cv2.findContours(closing, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
max_area = cv2.contourArea(cnt)
for cont in contours:
if cv2.contourArea(cont) > max_area:
cnt = cont
max_area = cv2.contourArea(cont)
# define main arc contour approx. and hull
perimeter = cv2.arcLength(cnt, True)
epsilon = 0.1 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
# hull = cv2.convexHull(cnt)
# cv2.isContourConvex(cnt)
imgcopy = np.copy(img)
cv2.drawContours(imgcopy, [cnt], -1, (0, 255, 0), 3)
cv2.drawContours(imgcopy, [approx], -1, (0, 0, 255), 3)
# plot figures
plt.figure(1)
plt.imshow(imgcopy, cmap="gray")
plt.figure(2)
plt.imshow(thresh, cmap="gray")
plt.figure(3)
plt.imshow(closing, cmap="gray")
I would suggest to use a RANSAC method to fit 2 ellipses using edge information of the arc. Edge can be obtain simply by using canny or any other method you see fit. Of course this method can only work if the arc is elliptical. If its a straight line, you can replace the ellipse fitting part with a line fitting part.
Here is the result:
Here is the code:
import numpy as np
import cv2
import random as rp
def ransac_ellipse(iter, srcimg, x, y):
x_size = np.size(x)
best_count = x_size
for i in range(iter):
base = srcimg.copy()
# get 5 random points
r1 = int(rp.random() * x_size)
r2 = int(rp.random() * x_size)
r3 = int(rp.random() * x_size)
r4 = int(rp.random() * x_size)
r5 = int(rp.random() * x_size)
p1 = (x[r1],y[r1])
p2 = (x[r2],y[r2])
p3 = (x[r3],y[r3])
p4 = (x[r4],y[r4])
p5 = (x[r5],y[r5])
p_set = np.array((p1,p2,p3,p4,p5))
# fit ellipse
ellipse = cv2.fitEllipse(p_set)
# remove intersected ellipse
cv2.ellipse(base,ellipse,(0),1)
# count remain
local_count = cv2.countNonZero(base)
# if count is smaller than best, update
if local_count < best_count:
best_count = local_count
best_ellipse = ellipse
return best_ellipse
img = cv2.imread('arc.jpg',0)
# Speed up and remove noise
small = cv2.resize(img,(0,0),fx = 0.25,fy = 0.25)
# remove remaining noise
median = cv2.medianBlur(small,21)
# get canny edge
edge = cv2.Canny(median,180,20)
cv2.imshow("Edge",edge)
# obtain the non zero locations
y, x = np.where(edge > 0)
# ransac ellipse to get the outter circle
ellipse1 = ransac_ellipse(10000,edge,x,y)
# remove the outter circle
cv2.ellipse(edge,ellipse1,(0),2)
# ransac ellipse to get the inner circle
y, x = np.where(edge > 0)
ellipse2 = ransac_ellipse(10000,edge,x,y)
disp = cv2.cvtColor(small,cv2.COLOR_GRAY2BGR)
cv2.ellipse(disp,ellipse1,(0,0,255),1)
cv2.ellipse(disp,ellipse2,(0,0,255),1)
cv2.imshow("result",disp)
cv2.waitKey(0)
You are on the right path. Your closing will likely work better if you smooth the image a little bit first. I like to apply the thresholding at the end, after the morphological operations. In this case, it doesn't really matter which the order for closing and thresholding is, but keeping thresholding at the end helps later when refining the pre-processing. Once you threshold you loose a lot of information, you need to make sure you preserve all the information you will need, and thus filtering the image properly before thresholding is important.
Here is a quick attempt, I'm sure it can be refined:
import matplotlib.pyplot as pp
import PyDIP as dip
img = pp.imread('/Users/cris/Downloads/MipBB.jpg')
img = img[:,:,0]
smooth = dip.Gauss(img, [3]) # Gaussian smoothing with sigma=3
smooth = dip.Closing(smooth, 25) # Note! This uses a disk SE with diameter 25 pixels
out = dip.Threshold(smooth, 'triangle')[0]
pp.imsave('/Users/cris/Downloads/MipBB_out.png', out)
I used the triangle threshold method (also known as the chord method, or skewed bi-modality threshold, see P.L. Rosin, "Unimodal thresholding", Pattern Recognition 34(11):2083-2096, 2001) because it works better in this case.
The code uses PyDIP, but I'm sure you can re-create the same process using OpenCV.
Related
I have an image and I've done some pre-processing on the that image. Below I showed my preprocessing:
img= cv2.imread("...my_drive...\\image_69.tif",0)
median=cv2.medianBlur(img,13)
ret, th = cv2.threshold(median, 0 , 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
kernel=np.ones((3,15),np.uint8)
closing1 = cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel, iterations=2)
kernel=np.ones((1,31),np.uint8)
closing2 = cv2.morphologyEx(closing1, cv2.MORPH_CLOSE, kernel)
kernel=np.ones((1,13),np.uint8)
opening1= cv2.morphologyEx(closing2, cv2.MORPH_OPEN, kernel, iterations=2)
So, basically I used "Threshold filtering" , "closing" and "opening" and the result looks like this:
Please note that when I used type(opening1), I got numpy.ndarray. So the image at this step is numpy array with 1021 x 1024 size.
Then I labeled my image:
label_image=measure.label(opening1, connectivity=opening1.ndim)
props= measure.regionprops_table (label_image, properties=['label', "area", "coords"])
and the result looks like this
Please note that when I used type(label_image), I got numpy.ndarray. So the image at this step is numpy array with 1021 x 1024 size.
As you can see, currently the image has 6 labels. Some of these labels are short and small pieces, so I tried to keep top 2 label based on area
slc=label_image
rps=regionprops(slc)
areas=[r.area for r in rps]
id=np.argsort(props["area"])[::-1]
new_slc=np.zeros_like(slc)
for i in id[:2]:
new_slc[tuple(rps[i].coords.T)]=i+1
Now the result looks like this:
It looks like I was successful in keeping 2 top regions (please note that by changing id[:2] you can select thickest white layer or thin layer). Now:
What I want to do: I want to find the average thickness of these two regions
Also, please note that I know each of my pixels is 314 nm
Can anyone here advise how I can do this task?
Original photo: Below I showed low quality of my original image, so you have better understanding as why I did all the pre-processing
you can also access the original photo here : https://www.mediafire.com/file/20h66aq83edy1h7/img.7z/file
Here is one way to do that in Python/OpenCV.
Read the input
Convert to gray
Threshold to binary
Get the contours and filter on area so that we have only the two primary lines
Sort by area
Select the first (smaller and thinner) contour
Draw it white filled on a black background
Get its skeleton
Get the points of the skeleton
Fit a line to the points and get the rotation angle of the skeleton
Loop over each of the two contours and draw them white filled on a black background. Then rotate to horizontal lines. Then get the vertical thickness of the lines from the average thickness along each column using np.count_nonzero() and print the value.
Save intermediate images
Input:
import cv2
import numpy as np
import skimage.morphology
import skimage.transform
import math
# read image
img = cv2.imread('lines.jpg')
# convert to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
# get contours
new_contours = []
img2 = np.zeros_like(thresh, dtype=np.uint8)
contour_img = thresh.copy()
contour_img = cv2.merge([contour_img,contour_img,contour_img])
contours = cv2.findContours(thresh , cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
for cntr in contours:
area = cv2.contourArea(cntr)
if area > 1000:
cv2.drawContours(contour_img, [cntr], 0, (0,0,255), 1)
cv2.drawContours(img2, [cntr], 0, (255), -1)
new_contours.append(cntr)
# sort contours by area
cnts_sort = sorted(new_contours, key=lambda x: cv2.contourArea(x), reverse=False)
# select first (smaller) sorted contour
first_contour = cnts_sort[0]
contour_first_img = np.zeros_like(thresh, dtype=np.uint8)
cv2.drawContours(contour_first_img, [first_contour], 0, (255), -1)
# thin smaller contour
thresh1 = (contour_first_img/255).astype(np.float64)
skeleton = skimage.morphology.skeletonize(thresh1)
skeleton = (255*skeleton).clip(0,255).astype(np.uint8)
# get skeleton points
pts = np.column_stack(np.where(skeleton.transpose()==255))
# fit line to pts
(vx,vy,x,y) = cv2.fitLine(pts, cv2.DIST_L2, 0, 0.01, 0.01)
#print(vx,vy,x,y)
x_axis = np.array([1, 0]) # unit vector in the same direction as the x axis
line_direction = np.array([vx, vy]) # unit vector in the same direction as your line
dot_product = np.dot(x_axis, line_direction)
[angle_line] = (180/math.pi)*np.arccos(dot_product)
print("angle:", angle_line)
# loop over each sorted contour
# draw contour filled on black background
# rotate
# get mean thickness from np.count_non-zeros
black = np.zeros_like(thresh, dtype=np.uint8)
i = 1
for cnt in cnts_sort:
cnt_img = black.copy()
cv2.drawContours(cnt_img, [cnt], 0, (255), -1)
cnt_img_rot = skimage.transform.rotate(cnt_img, angle_line, resize=False)
thickness = np.mean(np.count_nonzero(cnt_img_rot, axis=0))
print("line ",i,"=",thickness)
i = i + 1
# save resulting images
cv2.imwrite('lines_thresh.jpg',thresh)
cv2.imwrite('lines_filtered.jpg',img2)
cv2.imwrite('lines_small_contour_skeleton.jpg',skeleton )
# show thresh and result
cv2.imshow("thresh", thresh)
cv2.imshow("contours", contour_img)
cv2.imshow("lines_filtered", img2)
cv2.imshow("first_contour", contour_first_img)
cv2.imshow("skeleton", skeleton)
cv2.waitKey(0)
cv2.destroyAllWindows()
Threshold image:
Contour image:
Filtered contour image:
Skeleton image:
Angle (in degrees) and Thicknesses (in pixels):
angle: 3.1869032185349733
line 1 = 8.79219512195122
line 2 = 49.51609756097561
To get the thickness in nm, multiply thickness in pixels by your 314 nm/pixel.
ADDITION
If I start with your tiff image, the following shows my preprocessing, which is similar to yours.
import cv2
import numpy as np
import skimage.morphology
import skimage.transform
import math
# read image
img = cv2.imread('lines.tif')
# convert to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# threshold
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]
# apply morphology
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,5))
morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (29,1))
morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)
# get contours
new_contours = []
img2 = np.zeros_like(gray, dtype=np.uint8)
contour_img = gray.copy()
contour_img = cv2.merge([contour_img,contour_img,contour_img])
contours = cv2.findContours(morph , cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
for cntr in contours:
area = cv2.contourArea(cntr)
if area > 1000:
cv2.drawContours(contour_img, [cntr], 0, (0,0,255), 1)
cv2.drawContours(img2, [cntr], 0, (255), -1)
new_contours.append(cntr)
# sort contours by area
cnts_sort = sorted(new_contours, key=lambda x: cv2.contourArea(x), reverse=False)
# select first (smaller) sorted contour
first_contour = cnts_sort[0]
contour_first_img = np.zeros_like(morph, dtype=np.uint8)
cv2.drawContours(contour_first_img, [first_contour], 0, (255), -1)
# thin smaller contour
thresh1 = (contour_first_img/255).astype(np.float64)
skeleton = skimage.morphology.skeletonize(thresh1)
skeleton = (255*skeleton).clip(0,255).astype(np.uint8)
# get skeleton points
pts = np.column_stack(np.where(skeleton.transpose()==255))
# fit line to pts
(vx,vy,x,y) = cv2.fitLine(pts, cv2.DIST_L2, 0, 0.01, 0.01)
#print(vx,vy,x,y)
x_axis = np.array([1, 0]) # unit vector in the same direction as the x axis
line_direction = np.array([vx, vy]) # unit vector in the same direction as your line
dot_product = np.dot(x_axis, line_direction)
[angle_line] = (180/math.pi)*np.arccos(dot_product)
print("angle:", angle_line)
# loop over each sorted contour
# draw contour filled on black background
# rotate
# get mean thickness from np.count_non-zeros
black = np.zeros_like(thresh, dtype=np.uint8)
i = 1
for cnt in cnts_sort:
cnt_img = black.copy()
cv2.drawContours(cnt_img, [cnt], 0, (255), -1)
cnt_img_rot = skimage.transform.rotate(cnt_img, angle_line, resize=False)
thickness = np.mean(np.count_nonzero(cnt_img_rot, axis=0))
print("line ",i,"=",thickness)
i = i + 1
# save resulting images
cv2.imwrite('lines_thresh2.jpg',thresh)
cv2.imwrite('lines_morph2.jpg',morph)
cv2.imwrite('lines_filtered2.jpg',img2)
cv2.imwrite('lines_small_contour_skeleton2.jpg',skeleton )
# show thresh and result
cv2.imshow("thresh", thresh)
cv2.imshow("morph", morph)
cv2.imshow("contours", contour_img)
cv2.imshow("lines_filtered", img2)
cv2.imshow("first_contour", contour_first_img)
cv2.imshow("skeleton", skeleton)
cv2.waitKey(0)
cv2.destroyAllWindows()
Threshold image:
Morphology image:
Filtered Lines image:
Skeleton image:
Angle (degrees) and Thickness (pixels):
angle: 3.206927978669998
line 1 = 9.26171875
line 2 = 49.693359375
Use Deskew to straighten up the image.
Then, count the pixels of each column of the color of the label you want to measure then divide it by the number of columns to get the average thickness
This can be done with various tools in scipy. Assume you have the image here:
I = PIL.Image.open("input.jpg")
img = np.array(I).mean(axis=2)
mask = img==255 # or some kind of thresholding
imshow(mask) #note this is a binary image, the green coloring is due to some kind of rendering artifact or aliasing
If one zooms in they can see split up regions
To get around that we can dilate the mask
from scipy import ndimage as ni
mask1 = ni.binary_dilation(mask, iterations=2)
imshow(mask1)
Now, we can find connected regions, and find the top regions with the most pixels, which should be the two lines of interest:
lab, nlab = ni.label(mask1)
max_labs = np.argsort([ (lab==i).sum() for i in range(1, nlab+1)])[::-1]+1
imshow(lab==max_labs[0])
and imshow(lab==max_labs[1])
Working with the first line as an example:
from scipy.stats import linregress
y0,x0 = np.where(lab==max_labs[0])
l0 = linregress( x0, y0)
xi,yi = np.arange(img.shape[3]), np.arange(img.shape[3])*l0.slope + l0.intercept
plot( xi, yi, 'r--')
Interpolate along this region at different y-intercepts and compute the average signal along each line
from scipy.interpolate import RectBivariateSpline
img0 = img.copy()
img0[~(lab==max_labs[0])] = 0 # set everything outside this line region to 0
rbv = RectBivariateSpline(np.arange(img.shape[0]), np.arange(img.shape[1]), img0)
prof0 = [rbv.ev(yi+i, xi).mean() for i in np.arange(-300,300)] # pick a wide window here (-300,300), can be more technical, but not necessary
plot(prof0)
Use your favorite method to compute the FWHM of this profile, then multiply by your pixel-to-nanometers factor.
I would just use a Gaussian fit to compute fwhm
xvals = np.arange(len(prof0))
yvals = np.array(prof0)
def func(p, xvals, yvals):
mu,var, amp = p
model = np.exp(-(xvals-mu)**2/2/var)*amp
resid = (model-yvals)**2
return resid.sum()
from scipy.optimize import minimize
x0 = 300,200,255 # initial estimate of mu, variance, amplitude
fit_gauss = minimize(func, x0=x0, args=(xvals, yvals), method='Nelder-Mead')
mu, var, amp = fit_gauss.x
fwhm = 2.355 * np.sqrt(var)
# display using matplotlib plot /hlines
plot( xvals, yvals)
plot( xvals, amp*np.exp(-(xvals-mu)**2/2/var) )
hlines(amp*0.5, mu-fwhm/2., mu+fwhm/2, color='r')
legend(("profile","fit gauss","fwhm=%.2f pix" % fwhm))
Finally, thickness=fwhm*314, or about 13 microns.
Following the exact same approach for the second line (lab==max_labs[1]) gives a thickness of about 2.2 microns:
Note, I was using interactive plotting to do this example, hence calls to imshow , plot etc. are meant motly as a reference to the reader. One may need to take extra steps to recreate the exact images I've uploaded (zooming etc).
I have an image of an ellipse and if an image has ellipse i am finding it using findcontours() and then i want to convert this ellipse to a circle.
see the example
and i want transform each of them to like this
First I have applied canny edge detection.Then on this image findcontour() is applied.
I have found the ellipse using findcontours() to get all contours and get the required elliptical contour and then the i am using fitellipse() to get center, rotation angle and major and minor axis of ellipse.
I have then tried to rotate the image by the rotated angle and then scale height and width of image w.r.t minor and major axis(i.e. making major axis and minor axis length same) but also I am not getting the proper circular object image as above. There will be some rotation left/it will still be like an ellipse which will be near to circle or so.
_, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
if len(c) >= 5:
a = cv2.fitEllipse(c)
(x, y), (MA, ma), angle = a
area = (math.pi * MA * ma)/4
if abs(x-image.shape[0]/2) <= 2 and abs(y-image.shape[1]/2) <= 2 and (area - cv2.contourArea(c)) < 50:
screenCount = c
width, height = MA, ma
centerX, centerY = x, y
ellipseAngle = angle
print(width, height, centerX, centerY, ellipseAngle)
# cv2.drawContours(img, c, -1, (0, 255, 0), 4)
cv2.ellipse(img, a, (0, 0, 255), 2, 8)
cv2.imshow("ellipse", img)
break
img = image.copy()
if ellipseAngle < 90:
rotatedImg = imutils.rotate(img, ellipseAngle)
else:
rotatedImg = imutils.rotate(img, -(ellipseAngle - 90))
Then i have scaled as per major and minor axis
after applying findcontour() i got these 2 contours for 1st image in the post
from these any contour is fine right? i am using the first contour from countour as per the code and fitellipse() gives me this ellipse
EDITED - If there are any better approach to solve this problem it would be helpful.
There are several issues I can see in the code:
You are using an edge detection algorithm, and getting the contours of the result. This is OK in principle, but it leads to an image that has two contours: one for the inner edge and one for the outer edge of the edge detection result. It is easier to just threshold the image and obtain a single edge. Though if the image gets more complex an edge detection might be relevant. Indeed, any one of the two contours you obtain should be useful.
The line if abs(x-image.shape[0]/2) <= 2 and abs(y-image.shape[1]/2) <= 2 and (area - cv2.contourArea(c)) < 50 is very restrictive, it didn't trigger for the second image for me.
Rotating by -(ellipseAngle - 90) if the angle is negative is strange. You should rotate all ellipses in the same way.
The code below produces a circular output for both ellipse images shown in the question. I think that the ellipse parameters determined this way are not very precise, but it looks like they're good enough for this application.
import cv2
import numpy as np
img = cv2.imread('im1.png',0)
_, thresh = cv2.threshold(img, 128, 255, type=cv2.THRESH_BINARY_INV)
_, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
params = cv2.fitEllipse(contours[0])
angle = params[2]
scale = params[1]
scale = scale[0]/scale[1]
M = cv2.getRotationMatrix2D((img.shape[0]/2, img.shape[1]/2), angle, 1)
# Let's add the scaling too:
M[:,0:2] = np.array([[1,0],[0,scale]]) # M[:,0:2]
M[1,2] = M[1,2] * scale # This moves the ellipse so it doesn't end up outside the image (it's not correct to keep the ellipse in the middle of the image)
out = cv2.warpAffine(img, M, img.shape, borderValue=255)
cv2.imshow('out',out)
cv2.waitKey()
Using PyDIP (I'm an author) you can get a more precise measure in the idealized case of the OP by not thresholding, and using the grey-values around the edges of the ellipse to get a more precise fit. We compute the second order central moments of the image, and derive the ellipse parameters from those. It is important here that the background is exactly 0, and that the foreground (ellipse pixels) are uniform in intensity except at the edge, where the intermediate grey-values add information about the sub-pixel location of the edge.
import PyDIP as dip
import numpy as np
img = -dip.ImageRead('im1.png').TensorElement(0) # We use the inverted first channel
params = dip.Moments(img).secondOrder
M = np.array([[params[0],params[2]],[params[2],params[1]]])
d, V = np.linalg.eig(M)
d = np.sqrt(d)
scale = d[0]/d[1]
angle = np.arctan2(V[1,0],V[0,0])
img = dip.Rotation2D(img, -angle)
img = dip.Resampling(img, [scale, 1])
img.Show()
I would like to convert this radar graph into numeric values for each component (dotted lines), using openCV for python. How would I best do this? I've been thinking about detecting the center of the graph as well as the intersections of the dotted lines with the gray area. By doing this I could measure the distance between the center and each of these intersections to determine a value for each component. However this seems rather complex, and I have no idea how to start. Could someone help me out here?
EDIT: The purpose is to write a piece of software that can extract data from 250 of these graphs. (I have better quality scans)
I would do something like this (sorry it's pseudo-code, if you think the idea it's good enough I'll try to write some real code):
1.find the circle center(maybe using HoughCircle function)
2.inverse thresholding to highlight your dark grey area
3.Call opencv function approx poly to get the polygon representing this area
4.for each vertex, measure its distance from the center and convert it to your desidered scale
I think it should work.
hough circle tutorial
https://docs.opencv.org/2.4/doc/tutorials/imgproc/imgtrans/hough_circle/hough_circle.html
approx poly tutorial
https://docs.opencv.org/3.1.0/dd/d49/tutorial_py_contour_features.html
EDIT:
I've had some spare time so I wrote an initial piece of code to extract circle, radius and the poly, hope it helps you
img = cv.imread("c:\\temp\\test.jpg", cv.IMREAD_COLOR)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
#thresholds for isolate circle and poly
ret, thres_poly_temp = cv.threshold(gray, 90, 255, cv.THRESH_BINARY_INV)
ret, thres_circle = cv.threshold(gray, 110, 255, cv.THRESH_BINARY_INV)
#cleanup image for detect poly
thres_poly = thres_poly_temp.copy()
thres_poly = cv.GaussianBlur(thres_poly_temp,(3,3),0)
thres_poly = cv.medianBlur( thres_poly, 5)
#bitwise_and to keep just the poly, removing the circle
cv.bitwise_and(thres_poly_temp, thres_circle, thres_poly)
kernel = np.ones((3, 3),np.uint8)
thres_poly = cv.morphologyEx(thres_poly, cv.MORPH_CLOSE, kernel)
kernel = np.ones((3, 3),np.uint8)
thres_poly = cv.morphologyEx(thres_poly, cv.MORPH_OPEN, kernel)
#find circle
circle = cv.HoughCircles(thres_circle, 3, 2, 800, minRadius = 100, maxRadius=500, param1=80, param2=100)
radius_list = []
if circle is not None:
circle = np.round(circle[0, :]).astype("int")
for (x,y,r) in circle:
cv.circle(gray, (x,y), r, (255,255,0),3)
cv.circle(gray, (x,y), 3, (255,255,0),3)
radius_list.append((x+r,y))
a = 0
#find radius
while(a < 360):
rad = math.radians(a)
x2 = int((radius_list[0][0] - x) * math.cos(rad)) - int((radius_list[0][1] - y) * math.sin(rad)) + x;
y2 = int((radius_list[0][0] - x) * math.sin(rad)) + int((radius_list[0][1] - y) * math.cos(rad)) + y;
radius_list.append((x2,y2))
a = a + 18
cv.line(gray, (x,y), (x2,y2), (255,255,0),2)
#find poly contour
contours,hierarchy = cv.findContours(thres_poly, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
#extract contour with max area
maxArea = -1
maxAreaContour = contours[0]
for contour in contours:
area = abs(cv.contourArea(contour))
if area > maxArea:
maxArea = area
maxAreaContour = contour
#approx poly to get contours
epsilon = 0.1*cv.arcLength(maxAreaContour,True)
approx = cv.approxPolyDP(maxAreaContour, 5, True)
cv.drawContours(gray, [approx],-1,(0,255,0),2)
cv.imshow("1", gray)
#now just iterate all the radius with the contour to find the intersection
# it should be pretty straight forward
output sample
EDIT 2: still missing: align image, as I said you can use the horizontal and vertical dotted lines to do that (use hough lines to get them,then determine m and q and do the inverse roto-translation)
This is my first question here so I'm asking for understanding. I have to process hundreds of the satellites images.
I try to find contour of the area of the useful data located on the image - only the largest one.
Then I want to save the coordinates of the few points (x,y) corresponding to this contour. In simplest case, the area is a square and can be represented by 4 points, but for more complicated shapes the contour will be approximated by a little more points (preferably no more than ~ fifteen). However I am still not be able to find the areas on my images. Sometimes the area touches the edge of the image. Therefore, in this script I enlarge the pictures and add additional boundaries filled by the background color. Examples of pictures you will find here satellite1,satellite2,satellite3
As you see the images can have different background colors and in addition they contain countries borders and legend. I have tried to use Aidenhjj tips OpenCV - using cv2.approxPolyDP() correctly and prepared my script. I tried many approaches, filtering and tune parameters but still can't succeed with my data. I am asking you for help.
import numpy as np
import cv2
import matplotlib.pyplot as plt
image = cv2.imread('image1.jpg')
image = cv2.resize(image, None,fx=0.25, fy=0.25, interpolation = cv2.INTER_CUBIC)
ysize, xsize, channels = image.shape
print("Image size: {} x {}".format(xsize, ysize))
#calculate the histograms in r,g,b channels, measure background color
r, g, b = cv2.split(image)
image_data = image
histr = cv2.calcHist([r],[0],None,[256],[0,256])
for y in range(0,len(histr)):
elem = histr[y]
if elem == histr.max():
break
else:
y = none
R=y
histr = cv2.calcHist([g],[0],None,[256],[0,256])
for y in range(0,len(histr)):
elem = histr[y]
if elem == histr.max():
break
else:
y = none
G=y
histr = cv2.calcHist([b],[0],None,[256],[0,256])
for y in range(0,len(histr)):
elem = histr[y]
if elem == histr.max():
break
else:
y = none
B=y
color = (R, G, B)
#add borders around the image colorized as background. This will allow me to find closed contour around area with data.
bordersize=100
new_xsize = xsize + bordersize*2
new_ysize = ysize + bordersize*2
#image_border.show()
image_border=cv2.copyMakeBorder(image, top=bordersize, bottom=bordersize, left=bordersize, right=bordersize, borderType= cv2.BORDER_CONSTANT, value=[R,G,B] )
#ysizeb, xsizeb, channelsb = image_border.shape
# get a blank canvas for drawing contour on and convert image to grayscale
canvas = np.zeros(image_border.shape, np.uint8)
#imgc = cv2.medianBlur(img,21)
img2gray = cv2.cvtColor(image_border,cv2.COLOR_BGR2GRAY)
# filter out country borders
kernel = np.ones((5,5),np.float32)/25
img2gray = cv2.filter2D(img2gray,-1,kernel)
# threshold the image and extract contours
thresh = cv2.adaptiveThreshold(img2gray,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,11)
contours,hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
plt.subplot(111),plt.imshow(thresh,'gray')
plt.show()
# find the biggest area
cnt = contours[0]
max_area = cv2.contourArea(cnt)
for cont in contours:
if cv2.contourArea(cont) > max_area:
cnt = cont
max_area = cv2.contourArea(cont)
perimeter = cv2.arcLength(cnt,True)
epsilon = 0.01*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True)
hull = cv2.convexHull(cnt)
# cv2.isContourConvex(cnt)
cv2.drawContours(canvas, cnt, -1, (0, 255, 0), 3)
cv2.drawContours(canvas, approx, -1, (0, 0, 255), 3)
#cv2.drawContours(canvas, [hull], -1, (0, 0, 255), 3)
cv2.imshow("Contour", canvas)
k = cv2.waitKey(0)
if k == 27: # wait for ESC key to exit
cv2.destroyAllWindows()
I am trying to take the below image, trace the white shape, and export the resulting path to pdf. The problem I have is that findContours seeming only finds points along the edge of the shape. Is there a solution out there, similar to findContours, that detects curves in a shape and replaces its points with a spline wherever there is a curve? If I use scipy.interpolate it ignores straight lines and turns the entire contour into one big curved shape, which is no good either. I need something that does both things.
import numpy as np
import cv2
from scipy.interpolate import splprep, splev
from pyx import *
import matplotlib.pyplot as plt
#read in image file
original = cv2.imread('test.jpg')
#blur the image to smooth edges
im = cv2.medianBlur(original,5)
#threshold the image
imgray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(imgray,170,255,cv2.THRESH_BINARY)
#findContours
im2, contours, hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_\
APPROX_SIMPLE)
#drawContours
cv2.drawContours(original, [approx], -1, (0,255,0), 3)
cv2.imshow("Imageee", original)
cv2.waitKey(0)
Except using cv2.findContours with flag cv2.CHAIN_APPROX_SIMPLE to approx the contours, we can do it manually.
use cv2.findContours with flag cv2.CHAIN_APPROX_NONE to find contours.
use cv2.arcLength to calculate the contour length.
use cv2.approxPoolyDP to approx the contour manually with epsilon = eps * arclen.
Here is one of the results when eps=0.005:
More results:
#!/usr/bin/python3
# 2018.01.04 13:01:24 CST
# 2018.01.04 14:42:58 CST
import cv2
import numpy as np
import os
img = cv2.imread("test.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret,threshed = cv2.threshold(gray,170,255,cv2.THRESH_BINARY)
# find contours without approx
cnts = cv2.findContours(threshed,cv2.RETR_LIST,cv2.CHAIN_APPROX_NONE)[-2]
# get the max-area contour
cnt = sorted(cnts, key=cv2.contourArea)[-1]
# calc arclentgh
arclen = cv2.arcLength(cnt, True)
# do approx
eps = 0.0005
epsilon = arclen * eps
approx = cv2.approxPolyDP(cnt, epsilon, True)
# draw the result
canvas = img.copy()
for pt in approx:
cv2.circle(canvas, (pt[0][0], pt[0][1]), 7, (0,255,0), -1)
cv2.drawContours(canvas, [approx], -1, (0,0,255), 2, cv2.LINE_AA)
# save
cv2.imwrite("result.png", canvas)
I think your problem actually consists of two issues.
The first issue is to extract the contour, which you can achieve using teh findContour function:
import numpy as np
print cv2.__version__
rMaskgray = cv2.imread('test.jpg', 0)
(thresh, binRed) = cv2.threshold(rMaskgray, 200, 255, cv2.THRESH_BINARY)
_, Rcontours, hier_r = cv2.findContours(binRed,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)
r_areas = [cv2.contourArea(c) for c in Rcontours]
max_rarea = np.argmax(r_areas)
CntExternalMask = np.ones(binRed.shape[:2], dtype="uint8") * 255
contour= Rcontours[max_rarea]
cv2.drawContours(CntExternalMask,[contour],-1,0,1)
print "These are the contour points:"
print c
print
print "shape: ", c.shape
for p in contour:
print p[0][0]
cv2.circle(CntExternalMask, (p[0][0], p[0][1]), 5, (0,255,0), -1)
cv2.imwrite("contour.jpg", CntExternalMask)
cv2.imshow("Contour image", CntExternalMask)
cv2.waitKey(0)
If you execute the program, the contour points are printed as a list of point coordinates.
The contour approximation method you choose influences the interpolation which is actually used (and the number of points found), as described here. I have added small dots at the points found with the approximation method cv2.CHAIN_APPROX_SIMPLE. You see that the straight lines are already approximated.
I may not fully have understood your second step, though. You want to omit some of those points, replacing point lists partially by splines. There might be different way to do this, depending on your final intention. Do you just want to replace the straight lines? If you replace curved parts, what is the margin of error you are allowing?
# import the necessary packages
import numpy as np
import argparse
import glob
import cv2
#For saving pdf
def save_pdf(imagename):
import img2pdf
# opening from filename
with open("output.pdf","wb") as f:
f.write(img2pdf.convert(imagename))
#for fouind biggest contours
def bigercnt(contours):
max_area=0
cnt=[]
for ii in contours:
area=cv2.contourArea(ii)
if area>max_area:
cnt = ii
return cnt
#STARTING
print ("Reading img.jpg file")
# load the image, convert it to grayscale, and blur it slightly
image = cv2.imread('img.jpg')
image = cv2.resize(image, (0,0), fx=0.5, fy=0.5)
print ("Converting it gray scale")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print ("Bluring")
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
print ("Looking for edges" )
# apply Canny edge detection using a wide threshold, tight
# threshold, and automatically determined threshold
tight = cv2.Canny(blurred, 255, 250)
print ("Looking for contours")
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (10, 10))
close = cv2.morphologyEx(tight, cv2.MORPH_CLOSE, kernel)
_,contours, hierarchy = cv2.findContours( close.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print("Looking for big contour")
cnt = bigercnt(contours)
print ("Cropping found contour")
x,y,w,h = cv2.boundingRect(cnt)
croped_image = image[y:y+h,x:x+w]
img2 = np.zeros((h,w,4),np.uint8)
print ("Taking only pixels in countour and creating png")
for i in range(h):
for j in range(w):
#print (x+j, y+i)
#print cv2.pointPolygonTest(cnt, (x+j, y+i), False)
if cv2.pointPolygonTest(cnt, (x+j, y+i), False)==1:
#print True
img2[i,j] = [croped_image[i, j][0],croped_image[i, j][1],croped_image[i, j][2],255]
else:
img2[i,j] = [255,255,255,0]
print ("Showing output image")
# Show the output image
#cv2.imshow('croped', croped_image)
cv2.imshow('output', img2)
params = list()
params.append(cv2.IMWRITE_PNG_COMPRESSION)
params.append(8)
print ("Saving output image")
cv2.imwrite("output.png",img2,params)
print ("Finish:converted")
cv2.waitKey(0)
cv2.destroyAllWindows()