Related
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)
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.
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.
I need to return the (average) contrast value of an image.
Is this possible and if so, please show how?
More than just the code, (obviously the code is welcome), in what color space should I work? Is HSV appropriate or best? Can the contrast value of a single pixel be computed?
Contrast can be computed from any intensity (I) like channel such as L in LAB, or I in HSI or V in HSV or Y in YCbCr (or even a grayscale version of the image from desaturation) using the max and min values either globally or an average of some region surrounding every pixel. LAB colorspace is often used, but I do not know if there is any general consensus about which would be "best".
One formula is:
contrast=(Imax - Imin)/(Imax + Imin)
See here
1) Convert the image to say LAB and get the L channel
2) Compute the max for an NxN neighborhood around each pixel
3) Compute the min for an NxN neighborhood around each pixel
4) Compute the contrast from the equation above at each pixel.
5) Compute the average for all pixels in the result of step 4)
where N is some small integer such as 5 or 7, for example.
Using ImageMagick (unix syntax), this would be:
magick zelda1.jpg -colorspace LAB -channel 0 -separate +channel \
\( -clone 0 -statistic maximum 5x5 \) \
\( -clone 0 -statistic minimum 5x5 \) \
\( -clone 1 -clone 2 +swap -define compose:clamp=off -compose minus -composite \) \
\( -clone 1 -clone 2 +swap -define compose:clamp=off -compose plus -composite \) \
-delete 0-2 \
+swap -define compose:clamp=off -compose divide -composite \
-scale 1x1! -format "%[fx:100*u]\%" info:
17.4745%
The contrast for 1 pixel would actually be the formula above for a 3x3 neighborhood around the given pixel. The neighborhood could contain all 8 surrounding pixels or just the top, bottom, left, right pixels around the give pixel.
A single pixel by itself cannot have a contrast. Contrast is a relative (difference) concept, so at least between two pixels.
Note that erode and dilate for an NxN structure element are equivalent to minimum and maximum.
Here is code in OpenCV:
#!/bin/python3.7
import cv2
import numpy as np
# read image
img = cv2.imread("zelda1.jpg")
# convert to LAB color space
lab = cv2.cvtColor(img,cv2.COLOR_BGR2LAB)
# separate channels
L,A,B=cv2.split(lab)
# compute minimum and maximum in 5x5 region using erode and dilate
kernel = np.ones((5,5),np.uint8)
min = cv2.erode(L,kernel,iterations = 1)
max = cv2.dilate(L,kernel,iterations = 1)
# convert min and max to floats
min = min.astype(np.float64)
max = max.astype(np.float64)
# compute local contrast
contrast = (max-min)/(max+min)
# get average across whole image
average_contrast = 100*np.mean(contrast)
print(str(average_contrast)+"%")
17.481959221048086%
Contrast is usually understood as intensity contrast and can be computed on the Luminance component (Y). It is a measure of spread of the histogram, such as the standard deviation.
I have an image like the following:
What I would like is to get the coordinates of the start and end point of each segment. Actually what I thought was to consider the fact that each extreme point should have just one point belonging to the segment in its neighborhood, while all other point should have at least 2. Unfortunately the line does not have thickness equal to one pixel so this reasoning does not hold.
Here's a fairly simple way to do it:
load the image and discard the superfluous alpha channel
skeletonise
filter looking for 3x3 neighbourhoods that have the central pixel set and just one other
#!/usr/bin/env python3
import numpy as np
from PIL import Image
from scipy.ndimage import generic_filter
from skimage.morphology import medial_axis
# Line ends filter
def lineEnds(P):
"""Central pixel and just one other must be set to be a line end"""
return 255 * ((P[4]==255) and np.sum(P)==510)
# Open image and make into Numpy array
im = Image.open('lines.png').convert('L')
im = np.array(im)
# Skeletonize
skel = (medial_axis(im)*255).astype(np.uint8)
# Find line ends
result = generic_filter(skel, lineEnds, (3, 3))
# Save result
Image.fromarray(result).save('result.png')
Note that you can obtain exactly the same result, for far less effort, with ImageMagick from the command-line like this:
convert lines.png -alpha off -morphology HMT LineEnds result.png
Or, if you want them as numbers rather than an image:
convert result.png txt: | grep "gray(255)"
Sample Output
134,78: (65535) #FFFFFF gray(255) <--- line end at coordinates 134,78
106,106: (65535) #FFFFFF gray(255) <--- line end at coordinates 106,106
116,139: (65535) #FFFFFF gray(255) <--- line end at coordinates 116,139
196,140: (65535) #FFFFFF gray(255) <--- line end at coordinates 196,140
Another way of doing it is to use scipy.ndimage.morphology.binary_hit_or_miss and set up your "Hits" as the white pixels in the below image and your "Misses" as the black pixels:
The diagram is from Anthony Thyssen's excellent material here.
In a similar vein to the above, you could equally use the "Hits" and "Misses" kernels above with OpenCV as described here:
morphologyEx(input_image, output_image, MORPH_HITMISS, kernel);
I suspect this would be the fastest method.
Keywords: Python, image, image processing, line ends, line-ends, morphology, Hit or Miss, HMT, ImageMagick, filter.
The method you mentioned should work well, you just need to do a morphological operation before to reduce the width of the lines to one pixel. You can use scikit-image for that:
from skimage.morphology import medial_axis
import cv2
# read the lines image
img = cv2.imread('/tmp/tPVCc.png', 0)
# get the skeleton
skel = medial_axis(img)
# skel is a boolean matrix, multiply by 255 to get a black and white image
cv2.imwrite('/tmp/res.png', skel*255)
See this page on the skeletonization methods in skimage.
I would tackle this with watershed-style algorithm. I described method below, however it is created to deal only with single (multisegment) line, so you would need to split your image into images of separate lines.
Toy example:
0000000
0111110
0111110
0110000
0110000
0000000
Where 0 denotes black and 1 denotes white.
Now my implemention of solution:
import numpy as np
img = np.array([[0,0,0,0,0,0,0],
[0,255,255,255,255,255,0],
[0,255,255,255,255,255,0],
[0,255,255,0,0,0,0],
[0,0,0,0,0,0,0]],dtype='uint8')
def flood(arr,value):
flooded = arr.copy()
for y in range(1,arr.shape[0]-1):
for x in range(1,arr.shape[1]-1):
if arr[y][x]==255:
if arr[y-1][x]==value:
flooded[y][x] = value
elif arr[y+1][x]==value:
flooded[y][x] = value
elif arr[y][x-1]==value:
flooded[y][x] = value
elif arr[y][x+1]==value:
flooded[y][x] = value
return flooded
ends = np.zeros(img.shape,dtype='uint64')
for y in range(1,img.shape[0]-1):
for x in range(1,img.shape[1]-1):
if img[y][x]==255:
temp = img.copy()
temp[y][x] = 127
count = 0
while 255 in temp:
temp = flood(temp,127)
count += 1
ends[y][x] = count
print(ends)
Output:
[[0 0 0 0 0 0 0]
[0 5 4 4 5 6 0]
[0 5 4 3 4 5 0]
[0 6 5 0 0 0 0]
[0 0 0 0 0 0 0]]
Now ends are denoted by positions of maximal values in above array (6 in this case).
Explanation: I am examing all white pixels as possible ends. For each such pixel I am "flooding" image - I place special value (127 - different than 0 and different than 255) and then propogate it - in every step all 255 which are neighbors (in von Neumann's sense) of special value become special values themselves. I am counting steps required to remove all 255. Because if you start (constant velocity) flooding from end it would take more time than if you have source in any other location, then maximal times of flooding are ends of your line.
I must admit that I did not tested this deeply, so I can't guarantee correct working in special case, like for example in case of self-intersecting line. I am also aware of roughness of my solution especially in area of detecting neighbors and propagation of special values, so feel free to improve it. I assumed that all border pixels are black (no line is touching "frame" of your image).