Related
I've got some images of binary (black and white) grids that look like this:
Now, I want to convert such images to regular 2D NumPy arrays, where each cell must correspond to 0, if the source cell is white (or uncolored) and 1 if the cell is black. That is, the expected output is:
[[0,1,0,0,1],
[0,0,0,0,1],
[0,1,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,1,0],
[0,0,1,0,0]]
I've looked at a number of suggestions including this one, but they don't say anything about how I must reduce the raw pixels to a regular grid.
My current code:
import numpy as np
from PIL import Image
def from_img(imgfile, size, keep_ratio=True, reverse=False):
def resample(img_, size):
return img.resize(size, resample=Image.BILINEAR)
def makebw(img, threshold=200):
edges = (255 if reverse else 0, 0 if reverse else 255)
return img.convert('L').point(lambda x: edges[1] if x > threshold else edges[0], mode='1')
img = Image.open(imgfile)
if keep_ratio:
ratio = max(size) / max(img.size)
size = tuple(int(sz * ratio) for sz in img.size)
return np.array(makebw(resample(img, size)), dtype=int)
This code might be ok for images that don't contain borders between the cells, and only when specifying the number of rows and columns manually. But I am sure there must be a way of automating this routine by edge detection / resampling techniques...
Update
While there are good solutions (see suggested below) for even, regular black and white grids like shown above, the task is more difficult for uneven, noisy images with multiple non-BW colors like this one:
I'm now looking at an opencv implementation that detects contours and tries to single out the cell size to reconstruct the grid matrix. My current code:
import matplotlib.pyplot as plt
import numpy as np
import cv2
def find_contours(fpath, gray_thresh=150, extent_param=0.85, area_param=(0.0003, 0.3), ratio_param=(0.75, 1.33)):
"""
Finds contours (shapes) in an image (loading it from a file) and filters the contours
according to a number of parameters.
gray_thresh: grayscale threshold
extent_param: minimum extent of contour (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html#extent)
area_param: min and max ratio of contour area to image area
ratio_param: min and max ratio of contour (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html#aspect-ratio)
"""
image = cv2.imread(fpath)
# grayscale image
imgray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, gray_thresh, 255, 0)
# get all contours (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# get min and max contour area in pixels (from given ratios)
if area_param:
area = imgray.shape[0] * imgray.shape[1]
min_area = float(area) * area_param[0]
max_area = float(area) * area_param[1]
# filtered contours
contours2 = []
# contour sizes
sizes = []
# contour coords
pos = []
# iterate by found contours
for c in contours:
# get contour area
c_area = cv2.contourArea(c)
# get bounding rect
rect = cv2.boundingRect(c)
# get extent (ratio of contour area to bounding rect area)
extent = float(c_area) / (rect[2] * rect[3])
# get aspect ratio of bounding rect
ratio = float(rect[2]) / rect[3]
# perform filtering (leave rect-shaped contours or filter by extent)
if (len(c) == 4 or not extent_param or extent >= extent_param) and \
(not area_param or (c_area >= min_area and c_area <= max_area)) and \
(not ratio_param or (ratio >= ratio_param[0] and ratio <= ratio_param[1])):
# add filtered contour to list, as well as its size and pos
contours2.append(c)
sizes.append(rect[-2:])
pos.append(rect[:2])
# get most frequent block size (w, h), first and last block
size_mode = max(set(sizes), key=sizes.count)
first_pos = min(pos)
last_pos = max(pos)
# return original image, grayscale image, most frequent contour size, first and last contour coords
return image, imgray, contours2, size_mode, first_pos, last_pos
def get_mean_colors_of_contours(img, imgray, contours):
"""
Returns the mean colors of given contours and one common mean.
"""
l_means = []
for c in contours:
mask = np.zeros(imgray.shape, np.uint8)
cv2.drawContours(mask, [c], 0, 255, -1)
l_means.append(cv2.mean(img, mask=mask)[0])
return np.mean(l_means), l_means
def get_color(x):
if x == 'r':
return (255, 0, 0)
elif x == 'g':
return (0, 255, 0)
elif x == 'b':
return (0, 0, 255)
return x
def text_in_contours(img, contours, values, val_format=None, text_color='b', text_scale=1.0):
"""
Prints stuff inside given contours.
img: original image (array)
contours: identified contours
values: stuff to print (iterable of same length as contours)
val_format: optional callback function to format a single value before printing
text_color: color of output text (default = blue)
text_scale: initial font scale (font will be auto adjusted)
"""
text_color = get_color(text_color)
if not text_color: return
for c, val in zip(contours, values):
rect = cv2.boundingRect(c)
center = (rect[0] + rect[2] // 2, rect[1] + rect[3] // 2)
txt = val_format(val) if val_format else str(val)
if not txt: continue
font = cv2.FONT_HERSHEY_DUPLEX
fontScale = min(rect[2:]) * text_scale / 100
lineType = 1
text_size, _ = cv2.getTextSize(txt, font, fontScale, lineType)
text_origin = (center[0] - text_size[0] // 2, center[1] + text_size[1] // 2)
cv2.putText(img, txt, text_origin, font, fontScale, text_color, lineType, cv2.LINE_AA)
return img
def draw_contours(fpath, contour_color='r', contour_width=1, **kwargs):
"""
Finds contours in image and draws their outlines.
fpath: path to image file
contour_color: color used to outline contours (r,g,b, tuple or None)
contour_width: outline width
kwargs: args passed to find_contours()
"""
if not contour_color: return
contour_color = get_color(contour_color)
img, imgray, contours, size_mode, first_pos, last_pos = find_contours(fpath, **kwargs)
cv2.drawContours(img, contours, -1, contour_color, contour_width)
return img, imgray, contours, size_mode, first_pos, last_pos
def show_image(img, fig_height_inches=8):
"""
Shows an image in iPython notebook.
"""
height, width = img.shape[:2]
aspect = width / height
fig = plt.figure(figsize=(fig_height_inches * aspect, fig_height_inches))
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(img, interpolation='nearest', aspect='equal')
plt.show()
Now this helps me already identify the white cells in most cases, e.g.
img, imgray, contours, size_mode, first_pos, last_pos = draw_contours('sss4.jpg')
mean_col, cols = get_mean_colors_of_contours(img, imgray, contours)
print(f'mean color = {mean_col}')
on_contour = lambda val: str(int(val)) if (val / mean_col) >= 0.9 else None
img = text_in_contours(img, contours, cols, on_contour)
show_image(img, 15)
Output
mean color = 252.54154936140293
So, I only need now some way to reconstruct the grid with ones and zeros, adding ones in the missing spots (where no white cells were identified).
Given that you have a very nice grid with a regular shape, we can figure out the size of each tile by randomly sampling around and checking the size of our flood-filled area.
I used the mode of the counts I received back from the sample, but if you know some of the grids have a lot of black tiles, then you should probably take the smallest size returned by stipple() since anytime we hit a black tile, it'll include the entire background of the image which could overwhelm the count of white tiles.
Once we have the size of our tile, we can use that to index a pixel from each tile and check if it's white or black.
import cv2
import numpy as np
import random
import math
# stipple search
def stipple(mask, iters):
# get resolution
height, width = mask.shape[:2];
# do random checks
counts = [];
for a in range(iters):
# get random position
copy = np.copy(mask);
x = random.randint(0, width-1);
y = random.randint(0, height-1);
# fill
cv2.floodFill(copy, None, (x, y), 100);
# count
count = np.count_nonzero(copy == 100);
counts.append(count);
return counts;
# load image
gray = cv2.imread("tiles.jpg", cv2.IMREAD_GRAYSCALE);
# mask
mask = cv2.inRange(gray, 100, 255);
height, width = mask.shape[:2];
# check
sizes = stipple(mask, 10);
print(sizes);
# get most common size // or search for the smallest size
size = max(set(sizes), key=sizes.count);
# get side size
side = math.sqrt(size);
# get grid dimensions
grid_width = int(round(width / side));
grid_height = int(round(height / side));
print([grid_width, grid_height]);
# recalculate size to nearest rounded whole number
side = int(width / grid_width);
print(side);
# make grid
grid = [];
start_index = int(side / 2.0);
for y in range(start_index, height, side):
row = [];
for x in range(start_index, width, side):
row.append(mask[y,x] == 255);
grid.append(row[:]);
# print
out_str = "";
for row in grid:
for elem in row:
out_str += str(int(elem));
out_str += "\n";
print(out_str);
# show
cv2.imshow("Mask", mask);
cv2.waitKey(0);
My idea would be convert the input image to mode '1', somehow detect the tiles' width and height, resize the input image w.r.t. these, and simply convert to some NumPy array.
Detecting the tiles' width and height might work like this:
Detect changes between neighbouring pixels using np.diff, and create a union image from these information:
Calculate the distances between these detected changes, again using np.diff, np.sum, and np.nonzero.
Finally, get the median value of these distances using np.median, and from that, determine the number of rows and columns of the grid, and resize the input image accordingly.
Here's the full code:
import numpy as np
from PIL import Image
# Open image, convert to black and white mode
image = Image.open('grid.png').convert('1')
w, h = image.size
# Temporary NumPy array of type bool to work on
temp = np.array(image)
# Detect changes between neighbouring pixels
diff_y = np.diff(temp, axis=0)
diff_x = np.diff(temp, axis=1)
# Create union image of detected changes
temp = np.zeros_like(temp)
temp[:h-1, :] |= diff_y
temp[:, :w-1] |= diff_x
# Calculate distances between detected changes
diff_y = np.diff(np.nonzero(np.diff(np.sum(temp, axis=0))))
diff_x = np.diff(np.nonzero(np.diff(np.sum(temp, axis=1))))
# Calculate tile height and width
ht = np.median(diff_y[diff_y > 1]) + 2
wt = np.median(diff_x[diff_x > 1]) + 2
# Resize image w.r.t. tile height and width
array = (~np.array(image.resize((int(w/wt), int(h/ht))))).astype(int)
print(array)
For the given input image, we get the desired/expected output:
[[0 1 0 0 1]
[0 0 0 0 1]
[0 1 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 1 0]
[0 0 1 0 0]]
Full black columns or rows don't matter:
[[0 1 0 0 1]
[0 0 0 0 1]
[0 1 0 0 1]
[0 0 0 0 1]
[0 0 0 0 1]
[0 0 0 1 1]
[0 0 1 0 1]]
And, even single white tiles are enough:
[[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 0 1 1 1]
[1 1 1 1 1]]
For testing, I thresholded your input image, and saved it as single-channel PNG. For arbitrary JPG input images, you might want to have some thresholding before converting to mode '1' to avoid artifacts.
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.9.1
PyCharm: 2021.1.1
NumPy: 1.20.2
Pillow: 8.2.0
----------------------------------------
I am trying to detect a grainy printed line on a paper with cv2. I need the angle of the line. I dont have much knowledge in image processing and I only need to detect the line. I tried to play with the parameters but the angle is always detected wrong. Could someone help me. This is my code:
import cv2
import numpy as np
import matplotlib.pylab as plt
from matplotlib.pyplot import figure
img = cv2.imread('CamXY1_1.bmp')
crop_img = img[100:800, 300:900]
blur = cv2.GaussianBlur(crop_img, (1,1), 0)
ret,thresh = cv2.threshold(blur,150,255,cv2.THRESH_BINARY)
gray = cv2.cvtColor(thresh,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 60, 150)
figure(figsize=(15, 15), dpi=150)
plt.imshow(edges, 'gray')
lines = cv2.HoughLines(edges,1,np.pi/180,200)
for rho,theta in lines[0]:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 3000*(-b))
y1 = int(y0 + 3000*(a))
x2 = int(x0 - 3000*(-b))
y2 = int(y0 - 3000*(a))
cv2.line(img,(x1,y1),(x2,y2),(0, 255, 0),2)
imagetobedetected
Here's a possible solution to estimate the line (and its angle) without using the Hough line transform. The idea is to locate the start and ending points of the line using the reduce function. This function can reduce an image to a single column or row. If we reduce the image we can also get the total SUM of all the pixels across the reduced image. Using this info we can estimate the extreme points of the line and calculate its angle. This are the steps:
Resize your image because it is way too big
Get a binary image via adaptive thresholding
Define two extreme regions of the image and crop them
Reduce the ROIs to a column using the SUM mode, which is the sum of all rows
Accumulate the total values above a threshold value
Estimate the starting and ending points of the line
Get the angle of the line
Here's the code:
# imports:
import cv2
import numpy as np
import math
# image path
path = "D://opencvImages//"
fileName = "mmCAb.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Scale your BIG image into a small one:
scalePercent = 0.3
# Calculate the new dimensions
width = int(inputImage.shape[1] * scalePercent)
height = int(inputImage.shape[0] * scalePercent)
newSize = (width, height)
# Resize the image:
inputImage = cv2.resize(inputImage, newSize, None, None, None, cv2.INTER_AREA)
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Convert BGR to grayscale:
grayInput = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Adaptive Thresholding:
windowSize = 51
windowConstant = 11
binaryImage = cv2.adaptiveThreshold(grayInput, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, windowSize, windowConstant)
The first step is to get the binary image. Note that I previously downscaled your input because it is too big and we don't need all that info. This is the binary mask:
Now, we don't need most of the image. In fact, since the line is across the whole image, we can only "trim" the first and last column and check out where the white pixels begin. I'll crop a column a little bit wider, though, so we can ensure we have enough data and as less noise as possible. I'll define two Regions of Interest (ROIs) and crop them. Then, I'll reduce each ROI to a column using the SUM mode, this will give me the summation of all intensity across each row. After that, I can accumulate the locations where the sum exceeds a certain threshold and approximate the location of the line, like this:
# Define the regions that will be cropped
# from the original image:
lineWidth = 5
cropPoints = [(0, 0, lineWidth, height), (width-lineWidth, 0, lineWidth, height)]
# Store the line points here:
linePoints = []
# Loop through the crop points and
# crop de ROI:
for p in range(len(cropPoints)):
# Get the ROI:
(x,y,w,h) = cropPoints[p]
# Crop the ROI:
imageROI = binaryImage[y:y+h, x:x+w]
# Reduce the ROI to a n row x 1 columns matrix:
reducedImg = cv2.reduce(imageROI, 1, cv2.REDUCE_SUM, dtype=cv2.CV_32S)
# Get the height (or lenght) of the arry:
reducedHeight = reducedImg.shape[0]
# Define a threshold and accumulate
# the coordinate of the points:
threshValue = 100
pointSum = 0
pointCount = 0
for i in range(reducedHeight):
currentValue = reducedImg[i]
if currentValue > threshValue:
pointSum = pointSum + i
pointCount = pointCount + 1
# Get average coordinate of the line:
y = int(accX / pixelCount)
# Store in list:
linePoints.append((x, y))
The red rectangles show the regions I cropped from the input image:
Note that I've stored both points in the linePoints list. Let's check out our approximation by drawing a line that connects both points:
# Get the two points:
p0 = linePoints[0]
p1 = linePoints[1]
# Draw the line:
cv2.line(inputImageCopy, (p0[0], p0[1]), (p1[0], p1[1]), (255, 0, 0), 1)
cv2.imshow("Line", inputImageCopy)
cv2.waitKey(0)
Which yields:
Not bad, huh? Now that we have both points, we can estimate the angle of this line:
# Get angle:
adjacentSide = p1[0] - p0[0]
oppositeSide = p0[1] - p1[1]
# Compute the angle alpha:
alpha = math.degrees(math.atan(oppositeSide / adjacentSide))
print("Angle: "+str(alpha))
This prints:
Angle: 0.534210901840831
I am trying to transform an image along the edge of the object (here the object is the book). Using canny edge detection, I am detecting the edges and from the score matrix, based on pixel value, I am choosing a random 4 coordinates lying on the edge for transformation. But the transformation is not as it thought it would be. What is the problem/Where am I missing out?
First I have sliced out a portion of the image. Then applied canny edge detection and randomly selected 4 edge coordinate points based on my own condition as:
My original image is:
For experiment I have sliced out according to my need as:
The size of this image (61,160)
Now I need to transform the above image to make the edge of the book parallel to the horizontal axis.
img = cv2.imread('download1.jpg',0)
edges = cv2.Canny(img,100,200)
print(img.shape)
plt.show()
plt.imshow(img,cmap='gray')
l=[]
y_list=[]
k=1
for i in range (0,img.shape[0]):
for j in range (0,img.shape[1]):
if (edges[i][j]==255) and k<=4 and i>31 and j not in y_list:
l.append([j,i])
y_list.append(j)
k+=1
break
The edge detection image is obtained as:
The contents of l list are
[[49 32]
[44 33]
[40 34]
[36 35]]
Then set the destination points given by list lt as:
[[49 61]
[44 60]
[40 61]
[36 60]]
Then found out the homography matrix and used it to find out the warp perspective as :
h, status = cv2.findHomography(l,lt)
im_out = cv2.warpPerspective(img, h, (img.shape[1],img.shape[0]))
But it doesnot produce the required result! The resultant output image is obtained as:
I faced a similar issue, and this is how I solved it (quite similar to your method actually), just I used get rotation matrix instead homografy:
read image
edge detector
hough line to get all the lines (with an inclination inside a specific interval)
lines = cv.HoughLinesP(img, 1, np.pi/180, 100, minLineLength=100, maxLineGap=10)
get lines average inclination, cause in my case I had lot of parallel lines to use as
references and in this way I was able to get a better result
for line in lines:
x1,y1,x2,y2 = line[0]
if (x2-x1) != 0:
angle = math.atan((float(y2-y1))/float((x2-x1))) * 180 / math.pi
else:
angle = 90
#you can skip this test if you have no info about the lines you re looking for
#in this case offset_angle is = 0
if min_angle_threshold <= angle <= max_angle_threshold:
tot_angle = tot_angle + angle
cnt = cnt + 1
average_angle = (tot_angle / cnt) - offset_angle
apply the counter-rotation
center = your rotation center - probably the center of the image
rotation_matrix = cv.getRotationMatrix2D(center, angle, 1.0)
height, width = img.shape
rotated_image = cv.warpAffine(img, rotation_matrix, (width, height))
#do whatever you want, then rotate image back
counter_rotation_matrix = cv.getRotationMatrix2D(center, -angle, 1.0)
original_image = cv.warpAffine( rotated_image, counter_rotation_matrix, (width, height))
Edit: see the full example here:
import math
import cv2 as cv
img = cv.imread('C:\\temp\\test_3.jpg',0)
edges = cv.Canny(img,100,200)
lines = cv.HoughLinesP(edges[0:50,:], 1, np.pi/180, 50, minLineLength=10, maxLineGap=10)
tot_angle = 0
cnt = 0
for line in lines:
x1,y1,x2,y2 = line[0]
if (x2-x1) != 0:
angle = math.atan((float(y2-y1))/float((x2-x1))) * 180 / math.pi
else:
angle = 90
if -30 <= angle <= 30:
tot_angle = tot_angle + angle
cnt = cnt + 1
average_angle = (tot_angle / cnt)
h,w = img.shape[:2]
center = w/2, h/2
rotation_matrix = cv.getRotationMatrix2D(center, average_angle, 1.0)
height, width = img.shape
rotated_image = cv.warpAffine(img, rotation_matrix, (width, height))
cv.imshow("roto", rotated_image)
#do all your stuff here, add text and whatever
#...
#...
counter_rotation_matrix = cv.getRotationMatrix2D(center, -average_angle, 1.0)
original_image = cv.warpAffine( rotated_image, counter_rotation_matrix, (width, height))
cv.imshow("orig", original_image)
rotated
]1
counter_rotated
]2
EDIT:
in case you want apply an homography(different than just a simple rotation, 'cause it also applies a perspective transformation), below the code to make it work:
#very basic example, similar to your code with fixed terms
l = np.array([(11,32),(43,215),(142,1),(205,174)])
lt = np.array([(43,32),(43,215),(205,32),(205,215)])
h, status = cv.findHomography(l,lt)
im_out = cv.warpPerspective(img, h, (img.shape[1],img.shape[0]))
To do it programmatically
- for "l" : just use houghlines as well and find the 4 corners,
then add them
for "lt": find a "destination" for all the 4 points, for instance use the bottom corners as reference
lines = cv.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength=150, maxLineGap=5)
l = []
for line in lines:
x1,y1,x2,y2 = line[0]
if (x2-x1) != 0:
angle = math.atan((float(y2-y1))/float((x2-x1))) * 180 / math.pi
else:
angle = 90
# consider only vertical edges
if 60 <= angle:
l.append((x1,y1))
l.append((x2,y2))
x_values.append(max(x1,x2))
if len(y_values) == 0:
y_values.append(y1)
y_values.append(y2)
l = np.array(l)
lt = np.array([(x_values[0],y_values[0]),(x_values[0],y_values[1]),(x_values[1],y_values[0]),(x_values[1],y_values[1])])
then call findhomography as done above
Hope it's clear enough!
3
I got two detected contours in an image and need the diameter between the two vertical-edges of the top contour and the diameter between the vertical-edges of the lower contour. I achieved this with this code.
import cv2
import numpy as np
import math, os
import imutils
img = cv2.imread("1.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
edges = cv2.Canny(gray, 200, 100)
edges = cv2.dilate(edges, None, iterations=1)
edges = cv2.erode(edges, None, iterations=1)
cnts = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# sorting the contours to find the largest and smallest one
c1 = max(cnts, key=cv2.contourArea)
c2 = min(cnts, key=cv2.contourArea)
# determine the most extreme points along the contours
extLeft1 = tuple(c1[c1[:, :, 0].argmin()][0])
extRight1 = tuple(c1[c1[:, :, 0].argmax()][0])
extLeft2 = tuple(c2[c2[:, :, 0].argmin()][0])
extRight2 = tuple(c2[c2[:, :, 0].argmax()][0])
# show contour
cimg = cv2.drawContours(img, cnts, -1, (0,200,0), 2)
# set y of left point to y of right point
lst1 = list(extLeft1)
lst1[1] = extRight1[1]
extLeft1 = tuple(lst1)
lst2 = list(extLeft2)
lst2[1] = extRight2[1]
extLeft2= tuple(lst2)
# compute the distance between the points (x1, y1) and (x2, y2)
dist1 = math.sqrt( ((extLeft1[0]-extRight1[0])**2)+((extLeft1[1]-extRight1[1])**2) )
dist2 = math.sqrt( ((extLeft2[0]-extRight2[0])**2)+((extLeft2[1]-extRight2[1])**2) )
# draw lines
cv2.line(cimg, extLeft1, extRight1, (255,0,0), 1)
cv2.line(cimg, extLeft2, extRight2, (255,0,0), 1)
# draw the distance text
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale = 0.5
fontColor = (255,0,0)
lineType = 1
cv2.putText(cimg,str(dist1),(155,100),font, fontScale, fontColor, lineType)
cv2.putText(cimg,str(dist2),(155,280),font, fontScale, fontColor, lineType)
# show image
cv2.imshow("Image", img)
cv2.waitKey(0)
Now I would also need the angle of the slope lines on the bottom side of the upper contour.
Any ideas how I can get this? Is it possible using contours?
Or is it necessary to use HoughLinesP and sort the regarding lines somehow?
And continued question: Maybe its also possible to get function which describes parabola slope of that sides ?
Thanks alot for any help!
There are several ways to obtain just the slopes. In order to know the slope, we can can use cv2.HoughLines to detect the bottom horizontal line, detect to end points of that line and from those, obtain the slopes. As an illustration,
lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180, threshold=int(dist2*0.66) )
on edges in your code gives 4 lines, and if we force the angle to be horizontal
for line in lines:
rho, theta = line[0]
# here we filter out non-horizontal lines
if abs(theta - np.pi/2) > np.pi/180:
continue
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img_lines,(x1,y1),(x2,y2),(0,0,255),1)
we get:
For the extended question concerns with the parabolas, we first compose a function that returns the left and right points:
def horizontal_scan(gray_img, thresh=50, start=50):
'''
scan horizontally for left and right points until we met an all-background line
#param thresh: threshold for background pixel
#param start: y coordinate to start scanning
'''
ret = []
thickness = 0
for i in range(start,len(gray_img)):
row = gray_img[i]
# scan for left:
left = 0
while left < len(row) and row[left]<thresh:
left += 1
if left==len(row):
break;
# scan for right:
right = left
while right < len(row) and row[right] >= thresh:
right+=1
if thickness == 0:
thickness = right - left
# prevent sudden drop, error/noise
if (right-left) < thickness//5:
continue
else:
thickness = right - left
ret.append((i,left,right))
return ret
# we start scanning from extLeft1 down until we see a blank line
# with some tweaks, we can make horizontal_scan run on edges,
# which would be simpler and faster
horizontal_lines = horizontal_scan(gray, start = extLeft1[1])
# check if horizontal_line[0] are closed to extLeft1 and extRight1
print(horizontal_lines[0], extLeft1, extRight1[0])
Note that we can use this function to find the end points of the horizontal line returned by HoughLines.
# last line of horizontal_lines would be the points we need:
upper_lowest_y, upper_lowest_left, upper_lowest_right = horizontal_lines[-1]
img_lines = img.copy()
cv2.line(img_lines, (upper_lowest_left, upper_lowest_y), extLeft1, (0,0,255), 1)
cv2.line(img_lines, (upper_lowest_right, upper_lowest_y), extRight1, (0,0,255),1)
and that gives:
Let's return to the extended question, where we have those left and right points:
left_points = [(x,y) for y,x,_ in horizontal_lines]
right_points = [(x,y) for y,_,x in horizontal_lines]
Obviously, they would not fit perfectly in a parabola, so we need some sort of approximation/fitting here. For that, we can build a LinearRegression model:
from sklearn.linear_model import LinearRegression
class BestParabola:
def __init__(self, points):
x_x2 = np.array([(x**2,x) for x,_ in points])
ys = np.array([y for _,y in points])
self.lr = LinearRegression()
self.lr.fit(x_x2,ys)
self.a, self.b = self.lr.coef_
self.c = self.lr.intercept_
self.coef_ = (self.c,self.b,self.a)
def transform(self,points):
x_x2 = np.array([(x**2,x) for x,_ in points])
ys = self.lr.predict(x_x2)
return np.array([(x,y) for (_,x),y in zip(x_x2,ys)])
And then, we can fit the given left_points, right_points to get the desired parabolas:
# construct the approximate parabola
# the parabollas' coefficients are accessible by BestParabola.coef_
left_parabola = BestParabola(left_points)
right_parabola = BestParabola(right_points)
# get points for rendering
left_parabola_points = left_parabola.transform(left_points)
right_parabola_points = right_parabola.transform(right_points)
# render with matplotlib, cv2.drawContours would work
plt.figure(figsize=(8,8))
plt.imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB))
plt.plot(left_parabola_points[:,0], left_parabola_points[:,1], linewidth=3)
plt.plot(right_parabola_points[:,0], right_parabola_points[:,1], linewidth=3, color='r')
plt.show()
Which gives:
The left parabola is not perfect, but you should work out that if need be :-)
I'm interested in calculating the average minimum distance between elements of two sets of contours.
Here is a sample image:
Here's my code so far:
import cv2
import numpy as np
def contours(layer):
gray = cv2.cvtColor(layer, cv2.COLOR_BGR2GRAY)
ret,binary = cv2.threshold(gray, 1,255,cv2.THRESH_BINARY)
image, contours, hierarchy = cv2.findContours(binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
drawn = cv2.drawContours(image,contours,-1,(150,150,150),3)
return contours, drawn
def minDistance(contour, contourOther):
distanceMin = 99999999
for xA, yA in contour[0]:
for xB, yB in contourOther[0]:
distance = ((xB-xA)**2+(yB-yA)**2)**(1/2) # distance formula
if (distance < distanceMin):
distanceMin = distance
return distanceMin
def cntDistanceCompare(contoursA, contoursB):
cumMinDistList = []
for contourA in contoursA:
indMinDistList = []
for contourB in contoursB:
minDist = minDistance(contourA,contourB)
indMinDistList.append(minDist)
cumMinDistList.append(indMinDistList)
l = cumMinDistList
return sum(l)/len(l) #returns mean distance
def maskBuilder(bgr,hl,hh,sl,sh,vl,vh):
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
lower_bound = np.array([hl,sl,vl],dtype=np.uint8)
upper_bound = np.array([hh,sh,vh],dtype=np.uint8)
return cv2.inRange(hsv, lower_bound,upper_bound)
img = cv2.imread("sample.jpg")
maskA=maskBuilder(img, 150,185, 40,220, 65,240)
maskB=maskBuilder(img, 3,20, 50,180, 20,250)
layerA = cv2.bitwise_and(img, img, mask = maskA)
layerB = cv2.bitwise_and(img, img, mask = maskB)
contoursA = contours(layerA)[0]
contoursB = contours(layerA)[1]
print cntDistanceCompare(contoursA, contoursB)
As you can see from these images, the masking and thesholding works (shown for the first set of contours):
The cntDistanceCompare() function loops through each contour of set A and B, outputting average minimum distance between contours. Within this function, minDistance() calculates from the (x,y) points on each set of contours A and B a minimum pythagorean distance (using the distance formula).
The following error is thrown:
Traceback (most recent call last):
File "mindistance.py", line 46, in
cntDistanceCompare(contoursA, contoursB)
File "mindistance.py", line 26, in cntDistanceCompare
minDist = minDistance(contourA,contourB)
File "mindistance.py:, line 15, in minDistance
for xB, yB in contourOther[0]:
TypeError: 'numpy.uint8' object is not iterable
I suspect this problem arises from my lack of knowledge of how to reference the x,y coordinates of each contour vertex within the data structre given by cv2.findContours().
I am using an older version of openCV where findContours only returns two values, but hopefully the important part of this code makes sense. I didn't test your functions, but I did show how to get the contour centers. You have to do some stuff with "moments."
import cv2
import numpy as np
def contours(layer):
gray = cv2.cvtColor(layer, cv2.COLOR_BGR2GRAY)
ret,binary = cv2.threshold(gray, 1,255,cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
#drawn = cv2.drawContours(image,contours,-1,(150,150,150),3)
return contours #, drawn
def minDistance(contour, contourOther):
distanceMin = 99999999
for xA, yA in contour[0]:
for xB, yB in contourOther[0]:
distance = ((xB-xA)**2+(yB-yA)**2)**(1/2) # distance formula
if (distance < distanceMin):
distanceMin = distance
return distanceMin
def cntDistanceCompare(contoursA, contoursB):
cumMinDistList = []
for contourA in contoursA:
indMinDistList = []
for contourB in contoursB:
minDist = minDistance(contourA,contourB)
indMinDistList.append(minDist)
cumMinDistList.append(indMinDistList)
l = cumMinDistList
return sum(l)/len(l) #returns mean distance
def maskBuilder(bgr,hl,hh,sl,sh,vl,vh):
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
lower_bound = np.array([hl,sl,vl],dtype=np.uint8)
upper_bound = np.array([hh,sh,vh],dtype=np.uint8)
return cv2.inRange(hsv, lower_bound,upper_bound)
def getContourCenters(contourData):
contourCoordinates = []
for contour in contourData:
moments = cv2.moments(contour)
contourX = int(moments['m10'] / float(moments['m00']))
contourY = int(moments['m01'] / float(moments['m00']))
contourCoordinates += [[contourX, contourY]]
return contourCoordinates
img = cv2.imread("sample.jpg")
maskA=maskBuilder(img, 150,185, 40,220, 65,240)
maskB=maskBuilder(img, 3,20, 50,180, 20,250)
layerA = cv2.bitwise_and(img, img, mask = maskA)
layerB = cv2.bitwise_and(img, img, mask = maskB)
contoursA = contours(layerA)
contoursB = contours(layerB)
print getContourCenters(contoursA)
print getContourCenters(contoursB)
#print cntDistanceCompare(contoursA, contoursB)
Edit: I'm playing with your functions now and I fear I misread the question. Let me know and I'll delete my answer.