How to obtain the extreme edges from an identified contour? - python

I need to know how can I capture the extreme edges of a line(starting and ending coordinates) after identifying the contours. Currently, I am identifying the contours for the shapes(different types of lines) in the following image and drawing them back to a new image. I have already tried to obtain the topmost, the bottom-most, the leftmost, and the right-most coordinates from the contours array but they will not accurate to a line that has curves like below. So is there any way to capture those starting and ending points from the contours array?
Source Code
import cv2
import numpy as np
# Let's load a simple image with 3 black squares
image = cv2.imread("C:/Users/Hasindu/3D Objects/edge-test-188.jpg")
cv2.waitKey(0)
# Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Find Canny edges
edged = cv2.Canny(gray, 30, 200)
cv2.waitKey(0)
# Finding Contours
# Use a copy of the image e.g. edged.copy()
# since findContours alters the image
contours, hierarchy = cv2.findContours(edged,
cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cv2.imshow('Canny Edges After Contouring', edged)
cv2.waitKey(0)
print("Number of Contours found = " + str(len(contours)))
print(contours)
topmost = tuple(contours[0][contours[0][:,:,1].argmin()][0]);
bottommost = tuple(contours[0][contours[0][:,:,1].argmax()][0]);
print(topmost);
print(bottommost);
# Draw all contours
# -1 signifies drawing all contours
cv2.drawContours(image, contours, -1, (0, 255, 0), 3)
cv2.imshow('Contours', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Input 1
Output 1
EDIT:
I have followed the solution suggested by stateMachine but it was not 100 percent accurate on my all the inputs.You can see clearly some of the endpoints on the 2nd input image are not detected by the solution.
Input 2
Output 2

One possible solution involves applying the approach in this post. It involves convolving the input image with a special kernel used to identify end-points. These are the steps:
Converting the image to grayscale
Getting a binary image by applying Otsu's thresholding to the grayscale image
Applying a little bit of morphology, to ensure we have continuous and closed curves
Compute the skeleton of the image
Convolve the skeleton with the end-points kernel
Draw the end-points on the original image
Let's see the code:
# Imports:
import cv2
import numpy as np
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Prepare a deep copy of the input for results:
inputImageCopy = inputImage.copy()
# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
The first bit is very straightforward. Just get a binary image using Otsu's Thresholding. This is the result:
The thresholding could miss some pixels inside the curves, leading to "gaps". We don't want that, because we are trying to identify the end-points, which are essentially gaps on the curves. Let's fill possible gaps using a little bit of morphology - a closing will help fill those smaller gaps:
# Set morph operation iterations:
opIterations = 2
# Get the structuring element:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# Perform Closing:
binaryImage = cv2.morphologyEx(binaryImage, cv2.MORPH_CLOSE, kernel, None, None, opIterations, cv2.BORDER_REFLECT101)
This is now the result:
Ok, what follows is getting the skeleton of the binary image. The skeleton is a version of the binary image where lines have been normalized to have a width of 1 pixel. This is useful because we can then convolve the image with a 3 x 3 kernel and look for specific pixel patterns - those that identify a end-point. Let's compute the skeleton using OpenCV's extended image processing module:
# Compute the skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
Nothing fancy, the thing is done in just one line of code. The result is this:
It is very subtle with this image, but the curves have now 1 px of width, so we can apply the convolution. The main idea of this approach is that the convolution yields a very specific value where patterns of black and white pixels are found in the input image. The value we are looking for is 110, but we need to perform some operations before the actual convolution. Refer to the original post for details. These are the operations:
# Threshold the image so that white pixels get a value of 0 and
# black pixels a value of 10:
_, binaryImage = cv2.threshold(skeleton, 128, 10, cv2.THRESH_BINARY)
# Set the end-points kernel:
h = np.array([[1, 1, 1],
[1, 10, 1],
[1, 1, 1]])
# Convolve the image with the kernel:
imgFiltered = cv2.filter2D(binaryImage, -1, h)
# Extract only the end-points pixels, those with
# an intensity value of 110:
endPointsMask = np.where(imgFiltered == 110, 255, 0)
# The above operation converted the image to 32-bit float,
# convert back to 8-bit uint
endPointsMask = endPointsMask.astype(np.uint8)
If we imshow the endPointsMask, we would get something like this:
In the above image, you can see the location of the identified end-points. Let's get the coordinates of these white pixels:
# Get the coordinates of the end-points:
(Y, X) = np.where(endPointsMask == 255)
Finally, let's draw circles on these locations:
# Draw the end-points:
for i in range(len(X)):
# Get coordinates:
x = X[i]
y = Y[i]
# Set circle color:
color = (0, 0, 255)
# Draw Circle
cv2.circle(inputImageCopy, (x, y), 3, color, -1)
cv2.imshow("Points", inputImageCopy)
cv2.waitKey(0)
This is the final result:
EDIT: Identifying which blob produces each set of points
Since you need to also know which blob/contour/curve produced each set of end-points, you can re-work the code below with some other functions to achieve just that. Here, I'll mainly rely on a previous function I wrote that is used to detect the biggest blob in an image. One of the two curves will always be bigger (i.e., have a larger area) than the other. If you extract this curve, process it, and then subtract it from the original image iteratively, you could process curve by curve, and each time you could know which curve (the current biggest one) produced the current end-points. Let's modify the code to implement these ideas:
# Imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "w97nr.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# Set morph operation iterations:
opIterations = 2
# Get the structuring element:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# Perform Closing:
binaryImage = cv2.morphologyEx(binaryImage, cv2.MORPH_CLOSE, kernel, None, None, opIterations, cv2.BORDER_REFLECT101)
# Compute the skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
Up until the skeleton computation, everything is the same. Now, we will extract the current biggest blob and process it to obtain its end-points, we will continue extracting the current biggest blob until there are no more curves to extract. So, we just modify the prior code to manage the iterative nature of this idea. Additionaly, let's store the end-points on a list. Each row of this list will denote a new curve:
# Processing flag:
processBlobs = True
# Shallow copy for processing loop:
blobsImage = skeleton
# Store points per blob here:
blobPoints = []
# Count the number of processed blobs:
blobCounter = 0
# Start processing blobs:
while processBlobs:
# Find biggest blob on image:
biggestBlob = findBiggestBlob(blobsImage)
# Prepare image for next iteration, remove
# currrently processed blob:
blobsImage = cv2.bitwise_xor(blobsImage, biggestBlob)
# Count number of white pixels:
whitePixelsCount = cv2.countNonZero(blobsImage)
# If the image is completely black (no white pixels)
# there are no more curves to process:
if whitePixelsCount == 0:
processBlobs = False
# Threshold the image so that white pixels get a value of 0 and
# black pixels a value of 10:
_, binaryImage = cv2.threshold(biggestBlob, 128, 10, cv2.THRESH_BINARY)
# Set the end-points kernel:
h = np.array([[1, 1, 1],
[1, 10, 1],
[1, 1, 1]])
# Convolve the image with the kernel:
imgFiltered = cv2.filter2D(binaryImage, -1, h)
# Extract only the end-points pixels, those with
# an intensity value of 110:
endPointsMask = np.where(imgFiltered == 110, 255, 0)
# The above operation converted the image to 32-bit float,
# convert back to 8-bit uint
endPointsMask = endPointsMask.astype(np.uint8)
# Get the coordinates of the end-points:
(Y, X) = np.where(endPointsMask == 255)
# Prepare random color:
color = (np.random.randint(low=0, high=256), np.random.randint(low=0, high=256), np.random.randint(low=0, high=256))
# Prepare id string:
string = "Blob: "+str(blobCounter)
font = cv2.FONT_HERSHEY_COMPLEX
tx = 10
ty = 10 + 10 * blobCounter
cv2.putText(inputImageCopy, string, (tx, ty), font, 0.3, color, 1)
# Store these points in list:
blobPoints.append((X,Y, blobCounter))
blobCounter = blobCounter + 1
# Draw the end-points:
for i in range(len(X)):
x = X[i]
y = Y[i]
cv2.circle(inputImageCopy, (x, y), 3, color, -1)
cv2.imshow("Points", inputImageCopy)
cv2.waitKey(0)
This loop extracts the biggest blob and processes it just like in the first part of the post - we convolve the image with the end-point kernel and locate the matching points. For the original input, this would be the result:
As you see, each set of points is drawn using one unique color (randomly generated). There's also the current blob "ID" (just an ascending count) drawn in text with the same color as each set of points, so you know which blob produced each set of end-points. The info is stored in the blobPoints list, we can print its values, like this:
# How many blobs where found:
blobCount = len(blobPoints)
print("Found: "+str(blobCount)+" blobs.")
# Let's check out each blob and their end-points:
for b in range(blobCount):
# Fetch data:
p1 = blobPoints[b][0]
p2 = blobPoints[b][1]
id = blobPoints[b][2]
# Print data for each blob:
print("Blob: "+str(b)+" p1: "+str(p1)+" p2: "+str(p2)+" id: "+str(id))
Which prints:
Found: 2 blobs.
Blob: 0 p1: [39 66] p2: [ 42 104] id: 0
Blob: 1 p1: [129 119] p2: [25 49] id: 1
This is the implementation of the findBiggestBlob function, which just computes the biggest blob on the image using its area. It returns an image of the biggest blob isolated, this comes from a C++ implementation I wrote of the same idea:
def findBiggestBlob(inputImage):
# Store a copy of the input image:
biggestBlob = inputImage.copy()
# Set initial values for the
# largest contour:
largestArea = 0
largestContourIndex = 0
# Find the contours on the binary image:
contours, hierarchy = cv2.findContours(inputImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Get the largest contour in the contours list:
for i, cc in enumerate(contours):
# Find the area of the contour:
area = cv2.contourArea(cc)
# Store the index of the largest contour:
if area > largestArea:
largestArea = area
largestContourIndex = i
# Once we get the biggest blob, paint it black:
tempMat = inputImage.copy()
cv2.drawContours(tempMat, contours, largestContourIndex, (0, 0, 0), -1, 8, hierarchy)
# Erase smaller blobs:
biggestBlob = biggestBlob - tempMat
return biggestBlob

After finding the contours, we apply a special filter for each point of the contour. It applies the mask centered around each pixel, then finds the contours (or connected components or blobs) in the masked region. Ideally, for end points there'll be only one blob in the region, for other points there'll be more than one. We take the candidate end points for each contour, then cluster them into two clusters because there'll be more than two candidates because of the filter width and line thickness. If the clustering outputs two points, they are the end points of the processed contour.
An example is shown below.
mask:
1 1 1 1 1
1 0 0 0 1
1 0 0 0 1
1 0 0 0 1
1 1 1 1 1
image:
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 1 0 0 0
0 0 0 0 1 1 1 1 0 0 0
0 0 0 0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
response for end point:
1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 1 & 0 0 1 1 1 = 0 0 0 0 1
1 0 0 0 1 0 0 0 1 1 0 0 0 0 1
1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
response for corner point:
1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 1 & 1 1 1 0 0 = 1 0 0 0 0
1 0 0 0 1 1 1 1 0 0 1 0 0 0 0
1 1 1 1 1 0 0 1 0 0 0 0 1 0 0
This won't work well if the image is too noisy or your inputs are jpegs and the thresholding isn't good, because it can introduce some stray components in the masked region so that the point is counted as not a candidate for an end point.
If the lines in your input image (or the thresholded image) are more than 2 pixels wide, you can change the filter radius (r in the code).
If the line gap is less than two pixels, again you'll have problems with the current filter or anything larger. But in this case, you can draw each contour in a separate image and then apply the filter, but I haven't done this in the code for simplicity.
Here, we are using CHAIN_APPROX_SIMPLE to reduce the contour pixel count, and Otsu thresholding. For simplicity, the code does not handle cases where contour points fall at image boundaries.
import cv2 as cv
import numpy as np
im = cv.imread('dclSa.jpg')
gray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
# apply Otsu threshold
th, bw = cv.threshold(gray, 0, 1, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)
# find contours
contours, _ = cv.findContours(bw, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# create filter
r = 2
mask = np.ones((2*r+1, 2*r+1), dtype=np.uint8)
mask[1:2*r, 1:2*r] = 0
#print mask
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 5, 1.0)
for contour in contours:
all_x = []
all_y = []
for point in contour:
x = point[0][0]
y = point[0][1]
# extract the region centered around the contour pixel
roi = bw[y-r:y+r+1, x-r:x+r+1]
# find the blobs in masked region
n, _ = cv.findContours(roi & mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# if the blob count is 1, this pixel is an end point candidate
# if you use cv.connectedComponents to find blobs, then check 2 == n as it counts background
if 1 == len(n):
all_x.append(x)
all_y.append(y)
# if there are no candidate points, check next contour
if not all_x:
continue
# we are done with a contour. cluster the end point candidates into two clusters
points = np.vstack((all_x, all_y))
_, _, endpoints = cv.kmeans(np.float32(points.transpose()), 2, None, criteria, 5, cv.KMEANS_RANDOM_CENTERS)
# if the clustering goes well, we'll have the two end points of the contour
if 2 == len(endpoints) and 2 == len(endpoints[0]) and 2 == len(endpoints[1]):
im = cv.circle(im, (int(endpoints[0][0]), int(endpoints[0][1])), 3, (255, 0, 0), -1)
im = cv.circle(im, (int(endpoints[1][0]), int(endpoints[1][1])), 3, (0, 255, 0), -1)

Related

Which value is alpha in cv2 Laplacian function

I am trying to apply Laplacian filter to image from following text.
cv.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
But I am not sure which value is alpha.
Text:
we apply a 3×3 Laplacian filter with α = 0.2 to the image, and take its absolute value to ignore the direction of the gradients. For color images, we apply the filter to each of the red, green, and blue channels separately and then take the mean across the channels. Finally, we resize Laplacian image size to 100 × 100 and normalize the image sum to 1. This allows us to easily calculate the edge spatial distribution of the professional photos and snapshots by taking the mean across all the Laplacian images in each set.
In my experience, the use of an argument, possibly, alpha, for image sharpening is often done as follows:
result = input + alpha*Laplacian(input)
In terms of a convolution kernel that would be
result kernel = identify kernel + alpha * Laplacian kernel
So
result kernel = 0 0 0 + alpha * 0 -1 0
0 1 0 -1 4 -1
0 0 0 0 -1 0
and adding as
result kernel = 0 -alpha 0
-alpha 1+4*alpha -alpha
0 -alpha 0
The above may need clipping as it may overshoot.
So, alternately and probably better would be,
result = (1-alpha)*input + alpha*Laplacian(input)
So
result kernel = 0 0 0 + 0 -alpha 0
0 (1-alpha) 0 -alpha 4*alpha -alpha
0 0 0 0 -alpha 0
and adding together as
result kernel = 0 -alpha 0
-alpha 1+3*alpha -alpha
0 -alpha 0
So these would be implemented in Python/OpenCV as cv2.filter2D().
Note, that the negative of the Laplacian is typically used.

How to find the empty squares in a chess board image?

I am currently writing an image recognition script that makes a 2D array out of an image of a chess board for my chess project. However, I found it quite difficult to find which squares are empty:
So far, I have used the Canny edge detection on my image after applying the gaussian blur, which yielded the following results:
The code I used was:
sigma = 0.33
v = np.median(img)
img = cv2.GaussianBlur(img, (7, 7), 2) # we use gaussian blur on the image to make it clear.
lower = int(max(0, (1.0 - sigma) * v)) # we find the lower threshold.
upper = int(min(255, (1.0 + sigma) * v)) # we find the higher threshold.
img_edge = cv2.Canny(img, 50, 50) # we use the canny function to edge canny the image.
cv2.imshow('question', img_edge) # we show the image.
cv2.waitKey(0)
(You may notice I did not use the threshold I got, that's because I found it inaccurate. If anyone has any tips I'd love them!)
Now, after doing these steps, I have tried many other things such as finding contours, Hough transform, etc. Yet I can't seem to figure out how to move on from that and actually find out whether a square is empty.
Any help is appreciated!
Assuming you have some kind of square shaped input image covering the whole chess board (as the example suggests), you can resize the image by rounding width and height to the next smaller multiple of 8. So, you can derive 64 equally sized tiles from your image. For each tile, count the number of unique colors. Set up some threshold to distinguish two classes (empty vs. non-empty square), maybe by using Otsu's method.
That'd be my code (half of that is simply visualization stuff):
import cv2
import matplotlib.pyplot as plt
import numpy as np
from skimage.filters import threshold_otsu
# Round to next smaller multiple of 8
# https://www.geeksforgeeks.org/round-to-next-smaller-multiple-of-8/
def round_down_to_next_multiple_of_8(a):
return a & (-8)
# Read image, and shrink to quadratic shape with width and height of
# next smaller multiple of 8
img = cv2.imread('m0fAx.png')
wh = np.min(round_down_to_next_multiple_of_8(np.array(img.shape[:2])))
img = cv2.resize(img, (wh, wh))
# Prepare some visualization output
out = img.copy()
plt.figure(1, figsize=(18, 6))
plt.subplot(1, 3, 1), plt.imshow(img)
# Blur image
img = cv2.blur(img, (5, 5))
# Iterate tiles, and count unique colors inside
# https://stackoverflow.com/a/56606457/11089932
wh_t = wh // 8
count_unique_colors = np.zeros((8, 8))
for x in np.arange(8):
for y in np.arange(8):
tile = img[y*wh_t:(y+1)*wh_t, x*wh_t:(x+1)*wh_t]
tile = tile[3:-3, 3:-3]
count_unique_colors[y, x] = np.unique(tile.reshape(-1, tile.shape[-1]), axis=0).shape[0]
# Mask empty squares using cutoff from Otsu's method
val = threshold_otsu(count_unique_colors)
mask = count_unique_colors < val
# Some more visualization output
for x in np.arange(8):
for y in np.arange(8):
if mask[y, x]:
cv2.rectangle(out, (x*wh_t+3, y*wh_t+3),
((x+1)*wh_t-3, (y+1)*wh_t-3), (0, 255, 0), 2)
plt.subplot(1, 3, 2), plt.imshow(count_unique_colors, cmap='gray')
plt.subplot(1, 3, 3), plt.imshow(out)
plt.tight_layout(), plt.show()
And, that'd be the output:
As you can see, it's not perfect. One issue is the camera position, specifically that angle, but you already mentioned in the comments, that you can correct that. The other issue, as also already discussed in the comments, is the fact, that some pieces are placed between two squares. It's up to you, how to handle that. (I'd simply place the pieces correctly.)
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.19041-SP0
Python: 3.9.1
PyCharm: 2021.1.1
Matplotlib: 3.4.2
NumPy: 1.20.3
OpenCV: 4.5.2
scikit-image: 0.18.1
----------------------------------------
Not like the given original image, but if you have a chessboard with pieces of colours which are not of the same colour as the chessboard (as discussed in the comments), then you can do something like this:
import cv2
import numpy
img = cv2.imread("Chesss.PNG") # read image using cv2
for x in range(0,img.shape[0] - 8, img.shape[0]//8):
for y in range(0,img.shape[1] - 8, img.shape[1]//8):
square = img[x:x+img.shape[0]//8, y:y+img.shape[1]//8, :] # creating 8*8 squares of image
avg_colour_per_row = numpy.average(square, axis=0)
avg_colour = numpy.array(list(map(int, numpy.average(avg_colour_per_row, axis=0))))//8 # finding average colour of the square
if list(avg_colour) == list(numpy.array([0, 0, 0])) or list(avg_colour) == list(numpy.array([31, 31, 31])): # if average colour of the squareis black or white, then print the coordinates of the square
print(x//(img.shape[0]//8), y//(img.shape[1]//8))
My example image (I do not have a chessboard right now, so I used a rendered image):
Output:
0 0
0 1
0 2
0 3
0 4
0 5
0 6
0 7
1 1
1 3
1 4
1 6
2 0
2 1
2 2
2 3
2 4
2 5
2 6
2 7
3 0
3 1
3 2
3 3
3 5
3 6
3 7
4 0
4 1
4 2
4 3
4 4
4 6
4 7
5 0
5 1
5 2
5 3
5 4
5 5
5 6
5 7
6 0
6 3
6 4
6 5
6 6
7 0
7 1
7 2
7 3
7 4
7 5
7 6
7 7
Note that I have divided the average colour vales by 8. This is because we will perceive [0, 0, 0] and [1, 1, 1] (and similarly) as black only.
You can find chessboard and even find it's pose like here. Then you'll able to estimate ellipse shape of piece base.
Find ellipses, using, for instance, this project.
Filter out trash ellipses using pose knowledge, and you'll get pieces positions. Then you can find free cells.

How to randomly generate scratch like lines with opencv automatically

I am trying to generate synthetic images for my deep learning model. I need to draw scratches on a black surface. I already have a little script that can generate random white scratch like lines but only horizontally. I need the scratches to also be vertically and curved. On top of that it would also be very helpfull if the thickness of the scratches would also be random so I have thick and thin scratches.
This is my code so far:
import cv2
import numpy as np
import random
height = 384
width = 384
blank_image = np.zeros((height, width, 3), np.uint8)
num_scratches= random.randint(0,5)
for _ in range(num_scratches):
row_random = random.randint(20,370)
blank_image[row_random:(row_random+1), row_random:(row_random+random.randint(25,75))] = (255,255,255)
cv2.imshow("synthetic", blank_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
This is one example result outcome:
How do I have to edit my script so I can get more diverse looking scratches?
The scratches should somehow look like this for example (Done with paint):
need the scratches to also be vertically
Your method might be adopted as follows
import numpy as np # cv2 read image into np.array
img = np.zeros((5,5),dtype='uint8') # same as loading 5 x 5 px black rectangle
img[1:4,2:3] = 255
print(img)
Output:
[[ 0 0 0 0 0]
[ 0 0 255 0 0]
[ 0 0 255 0 0]
[ 0 0 255 0 0]
[ 0 0 0 0 0]]
Explanation: I set all elements (pixel) which have y-cordinate between 1 (inclusive) and 4 (exclusive) and x-cordinate between 2 (inclusive) and 3 (exclusive).
Nonetheless cv2 provide function for drawing lines namely cv2.line which is more handy to use, it does accept img on which to work, start point, end point, color and thickness, docs give following example:
# Draw a diagonal blue line with thickness of 5 px
img = cv2.line(img,(0,0),(511,511),(255,0,0),5)
If you are working in grayscale use value rather than 3-tuple as color.

Pixels intensity values between two lines

I have created an alghoritm that detects the edges of an extruded colagen casing and draws a centerline between these edges on an image. Casing with a centerline.
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
img = cv2.imread("C:/Users/5.jpg", cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (1500, 1200))
#ROI
fromCenter = False
r = cv2.selectROI(img, fromCenter)
imCrop = img[int(r[1]):int(r[1]+r[3]), int(r[0]):int(r[0]+r[2])]
#Operations on an image
_,thresh = cv2.threshold(imCrop,100,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
kernel = np.ones((5,5),np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
blur = cv2.GaussianBlur(opening,(7,7),0)
edges = cv2.Canny(blur, 0,20)
#Edges localization, packing coords into a list
indices = np.where(edges != [0])
coordinates = list(zip(indices[1], indices[0]))
num = len(coordinates)
#Separating into top and bot edge
bot_cor = coordinates[:int(num/2)]
top_cor = coordinates[-int(num/2):]
#Converting to arrays, sorting
a, b = np.array(top_cor), np.array(bot_cor)
a, b = a[a[:,0].argsort()], b[b[:,0].argsort()]
#Edges approximation by a 5th degree polynomial
min_a_x, max_a_x = np.min(a[:,0]), np.max(a[:,0])
new_a_x = np.linspace(min_a_x, max_a_x, imCrop.shape[1])
a_coefs = np.polyfit(a[:,0],a[:,1], 5)
new_a_y = np.polyval(a_coefs, new_a_x)
min_b_x, max_b_x = np.min(b[:,0]), np.max(b[:,0])
new_b_x = np.linspace(min_b_x, max_b_x, imCrop.shape[1])
b_coefs = np.polyfit(b[:,0],b[:,1], 5)
new_b_y = np.polyval(b_coefs, new_b_x)
#Defining a centerline
midx = [np.average([new_a_x[i], new_b_x[i]], axis = 0) for i in range(imCrop.shape[1])]
midy = [np.average([new_a_y[i], new_b_y[i]], axis = 0) for i in range(imCrop.shape[1])]
plt.figure(figsize=(16,8))
plt.title('Cross section')
plt.xlabel('Length of the casing', fontsize=18)
plt.ylabel('Width of the casing', fontsize=18)
plt.plot(new_a_x, new_a_y,c='black')
plt.plot(new_b_x, new_b_y,c='black')
plt.plot(midx, midy, '-', c='blue')
plt.show()
#Converting coords type to a list (plotting purposes)
coords = list(zip(midx, midy))
points = list(np.int_(coords))
mask = np.zeros((imCrop.shape[:2]), np.uint8)
mask = edges
#Plotting
for point in points:
cv2.circle(mask, tuple(point), 1, (255,255,255), -1)
for point in points:
cv2.circle(imCrop, tuple(point), 1, (255,255,255), -1)
cv2.imshow('imCrop', imCrop)
cv2.imshow('mask', mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
Now I would like to sum up the intensities of each pixel in a region between top edge and a centerline (same thing for a region between centerline and a bottom edge).
Is there any way to limit the ROI to the region between the detected edges and split it into two regions based on the calculated centerline?
Or is there any way to access the pixels which are contained between the edge and a centerline based on theirs coordinates?
(It's my very first post here, sorry in advance for all the mistakes)
I wrote a somewhat naïve code to get masks for the upper and lower part. My code considers that the source image will be always like yours: with horizontal stripes.
After applying Canny I get this:
Then I run some loops through image array to fill unwanted areas of your image. This is done separately for upper and lower part, creating masks. The results are:
Then you can use this masks to sum only the elements you're interested in, using cv.sumElems.
import cv2 as cv
#open as grayscale image
src = cv.imread("colagen.png",cv.IMREAD_GRAYSCALE)
# apply canny and find contours
threshold = 100
canny_output = cv.Canny(src, threshold, threshold * 2)
# find mask for upper part
mask1 = canny_output.copy()
x, y = canny_output.shape
area = 0
for j in range(y):
area = 0
for i in range(x):
if area == 0:
if mask1[i][j] > 0:
area = 1
continue
else:
mask1[i][j] = 255
elif area == 1:
if mask1[i][j] > 0:
area = 2
else:
continue
else:
mask1[i][j] = 255
mask1 = cv.bitwise_not(mask1)
# find mask for lower part
mask2 = canny_output.copy()
x, y = canny_output.shape
area = 0
for j in range(y):
area = 0
for i in range(x):
if area == 0:
if mask2[-i][j] > 0:
area = 1
continue
else:
mask2[-i][j] = 255
elif area == 1:
if mask2[-i][j] > 0:
area = 2
else:
continue
else:
mask2[-i][j] = 255
mask2 = cv.bitwise_not(mask2)
# apply masks and calculate sum of elements in upper and lower part
sums = [0,0]
(sums[0],_,_,_) = cv.sumElems(cv.bitwise_and(src,mask1))
(sums[1],_,_,_) = cv.sumElems(cv.bitwise_and(src,mask2))
cv.imshow('src',src)
cv.imshow('canny',canny_output)
cv.imshow('mask1',mask1)
cv.imshow('mask2',mask2)
cv.imshow('masked1',cv.bitwise_and(src,mask1))
cv.imshow('masked2',cv.bitwise_and(src,mask2))
cv.waitKey()
Alternatives...
Probably there exist some function that fill the areas of the Canny result. I tried cv.fillPoly and cv.floodFill, but didn't manage to make them work easily... But maybe someone else can help you with that...
Edit
Found another way to get the masks with a cleaner code. Using numpy np.add.accumulate then np.clip, and then a modulo operation:
# first divide canny_output by 255 to get 0's and 1's, then perform
# an accumulate addition for each column. Thus you'll get +1 for every
# line, "painting" areas with 1, 2, 3...
a = np.add.accumulate(canny_output/255,0)
# clip values: anything greater than 2 becomes 2
a = np.clip(a, 0, 2)
# performe a modulo, to get areas alternating with 0 or 1; then multiply by 255
a = a%2 * 255
# convert to uint8
mask1 = cv.convertScaleAbs(a)
# to get mask2 (the lower mask) flip the array then do the same as above
a = np.add.accumulate(np.flip(canny_output,0)/255,0)
a = np.clip(a, 0, 2)
a = a%2 * 255
mask2 = cv.convertScaleAbs(np.flip(a,0))
This returns almost the same result. The border of the mask is a little bit different...

How to measure the size of the room based off the color

So this code is able to segment a variety of rooms it identifies into different colors as seen below. The question is, how do i obtain the area of the rooms that are colored (Like those blue rooms). Rooms are in 1m:150m ratio.
The first image is the output i need to measure, the second room is the image i used to run the code with, the third image is an original image for reference. Thanks in advance.
import numpy as np
def find_rooms(img, noise_reduction=10, corners_threshold=0.0000001,
room_close=2, gap_in_wall_threshold=0.000001):
# :param img: grey scale image of rooms, already eroded and doors removed etc.
# :param noise_reduction: Amount of noise removed.
# :param corners_threshold: Corners to retained, higher value = more of house removed.
# :param room_close: Maximum line length to add to close off open doors.
# :param gap_in_wall_threshold: Minimum number of pixels to identify component as room instead of hole in the wall.
# :return: rooms: list of numpy arrays containing boolean masks for each detected room
# colored_house: Give room a color.
assert 0 <= corners_threshold <= 1
# Remove noise left from door removal
img[img < 128] = 0
img[img > 128] = 255
contours, _ = cv2.findContours(~img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask = np.zeros_like(img)
for contour in contours:
area = cv2.contourArea(contour)
if area > noise_reduction:
cv2.fillPoly(mask, [contour], 255)
img = ~mask
# Detect corners (you can play with the parameters here)
#harris corner detection
dst = cv2.cornerHarris(img, 4,3,0.000001)
dst = cv2.dilate(dst,None)
corners = dst > corners_threshold * dst.max()
# Draw lines to close the rooms off by adding a line between corners on the same x or y coordinate
# This gets some false positives.
# Can try disallowing drawing through other existing lines, need to test.
for y,row in enumerate(corners):
x_same_y = np.argwhere(row)
for x1, x2 in zip(x_same_y[:-1], x_same_y[1:]):
if x2[0] - x1[0] < room_close:
color = 0
cv2.line(img, (x1, y), (x2, y), color, 1)
for x,col in enumerate(corners.T):
y_same_x = np.argwhere(col)
for y1, y2 in zip(y_same_x[:-1], y_same_x[1:]):
if y2[0] - y1[0] < room_close:
color = 0
cv2.line(img, (x, y1), (x, y2), color, 1)
# Mark the outside of the house as black
contours, _ = cv2.findContours(~img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contour_sizes = [(cv2.contourArea(contour), contour) for contour in contours]
biggest_contour = max(contour_sizes, key=lambda x: x[0])[1]
mask = np.zeros_like(mask)
cv2.fillPoly(mask, [biggest_contour], 255)
img[mask == 0] = 0
# Find the connected components in the house
ret, labels = cv2.connectedComponents(img)
img = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB)
unique = np.unique(labels)
rooms = []
for label in unique:
component = labels == label
if img[component].sum() == 0 or np.count_nonzero(component) < gap_in_wall_threshold:
color = 0
else:
rooms.append(component)
color = np.random.randint(0, 255, size=3)
img[component] = color
return rooms, img
#Read gray image
img = cv2.imread('output16.png', 0)
rooms, colored_house = find_rooms(img.copy())
cv2.imshow('result', colored_house)
cv2.waitKey()
cv2.destroyAllWindows()
Ok so let's say that you read the segmented picture using OpenCV:
import cv2
import numpy as np
# reading the segmented picture in coloured mode
image = cv2.imread("path/to/segmented/coloured/picture.jpg", cv2.IMREAD_COLOR)
Now, suppose that you know the size in squared meters of the entire picture, so if for instance the picture reflects a total of 150m x 70m, you have a total size of 150x70 = 10500m². Let's declare this as a variable:
total_size = 10500
You also want to know the total number of pixels in the picture. If, for instance, your picture is 750*350 pixels, you have: 262500 pixels. You can just do that with:
total_number_of_pixels = image.shape[0]*image.shape[1]
Now, as I said in a comment, you also want to know the number of pixels for each unique colour in the segmented picture, which you can do using:
# count all occurrences of unique colours in your picture
unique, counts = np.unique(image.reshape(-1, image.shape[2]), axis=0, return_counts=True)
coloured_pixel_counts = sorted(zip(unique, counts), key=lambda x: x[1]))
Now, all you have left to do is just a cross-multiplication, which can be done with something like this:
rooms = []
for colour, pixel_count in coloured_pixel_counts:
rooms.append((colour, (pixel_count/total_number_of_pixels)*total_size))
You should now have a list of all colours and the respective approximated size in squared meters of the rooms of this colour.
Now, please note that, however, you would probably have to subset this list to the colours that strike your interest, as some colours seem to not really be linked to a room in your segmented pictures...
Again, please ask if anything is unclear!
So the measurement will be based on pixels, and you will need to know the maximum and minimum range of the RGB value of the color you want to "measure". I ran this code on your image to find the percentage of the green colored area to the whole area of the house and I got the following result:
The number of filtered pixels is: 331213 Which counts for %5 of the house
import cv2
import numpy as np
import math
img = cv2.imread('22I7X.png')
#Defining wanted color range
filteredColorMin = np.array([36,0,0], np.uint8) #Min range
filteredColorMax = np.array([70, 255,255], np.uint8) #High range
#Find all the pixels in the wanted color range
dst = cv2.inRange(img, filteredColorMin, filteredColorMax)
#count non-zero values from filtered range
numFilteredColor = cv2.countNonZero(dst)
#Getting total number of pixels in image to get the percentage of the filtered pixels from the total pixels
numTotalPixels=img.shape[0] *img.shape[1]
print('The number of filtered pixels is: ' + str(numFilteredColor) + " Which counts for %" + str(math.ceil((numFilteredColor/numTotalPixels)*100)) + " of the house")
cv2.imshow("original image",img)
cv2.waitKey(0)

Categories

Resources