Python OpenCV sorting contours in clockwise - python

I'm putting together an image processing tool to follow the deformation of a part using images. The part has rectangular markers that get detected with image segmentation and cv2.findContours function. Contour centers are then used to calculate distances and to bend radiuses. Everything seems to work fine, but I found out that the contours aren't sorted how I would like to sort them when reviewing results.
The part is repeatedly bent, and the contours are positioned in a circle.
I found this article that describes the sorting horizontally and vertically:
https://www.pyimagesearch.com/2015/04/20/sorting-contours-using-python-and-opencv/
Does anyone have any idea how to sort the contours in a clockwise direction?
The code is below.
import os
import exifread
import cv2
import numpy as np
import scipy
from matplotlib import pyplot as plt
import imutils
import pandas as pd
#---------- INPUT ----------
# Define the image filename
img_filename = 'frame397.jpg'
img_path = img_filename
# Define values for cropping
x = 0
y = 200
w = 1200
h = 800
# Define color values for segmentation
# the values can be probed with GIMP
h1 = 0
s1 = 70
v1 = 120
h2 = 255
s2 = 255
v2 = 255
red_lower = np.array([h1,s1,v1])
red_upper = np.array([h2,s2,v2])
# Define desired area size
# desired area size is pixel count - use GIMP for probe
s1 = 500
s2 = 10000
#---------- PROCESS IMAGES ----------
# Create an empty dataframe for storing results
# in shape of (image_name,time,angle,angle_smooth,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11)
# Define the results dataframe shape and column names
results_df = pd.DataFrame(columns=['image_name','alpha','r1','r2','r3','r4','r5','r6','r7','r8','r9','r10','r11',
'center_dist1', 'center_dist2','center_dist3','center_dist4',
'center_dist5','center_dist6','center_dist7','center_dist8',
'center_dist9','center_dist10','center_dist11'])
# Open image, make it black and white and find contours
img = cv2.imread(img_path)
crop = img[y:y+h, x:x+w]
blur = cv2.blur(crop,(2,2))
hsv = cv2.cvtColor(blur,cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, red_lower, red_upper)
mask_copy = mask.copy()
cnts = cv2.findContours(mask_copy,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
#print cnts
x = []
y = []
# Loop through contours, calculate the centers and prepare the
#contours and contour centers display
#define the font for the text on the image
font = cv2.FONT_HERSHEY_SIMPLEX
for cnt in cnts:
area = cv2.contourArea(cnt)
moment = cv2.moments(cnt)
if s1<area<s2:
print area
c_x = int(moment["m10"]/moment["m00"])
c_y = int(moment["m01"]/moment["m00"])
#draw contours
cv2.drawContours(crop, cnt, -1, (0,255,0),3)
#draw a circle in the center of every contour, -1 is for thickness, this means
#that the cirlce will get filled in
cv2.circle(crop, (c_x,c_y), 10, (0,255,0),-1)
#display center coordinates on the image
string = str(c_x) + ',' + str(c_y)
cv2.putText(crop,string,(c_x,c_y),font,0.5,(255,255,255),2)
x.append(float(c_x))
y.append(float(c_y))
print (c_x, c_y)
print x
print y
# Display image
cv2.namedWindow('Contours', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Contours', 1200,900)
cv2.imshow('Contours', crop)
# Wait for windows closing
cv2.waitKey() & 0xFF
cv2.destroyAllWindows
Image is here:

I used openCV's minEnclosingCircle to "fit" a circle to the points (it's not actually a fit, but it's good enough for finding a point inside the curvature of the markers). Marking each contour with the angle from its centroid to the circle's center gave me a set of angles that I could sort with.
import cv2
import numpy as np
import math
# 2d distance
def dist2D(one, two):
dx = one[0] - two[0];
dy = one[1] - two[1];
return math.sqrt(dx*dx + dy*dy);
# angle between three points (the last point is the middle)
def angle3P(p1, p2, p3):
# get distances
a = dist2D(p3, p1);
b = dist2D(p3, p2);
c = dist2D(p1, p2);
# calculate angle // assume a and b are nonzero
# (law of cosines)
numer = c**2 - a**2 - b**2;
denom = -2 * a * b;
if denom == 0:
denom = 0.000001;
rads = math.acos(numer / denom);
degs = math.degrees(rads);
# check if past 180 degrees
if p1[1] > p3[1]:
degs = 360 - degs;
return degs;
# load image
img = cv2.imread("slinky.jpg");
# rescale
scale = 0.5;
h, w = img.shape[:2];
h = int(h * scale);
w = int(w * scale);
img = cv2.resize(img, (w,h));
# change color space
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB);
l,a,b = cv2.split(lab);
# threshold
thresh = cv2.inRange(a, 140, 255);
# get rid of little dots
kernel = np.ones((3,3),np.uint8)
thresh = cv2.erode(thresh,kernel,iterations = 1);
thresh = cv2.dilate(thresh,kernel, iterations = 1);
# contours
_, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE);
# get centroids
centroids = [];
centers = [];
for con in contours:
m = cv2.moments(con);
cx = int(m['m10'] / m['m00']);
cy = int(m['m01'] / m['m00']);
centers.append([cx, cy]);
centroids.append([[cx, cy], con]);
img = cv2.circle(img, (cx, cy), 10, (0,0,255), -1);
# find circle around points
# NOTE: this doesn't "fit" a circle to the points
# I'm just using this to find a "good enough" center
# that's in the direction of the curve
numped = np.array(centers);
(x, y), radius = cv2.minEnclosingCircle(numped);
img = cv2.circle(img, (int(x), int(y)), int(radius), (255,0,0), 2);
middle = [x,y];
offshoot = [x + 100, y];
# get angles
angles = [];
for cen in centroids:
center, contour = cen;
angle = angle3P(center, offshoot, middle);
angles.append([angle, center, contour]);
# sort by angle
final = sorted(angles, key = lambda a: a[0], reverse = True);
# pull out just the contours
contours = [clump[2] for clump in final];
# draw contours in order
marked = img.copy();
counter = 0;
for con in contours:
cv2.drawContours(marked, [con], -1, (0, 255, 0), 2);
cv2.imshow("marked", marked);
cv2.imwrite("marking_seq/" + str(counter) + ".png", marked);
counter += 1;
cv2.waitKey(0);
# show
cv2.imshow("orig", img);
cv2.imshow("a", a);
cv2.imshow("thresh", thresh);
cv2.waitKey(0);

Related

How to implement a precise detector for the matrix of color blocks in Python?

Let me illustrate the problem with an example first.
Below is an example input image which might be noised and in other kinds of affine transformation.
The output should return a 2-D array/matrix (Use a standard color character to represent each color block) like
[[w, g, w, b],
[y, o, o, y],
[g, b, g, y],
[b, o, y, b]]
So the aim of this problem is to detect the matrix of color blocks for each input image file, and the orientation doesn't matter.
The idea to solve this problem gives me the intuition that the solution is like detecting and parsing a QR code, but I don't know how to work it out specifically.
Could anyone give me some suggestions like
The idea/procedure to solve this problem.
Which APIs of which Python packages should I explore and use.
Any similar classic problem that I should delve into.
Some proper code.
...
We can start by finding the black dots and "rectifying" the image
The corners can be found by thresholding and looking for "circular" blobs. The metric can be found here: https://learnopencv.com/blob-detection-using-opencv-python-c/
Once we have the corners we "rectify" the image (the image may be rotated and/or flipped depending on the ordering of the corners)
Convert to HSV
Mask again on the "v" channel to get the individual squares
Then we calculate the average hue and saturation of each square to smooth over noise
(we need the saturation to distinguish white squares)
We can match these numbers up against our pre-defined color values to print out a string:
['b', 'w', 'g', 'w']
['y', 'o', 'o', 'y']
['y', 'g', 'b', 'g']
['b', 'y', 'o', 'b']
Full Code
import cv2
import numpy as np
import math
# get average color of image within mask
# I think this can be done with some mix of numpy commands (it'd be much faster)
def maskedAverageColor(mask, img):
# convert to hsv
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV);
h,s,v = cv2.split(hsv);
height,width = img.shape[:2];
count = 0;
hue_ave = 0;
sat_ave = 0;
# calculate average
for y in range(height):
for x in range(width):
if mask[y,x] == 255:
count += 1;
hue_ave += h[y,x];
sat_ave += s[y,x];
hue_ave /= count;
sat_ave /= count;
return [hue_ave, sat_ave];
# center of contour
def getCenter(contour):
M = cv2.moments(contour);
cx = int(M['m10']/M['m00']);
cy = int(M['m01']/M['m00']);
return [cx, cy];
# predefined colors
# white
# green
# blue
# orange
# yellow
# ??? missing one color (need 6 for a cube?)
color_map = {};
color_map['g'] = 53;
color_map['y'] = 27;
color_map['o'] = 10;
color_map['b'] = 120;
# load image
img = cv2.imread("cube.png");
# show
cv2.imshow("Image", img);
# grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY);
# threshold
mask = cv2.inRange(gray, 0, 50);
# close small black speckles and dilate
kernel = np.ones((3,3),np.uint8);
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel);
mask = cv2.dilate(mask, kernel, iterations = 1);
# find contours
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE);
# fill in black spaces
for con in contours:
cv2.drawContours(mask, [con], -1, 255, -1);
cv2.imshow("Mask", mask);
# get the contours again
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE);
# filter out little contours
filtered = [];
for con in contours:
area = cv2.contourArea(con);
if area > 25:
filtered.append(con);
contours = filtered;
# circularity
corners = [];
for con in contours:
area = cv2.contourArea(con);
perm = cv2.arcLength(con, True);
if area != 0 and perm != 0:
circ = (4 * math.pi * area) / (perm**2);
if circ > 0.5:
# get the corners
corners.append(getCenter(con));
# sort corners
# find point in the middle of the corners
cx = 0;
cy = 0;
for corner in corners:
x,y = corner;
cx += x;
cy += y;
cx /= 4;
cy /= 4;
# calculate angles
angles = []; # [angle, point]
for corner in corners:
x, y = corner;
dx = 1000;
dy = y - cy;
angle = math.atan2(dy, dx);
angles.append([angle, corner]);
# sort by angles (counter-clockwise)
angles = sorted(angles, key = lambda x : x[0]);
corners = [p[1] for p in angles];
cv2.destroyAllWindows();
# unmorph with corners
to_rect = [
[0, 0],
[500, 0],
[500, 500],
[0, 500]
]
corners = np.array(corners, dtype=np.float32);
to_rect = np.array(to_rect, dtype=np.float32);
warp_mat = cv2.getPerspectiveTransform(corners, to_rect);
rectified = cv2.warpPerspective(img, warp_mat, (500,500));
cv2.imshow("Rect", rectified);
# hsv and mask again
hsv = cv2.cvtColor(rectified, cv2.COLOR_BGR2HSV);
h,s,v = cv2.split(hsv);
cv2.imshow("Hue", h);
cv2.imshow("Sat", s);
cv2.imshow("Val", v);
mask = cv2.inRange(v, 0, 150);
# dilate mask
mask = cv2.dilate(mask, kernel, iterations = 5);
mask = cv2.bitwise_not(mask);
# get contours (yes again)
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE);
# get the squares
squares = [];
for con in contours:
area = cv2.contourArea(con);
if area < 100000:
squares.append(con);
# get the center of squares
drawn = np.copy(rectified);
for square in squares:
cx, cy = getCenter(square);
cv2.circle(drawn, (int(cx), int(cy)), 4, (255,0,0), -1);
# split into rows
rows = [];
index = -1;
for y in range(4):
row = [];
for x in range(4):
row.append(squares[index]);
index -= 1;
rows.append(row);
# draw rows
drawn = np.copy(rectified);
for row in rows:
for square in row:
cx, cy = getCenter(square);
cv2.circle(drawn, (int(cx), int(cy)), 4, (255,0,0), -1);
# get color of each square
color_rows = [];
for row in rows:
color_row = [];
for square in row:
# get mask
h,w = rectified.shape[:2];
mask = np.zeros((h,w), np.uint8);
mask = cv2.drawContours(mask, [square], -1, 255, -1);
# calculate average colors
hue, sat = maskedAverageColor(mask, rectified);
# convert to string colors
# white check
color_str = "NONE";
if sat < 50:
color_str = "w";
else:
margin = 5;
for key in color_map:
if color_map[key] - margin < hue and hue < color_map[key] + margin:
color_str = key;
if color_str == "NONE":
print("INCREASE MARGIN");
print("HUE SAT: " + str([hue, sat]));
color_row.append(color_str);
color_rows.append(color_row);
# print out results
for row in color_rows:
print(row);
cv2.waitKey(0);

Fit theoretical dartboard in image containing dartboard

I have the following image containing a dartboard
After processing the image looks as follows:
In addition, I have a function that creates a theoretical dartboard:
import cv2
import numpy as np
def draw_dartboard():
IMG = np.ones((400, 400), 'uint8') * 255
center = (int(IMG.shape[0] // 2), int(IMG.shape[1] // 2))
size_dartboard = int(340)
r_board = int(170)
r_db = int(6.35)
r_sb = int(15.9)
r_doubles = int(162)
r_triples = int(99)
width_rings = int(8)
cv2.circle(IMG, center, r_doubles + width_rings, (0,0,0), -1)
cv2.circle(IMG, center, r_doubles, (255,255,255), -1)
cv2.circle(IMG, center, r_triples + width_rings, (0,0,0), -1)
cv2.circle(IMG, center, r_triples, (255,255,255), -1)
thetas_min = np.radians([(18 * t - 9) for t in range(20)])
thetas_max = np.radians([(18 * t + 9) for t in range(20)])
for idx, (theta_min, theta_max) in enumerate(zip(thetas_min, thetas_max)):
if (idx % 2) == 0:
x_min = int(center[0] + r_board * np.cos(theta_min))
y_min = int(center[1] + r_board * np.sin(theta_min))
x_max = int(center[0] + r_board * np.cos(theta_max))
y_max = int(center[1] + r_board * np.sin(theta_max))
cv2.fillPoly(IMG, np.array([(center, (x_min,y_min), (x_max,y_max))]), (0,0,0))
cv2.circle(IMG, center, r_sb, (0,0,0), -1)
return IMG
The output of this image looks as follows:
How can I “fit” the theoretical dartboard in the real image? Clearly, there is a mismatch in orientation and scale. What's the best way to do this?
You can register your dartboard image (i.e. source image) to the one you processed (i.e. destination image) by using affine transformations.
Here is my approach, and the outcome.
import cv2
import matplotlib.pyplot as plt
import numpy as np
# read images and remove matplotlib axes
src = cv2.imread('source.png',0)
src = src[20:-30,40:-20]
dest = cv2.imread('dest.png',0)
dest = dest[40:-40,40:-40]
# find matching points manually
dest_pts = np.array([[103,29],[215,13],[236,125]]).astype(np.float32) # x,y
src_pts = np.array([[19,175],[145,158],[176,284]]).astype(np.float32) #x,y
# calculate the affine transformation matrix
warp_mat = cv2.getAffineTransform(src_pts, dest_pts)
# get the registered source image
warp_dst = cv2.warpAffine(src, warp_mat, (dest.shape[1], dest.shape[0]))
fig,ax = plt.subplots(1,3)
ax[0].imshow(src,'gray')
ax[0].scatter(src_pts[:,0],src_pts[:,1],s=1,c='r')
ax[0].set_title('src')
ax[1].imshow(dest,'gray')
ax[1].scatter(dest_pts[:,0],dest_pts[:,1],s=1,c='r')
ax[1].set_title('dest')
ax[2].imshow(warp_dst,'gray')
ax[2].set_title('registered src')
plt.savefig('result.png')
fig, ax = plt.subplots(1)
ax.imshow(dest,'gray')
ax.imshow(warp_dst,cmap='jet',alpha=0.5)
plt.savefig('overlayed_result.png')
# plt.show()
In order to calculate affine transformation matrix, you will need 3 matching points on both images. I highlighted the points I chose on both images. FYI, you can develop a way to automate finding matching points, let us know in your question if you need that.
As you have already done the image processing, I will take it from there. So just to be clear, this is the image I will be working with (I cropped out the matplotlib axises, as I'm sure they aren't present in your actual image):
The concept is really simple:
Find the bounding box of the contour of the target.
With the bounding box, we can find the radius of the target by selecting the greatest among the dimensions (width and height) of the bounding box, and dividing it by 2.
With the radius of the target and the top-left corner coordinates of the target (returned when finding the bounding box of the target), we can find the center of the target with the expressions x + r and y + h - r.
With the radius of the target, you can scale your theoretical target accordingly, and with the center of the target, you can draw your theoretical target at the right coordinates.
Here is how the code goes, where Image.png is the above image. Note that I only draw one circle onto the image; the rest of them can be drawn on using the same way, with just some added scaling:
import cv2
import numpy as np
img = cv2.imread("Image.png")
img_processed = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, _ = cv2.findContours(img_processed, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
cnt = sorted(contours, key=cv2.contourArea)[-2]
x, y, w, h = cv2.boundingRect(cnt)
r = max(w, h) // 2
center_x = x + r
center_y = y + h - r
cv2.circle(img, (center_x, center_y), r, (0, 255, 0), 5)
cv2.imshow("Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Output:
Note that at this line:
cnt = sorted(contours, key=cv2.contourArea)[-2]
I am getting the contour with the second-greatest area, as the one with the greatest area would be the border of the image.

Approximating a quadrilateral from a given mask

Goal:
I'd like to estimate a 4 coordinates quadrilateral (not only rectangles) of a given masked object as shown in the image + without losing any pixel of the masked object.
Trials:
I tried using CV2 however couldn't end up with a solution.
cv2.boundingRect: returns the coordinates of the bounding rectangle (while the quadrilateral estimation is not always necessary to be a perfect rectangle)
cv2.findContours + cv2.approxPolyDP: isn't that accurate and returns an estimate extreme points of the object (Needs more work to estimate the quadrilateral 4 coordinates and there might be an easier and faster solution).
Code Snippets:
Trying cv2.boundinRect:
#mask = grayed image with only a specific object being masked
#image = the original rgb image
x,y,x_width,y_height = cv2.boundingRect(mask)
image=np.array(im[0])
cv2.rectangle(image,(x,y),(x+x_width,y+y_height),(0,255,0),2)
plt.imshow(image)
Trying cv2.findContours + cv2.approxPolyDP:
#mask = grayed image with only a specific object being masked
#image = the original rgb image
contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
selected_contour = max(contours, key=lambda x: cv2.contourArea(x))
approx = cv2.approxPolyDP(selected_contour, 0.0035 * cv2.arcLength(selected_contour, True), True)
cv2.drawContours(image, [approx], 0, (0, 0, 255), 5)
plt.imshow(image)
I am not sure if there is a better or built-in version; but i have a simple idea based on random numbers:
I only did this for the top, but you can do the same for other sides. The idea is to find the bounding-box of object first; and then divide the object into equal parts so that we can find the highest peaks.
In each range, You can find points randomly; But for best results, it is best to check all the top points of the shape to find the highest peaks correctly.
After finding the highest peaks, we have to calculate a line equation with respect to those 2 points so that we can draw a global line with respect to that line equation.
import sys
import cv2
import random
import numpy as np
from tqdm import tqdm
def rndPt(l, t, r, b):
# Generate a random point in given ROI
return (random.randint(int(l), int(r)), random.randint(int(t), int(b)))
def intArr(arr):
# Cast each item of 1D array to integer
return [int(x) for x in arr]
# Load our image
pth = sys.path[0]
org = cv2.imread(pth+'/bound.png')
im = org.copy()
H, W = im.shape[:2]
# Make mask and copy from that image
im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
bw = cv2.threshold(im, 127, 255, cv2.THRESH_BINARY)[1]
im = bw.copy()
# Find the ROI of object
cnts, _ = cv2.findContours(bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cnts.sort(key=lambda x: cv2.boundingRect(x)[0])
ROI = None
for cnt in cnts:
x, y, w, h = cv2.boundingRect(cnt)
if w < W-1 and h < H-1:
cv2.rectangle(bw, (x, y), (x+w, y+h), 127, 2)
ROI = {'x': x, 'y': y, 'w': w, 'h': h, 'h2': y+h}
# We have to find the peaks; so we have to
# divide the bounding-box of shape into several
# ranges.
spaces = 5
sw = ROI['w']//spaces
# Each range can have a peak as a candidate point
candidates = [(ROI['x']+(sw*x)+sw//2, ROI['h']//2) for x in range(0, spaces)]
# Divide the object and find the highest point in
# each range
for s in tqdm(range(0, spaces)):
l = ROI['x']+(sw*s)
cv2.line(im, pt1=(l, ROI['y']), pt2=(l, ROI['h2']),
color=127, thickness=2)
for x in range(0, sw):
for i in range(0, 200):
pt = rndPt(l, ROI['y'], l+sw, ROI['h2']//4)
if pt[1] < candidates[s][1] and bw[pt[1], pt[0]] == 0:
candidates[s] = pt
l = ROI['x']+(sw*spaces)
cv2.line(im, pt1=(l, ROI['y']), pt2=(l, ROI['h2']), color=127, thickness=2)
print(candidates)
# We remove duplicate points and also sort the points
# according to the peak
candidates = list(set(candidates))
candidates.sort(key=lambda p: p[1])
print(candidates)
c = candidates
# Now that we have found two of the highest points, we can
# write a line equation for these two points
xA, xB = ROI['x'], ROI['x']+ROI['w']
x1, y1 = c[0][0], c[0][1]
x2, y2 = c[1][0], c[1][1]
m = (y2-y1)/(x2-x1)
# y=mx+b -> y-mx=b
b = y1-m*x1
yA = m*xA+b
yB = m*xB+b
# Convert images to BGR
im = cv2.cvtColor(im, cv2.COLOR_GRAY2BGR)
bw = cv2.cvtColor(bw, cv2.COLOR_GRAY2BGR)
# Make a copy of image to draw candidate points
marker = im.copy()
for p in candidates:
cv2.circle(marker, (p[0],p[1]),
h//25, color=(50, 100, 200),thickness=4)
# Draw lines
cv2.line(im, pt1=intArr((xA, yA)), pt2=intArr((xB, yB)),
color=(255, 0, 100), thickness=4, lineType=cv2.LINE_AA)
cv2.line(bw, pt1=intArr(c[0]), pt2=intArr(c[1]),
color=(100, 0, 255), thickness=4, lineType=cv2.LINE_AA)
# Save final output
top = np.hstack((org, marker))
btm = np.hstack((bw, im))
cv2.imwrite(pth+'/out.png', np.vstack((top, btm)))

Algorithmic problem, how to find the number of pixels bordered by an angle code in python and opencv

A newbie is here and I'm working on python/opencv about spatial relations problem, I need to find from a point P the number of white pixels in the range (0,alpha), do you have an idea how to do it or is there any tool.
Here two images to describe well my question
The original binary image I've
From a given point p, I need to compute how much white pixels are inside the shape created by the angle
If there is any algorithmic solution I can implement or any opencv function tell me please.
Lol, #Fiver got to it before I could finish coding up the example. Well, here's what I did to get that.
I created a set of three points (the original point, a point far off to the right, and a rotate point that matched the given angle).
I used those three points to draw two lines. I used copyMakeBorder to create a square edge outside of the image to create a closed shape with the lines I drew. I then used findContours and I know that I'm going to get three contours (except when angle == 0, but let's ignore that).
The first contour will be the whole rectangle because I chose the tree hierarchy (it's the parent). Of the two children contours I separate them into big and small and choose which one I want make a mask with based on the given angle.
Marked Image
Just the mask
Note: This method will fail when angle == 0 or abs(angle) == 180. When angle == 0 there are only two contours so the code will crash. When abs(angle) == 180 it'll arbitrarily choose one of the contours.
import cv2
import math
import numpy as np
# turns a list into an int tuple
def tup(point):
return (int(point[0]), int(point[1]));
# translate a point
def translate2D(point, target, sign):
point[0] += target[0] * sign;
point[1] += target[1] * sign;
# rotate a point
def rotate2D(point, deg):
# unpack
x, y = point;
rads = math.radians(deg);
# trig
rcos = math.cos(rads);
rsin = math.sin(rads);
# rotate
point[0] = x * rcos - y * rsin;
point[1] = x * rsin + y * rcos;
# load image
img = cv2.imread("blub.png");
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY);
# point and angle
point = [100, 150];
angle = -60; # negative is counter-clockwise
# create projection
xproj = [point[0] + img.shape[1], point[1]];
rot = xproj[:];
translate2D(rot, point, -1);
rotate2D(rot, angle);
translate2D(rot, point, 1);
# create copy and draw lines
mask = np.zeros(img.shape[:2], np.uint8);
mask = cv2.line(mask, tup(point), tup(xproj), (255), 1);
mask = cv2.line(mask, tup(point), tup(rot), (255), 1);
border = 1;
mask = cv2.copyMakeBorder(mask, border, border, border, border, cv2.BORDER_CONSTANT, None, (255));
# contours (should always be 3 unless 0 degrees)
_, contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE);
print(len(contours));
# compare two children contours
one, two = contours[1:];
if cv2.contourArea(one) > cv2.contourArea(two):
big = one;
small = two;
else:
big = two;
small = one;
# check which one we should draw
if abs(angle) >= 180:
con = small;
else:
con = big;
cv2.drawContours(gray, [con], -1, (0), -1);
mask = cv2.inRange(gray, 100, 255);
# draw visual
img[mask == 255] = (0,0,200);
img = cv2.line(img, tup(point), tup(xproj), (0,255,0), 1);
img = cv2.line(img, tup(point), tup(rot), (0,255,0), 1);
# count
print("White Pixels: " + str(np.count_nonzero(mask)));
# show
cv2.imshow("mask", mask);
cv2.imshow("blub", img);
cv2.waitKey(0);
Edit:
If you want to generalize this to the case of two points + an angle (not just projecting along the x-axis) we can use the same algorithm, but just replace the x-projection point with a projected point along the <p2 - p1> vector.
import cv2
import math
import numpy as np
# extends a p2 along its line
def extend(p1, p2, proj_length):
# get unit vector
dx = p2[0] - p1[0];
dy = p2[1] - p1[1];
dist = math.sqrt(dx*dx + dy*dy);
dx /= dist;
dy /= dist;
# project
dx *= proj_length;
dy *= proj_length;
return [dx, dy];
# turns a list into an int tuple
def tup(point):
return (int(point[0]), int(point[1]));
# translate a point
def translate2D(point, target, sign):
point[0] += target[0] * sign;
point[1] += target[1] * sign;
# rotate a point
def rotate2D(point, deg):
# unpack
x, y = point;
rads = math.radians(deg);
# trig
rcos = math.cos(rads);
rsin = math.sin(rads);
# rotate
point[0] = x * rcos - y * rsin;
point[1] = x * rsin + y * rcos;
# load image
img = cv2.imread("blub.png");
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY);
# get image resolution
h, w = img.shape[:2];
# point and angle
point = [100, 150];
angle = -60; # negative is counter-clockwise
# second point
p2 = [150, 90];
# create projection
# xproj = [point[0] + img.shape[1], point[1]];
projection = extend(point, p2, h * w);
rot = projection[:];
translate2D(rot, point, -1);
rotate2D(rot, angle);
translate2D(rot, point, 1);
# create copy and draw lines
mask = np.zeros(img.shape[:2], np.uint8);
mask = cv2.line(mask, tup(point), tup(projection), (255), 1);
mask = cv2.line(mask, tup(point), tup(rot), (255), 1);
border = 1;
mask = cv2.copyMakeBorder(mask, border, border, border, border, cv2.BORDER_CONSTANT, None, (255));
# contours (should always be 3 unless 0 degrees)
_, contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE);
print(len(contours));
# compare two children contours
one, two = contours[1:];
if cv2.contourArea(one) > cv2.contourArea(two):
big = one;
small = two;
else:
big = two;
small = one;
# check which one we should draw
if abs(angle) >= 180:
con = small;
else:
con = big;
cv2.drawContours(gray, [con], -1, (0), -1);
mask = cv2.inRange(gray, 100, 255);
# draw visual
img[mask == 255] = (0,0,200);
cv2.line(img, tup(point), tup(projection), (0,255,0), 1);
cv2.line(img, tup(point), tup(rot), (0,255,0), 1);
cv2.circle(img, tup(point), 3, (255,255,0), -1);
cv2.circle(img, tup(p2), 3, (255,255,0), -1);
# count
print("White Pixels: " + str(np.count_nonzero(mask)));
# show
cv2.imshow("mask", mask);
cv2.imshow("blub", img);
cv2.waitKey(0);

OpenCV Find a middle line of a contour [Python]

In my image processing project, I have already obtained a masked image (black-and-white image) and its contours using the cv.findContours function. My goal now is to create an algorithm that can draw a middle line for this contour. The masked image and its contour are shown in the following images.
Masked image:
Contour:
In my imagination, for that contour, I would like to create a middle line which is near horizontal. I have manually marked my ideal middle line in red. Please check the following image for the red middle line that I have mentioned.
Contour with the middle line:
It is noticeable that my ultimate goal is to find the tip point that I have marked in yellow. If you have other ideas that can directly find the yellow tip point, please also let me know. For finding the yellow tip point, I have tried two approaches cv.convexHull and cv.minAreaRect, but the issue is the robustness. I made these two approaches worked for some images but for some other images in my dataset, they are not working very well. Therefore, I think to find the middle line might be a good approach that I can try.
I believe you're trying to determine the contour's center of gravity and orientation. We can easily do this using Central Moments. More info on that here.
The code below generates this plot. Is this the result you wanted?
# Determine contour
img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)
img_bin = (img>128).astype(np.uint8)
contours, _ = cv2.findContours(img_bin, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
# Determine center of gravity and orientation using Moments
M = cv2.moments(contours[0])
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
theta = 0.5*np.arctan2(2*M["mu11"],M["mu20"]-M["mu02"])
endx = 600 * np.cos(theta) + center[0] # linelength 600
endy = 600 * np.sin(theta) + center[1]
# Display results
plt.imshow(img_bin, cmap='gray')
plt.scatter(center[0], center[1], marker="X")
plt.plot([center[0], endx], [center[1], endy])
plt.show()
My goal right now is to create an algorithm that can draw a middle line for this contour.
If you detect the upper and lower bounds of your horizontal-lines, then you can calculate the middle-line coordinates.
For instance:
Middle-line will be:
If you change the size to the width of the image:
Code:
import cv2
img = cv2.imread("contour.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
(h, w) = img.shape[:2]
x1_upper = h
x1_lower = 0
x2_upper = h
x2_lower = 0
y1_upper = h
y1_lower = 0
y2_upper = h
y2_lower = 0
lines = cv2.ximgproc.createFastLineDetector().detect(gray)
for cur in lines:
x1 = cur[0][0]
y1 = cur[0][1]
x2 = cur[0][2]
y2 = cur[0][3]
# upper-bound coords
if y1 < y1_upper and y2 < y2_upper:
y1_upper = y1
y2_upper = y2
x1_upper = x1
x2_upper = x2
elif y1 > y1_lower and y2 > y2_lower:
y1_lower = y1
y2_lower = y2
x1_lower = x1
x2_lower = x2
print("\n\n-lower-bound-\n")
print("({}, {}) - ({}, {})".format(x1_lower, y1_lower, x2_lower, y2_lower))
print("\n\n-upper-bound-\n")
print("({}, {}) - ({}, {})".format(x1_upper, y1_upper, x2_upper, y2_upper))
cv2.line(img, (x1_lower, y1_lower), (x2_lower, y2_lower), (0, 255, 0), 5)
cv2.line(img, (x1_upper, y1_upper), (x2_upper, y2_upper), (0, 0, 255), 5)
x1_avg = int((x1_lower + x1_upper) / 2)
y1_avg = int((y1_lower + y1_upper) / 2)
x2_avg = int((x2_lower + x2_upper) / 2)
y2_avg = int((y2_lower + y2_upper) / 2)
cv2.line(img, (0, y1_avg), (w, y2_avg), (255, 0, 0), 5)
cv2.imshow("result", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
I beleive skeleton is what you are looking for.
import cv2
import timeit
img = cv2.imread('Ggh8d - Copy.jpg',0)
s = timeit.default_timer()
thinned = cv2.ximgproc.thinning(img, thinningType = cv2.ximgproc.THINNING_ZHANGSUEN)
e = timeit.default_timer()
print(e-s)
cv2.imwrite("thinned1.png", thinned)
if smooth the edge a little bit
Actually the line will not torch the yellow point, since the algorithm have to check distance from edges, yellow point is located on the edge.
Here is another way to do that by computing the centerline of the rotated bounding box about your object in Python/OpenCV.
Input:
import cv2
import numpy as np
# load image
img = cv2.imread("blob_mask.jpg")
# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold the grayscale image
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1]
# get coordinates of all non-zero pixels
# NOTE: must transpose since numpy coords are y,x and opencv uses x,y
coords = np.column_stack(np.where(thresh.transpose() > 0))
# get rotated rectangle from
rotrect = cv2.minAreaRect(coords)
box = cv2.boxPoints(rotrect)
box = np.int0(box)
print (box)
# get center line from box
# note points are clockwise from bottom right
x1 = (box[0][0] + box[3][0]) // 2
y1 = (box[0][1] + box[3][1]) // 2
x2 = (box[1][0] + box[2][0]) // 2
y2 = (box[1][1] + box[2][1]) // 2
# draw rotated rectangle on copy of img as result
result = img.copy()
cv2.drawContours(result, [box], 0, (0,0,255), 2)
cv2.line(result, (x1,y1), (x2,y2), (255,0,0), 2)
# write result to disk
cv2.imwrite("blob_mask_rotrect.png", result)
# display results
cv2.imshow("THRESH", thresh)
cv2.imshow("RESULT", result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result:

Categories