I am trying to write computer vision code based on the OpenCV library in Python to detect horizontal lines with intensity close to background. See example of the image below.
I have tried 2 approaches. The first one is based on Canny edge detection and Hough transform, but it detected only a few lines (see code and image below).
import math
import numpy as np
import cv2
scaleFactor = 1
maskX1 = 57
maskX2 = 263
maskY1 = 30
maskY2 = 164
angleStart = -1
angleEnd = 1
verticalKernel = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])
sharpenKernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
def applyKernel(image, kernel):
return cv2.filter2D(image, -1, kernel)
# read image
image_c = cv2.imread('images/1.png')
image_c = cv2.resize(image_c, None, fx=scaleFactor, fy=scaleFactor, interpolation=cv2.INTER_CUBIC)
cv2.imshow('Original Image', image_c)
# convert to grayscale
image_g = cv2.cvtColor(image_c, cv2.COLOR_RGB2GRAY)
# image_g = cv2.bilateralFilter(image_g, 15, 15, 15)
image_g = applyKernel(image_g, sharpenKernel)
cv2.imshow('Sharpen Image', image_g)
image_g = applyKernel(image_g, verticalKernel)
cv2.imshow('Vertical Sobel Operator', image_g)
# Gaussian blur and Canny
threshold_low = 250
threshold_high = 300
image_canny = cv2.Canny(image_g, threshold_low, threshold_high)
cv2.imshow('Canny Image', image_canny)
# Visualize region of interest
mask = np.zeros_like(image_g)
vertices = np.array([[(maskX1 * scaleFactor, maskY1 * scaleFactor), (maskX2 * scaleFactor, maskY1 * scaleFactor), (maskX2 * scaleFactor, maskY2 * scaleFactor), (maskX1 * scaleFactor, maskY2 * scaleFactor)]], dtype=np.int32)
cv2.fillPoly(mask, vertices, 255)
masked_image = cv2.bitwise_and(image_canny, mask)
# masked_image = image_canny
cv2.imshow('Region of interest', masked_image)
rho = 1 * scaleFactor # distance resolution in pixels
theta = np.pi / 180 # angular resolution in radians
threshold = 3 # minimum number of votes
min_line_len = 10 * scaleFactor # minimum number of pixels making up a line
max_line_gap = 20 * scaleFactor # maximum gap in pixels between connectable line segments
lines = cv2.HoughLinesP(masked_image, rho, theta, threshold, np.array([]), minLineLength=min_line_len,
maxLineGap=max_line_gap)
line_image = np.zeros((masked_image.shape[0], masked_image.shape[1], 3), dtype=np.uint8)
numLines = 0
totalLineLength = 0
for line in lines:
for x1, y1, x2, y2 in line:
if x2 == x1:
lineAngle = 90
else:
lineAngle = math.degrees(math.atan((y2 - y1) / (x2 - x1)))
if angleStart < lineAngle < angleEnd:
cv2.line(line_image, (x1, y1), (x2, y2), [0, 0, 255], 2)
numLines = numLines + 1
totalLineLength = totalLineLength + math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
α = 1
β = 0.3
γ = 0
# Resultant weighted image is calculated as follows: original_img * α + img * β + γ
image_with_lines = cv2.addWeighted(image_c, α, line_image, β, γ)
cv2.imshow('Image with lines', image_with_lines)
cv2.waitKey()
cv2.destroyAllWindows()
The second approach was based on image thresholding and contour analysis, but the results were also disappointing (see code and image below).
import math
import numpy as np
import cv2
scaleFactor = 1
maskX1 = 57
maskX2 = 263
maskY1 = 30
maskY2 = 164
angleStart = -5
angleEnd = 5
verticalKernel = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])
sharpenKernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
basePath = 'images/'
fileExtension = '.png'
def applyKernel(image, kernel):
return cv2.filter2D(image, -1, kernel)
def getHoughLines(image, masked_image):
rho = 1 * scaleFactor # distance resolution in pixels
theta = np.pi / 180 # angular resolution in radians
threshold = 3 # minimum number of votes
min_line_len = 10 * scaleFactor # minimum number of pixels making up a line
max_line_gap = 5 * scaleFactor # maximum gap in pixels between connectable line segments
lines = cv2.HoughLinesP(masked_image, rho, theta, threshold, np.array([]), minLineLength=min_line_len,
maxLineGap=max_line_gap)
line_image = np.zeros((masked_image.shape[0], masked_image.shape[1]), dtype=np.uint8)
numLines = 0
totalLineLength = 0
for line in lines:
for x1, y1, x2, y2 in line:
if x2 == x1:
lineAngle = 90
else:
lineAngle = math.degrees(math.atan((y2 - y1) / (x2 - x1)))
if angleStart < lineAngle < angleEnd:
cv2.line(line_image, (x1, y1), (x2, y2), 255, 2)
numLines = numLines + 1
totalLineLength = totalLineLength + math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
α = 1
β = 0.3
γ = 0
# Resultant weighted image is calculated as follows: original_img * α + img * β + γ
image_with_lines = cv2.addWeighted(image, α, line_image, β, γ)
cv2.imshow('Image with lines', image_with_lines)
return image_with_lines
# read image
image_g = cv2.imread('images/1.png', cv2.IMREAD_GRAYSCALE)
# image_g = cv2.resize(image_g, None, fx=scaleFactor, fy=scaleFactor, interpolation=cv2.INTER_CUBIC)
cv2.imshow('Original Image', image_g)
# Apply Gaussian blur to reduce noise
# image_blurred = cv2.GaussianBlur(image_g, (5, 5), 0)
image_blurred = image_g
image_blurred = applyKernel(image_blurred, sharpenKernel)
cv2.imshow('Sharpen Image', image_blurred)
image_blurred = applyKernel(image_blurred, verticalKernel)
cv2.imshow('Vertical Sobel Operator', image_blurred)
# Apply adaptive thresholding to binarize the image
# _, binary_image = cv2.threshold(image_blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
_, binary_image = cv2.threshold(image_blurred, 70, 255, cv2.THRESH_BINARY)
cv2.imshow('Binary image', binary_image)
# Visualize region of interest
mask = np.zeros_like(image_g)
vertices = np.array([[(maskX1 * scaleFactor, maskY1 * scaleFactor), (maskX2 * scaleFactor, maskY1 * scaleFactor), (maskX2 * scaleFactor, maskY2 * scaleFactor), (maskX1 * scaleFactor, maskY2 * scaleFactor)]], dtype=np.int32)
cv2.fillPoly(mask, vertices, 255)
masked_image = cv2.bitwise_and(binary_image, mask)
cv2.imshow('Masked image', masked_image)
# morphological operations
# kernel = np.ones((2,2),np.uint8)
# masked_image = cv2.morphologyEx(masked_image, cv2.MORPH_OPEN, kernel)
cv2.imshow('morphologyEx', masked_image)
# Perform edge detection
edges = cv2.Canny(masked_image, 30, 200)
cv2.imshow('Edges', edges)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(contours)
for contour in contours:
# Calculate the length of the contour
length = cv2.arcLength(contour, True)
# Calculate the area of the contour
area = cv2.contourArea(contour)
# Filter out small contours (adjust the area threshold as needed)
if area > 1:
x, y, width, height = cv2.boundingRect(contour)
if width > 5:
# Draw the contour on the original image
cv2.drawContours(image_g, [contour], -1, (0, 255, 0), 2)
# Print or store the length and area
print(f"Length: {length}, Area: {area}")
cv2.imshow('Processed Image', image_g)
cv2.waitKey(0)
cv2.destroyAllWindows()
Is there a way to detect these lines more accurately?
I came up with the following solution based on image thresholding and contours detection. Combination of 2 additional filters gave more visible lines which was easier to detect using thresholding and contours detection.
def getStreakyStructuresForOneImage(imagePath, showProcessingImages, filterVertical = False):
scaleFactor = 1
maskX1 = 128 # 57
maskX2 = 355 # 263
maskY1 = 50
maskY2 = 205
angleStart = -5
angleEnd = 5
verticalKernel = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])
sharpenKernel2 = 0.64 * np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
sharpenKernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
if filterVertical:
verticalKernel = np.transpose(verticalKernel)
sharpenKernel2 = np.transpose(sharpenKernel2)
sharpenKernel = np.transpose(sharpenKernel)
# read image
image_g = cv2.imread(imagePath, cv2.IMREAD_GRAYSCALE)
if showProcessingImages:
cv2.imshow('Original Image', image_g)
image_blurred = applyKernel(image_g, sharpenKernel)
if showProcessingImages:
cv2.imshow('Sharpen Image', image_blurred)
image_blurred = applyKernel(image_blurred, verticalKernel)
image_blurred = applyKernel(image_blurred, sharpenKernel2)
if showProcessingImages:
cv2.imshow('Sharpening using different kernels', image_blurred)
_, binary_image = cv2.threshold(image_blurred, 70, 255, cv2.THRESH_BINARY)
if showProcessingImages:
cv2.imshow('Binary image', binary_image)
# Visualize region of interest
mask = np.zeros_like(image_g)
vertices = np.array([[(maskX1 * scaleFactor, maskY1 * scaleFactor), (maskX2 * scaleFactor, maskY1 * scaleFactor),
(maskX2 * scaleFactor, maskY2 * scaleFactor), (maskX1 * scaleFactor, maskY2 * scaleFactor)]],
dtype=np.int32)
cv2.fillPoly(mask, vertices, 255)
masked_image = cv2.bitwise_and(binary_image, mask)
if showProcessingImages:
cv2.imshow('Masked image', masked_image)
contours, _ = cv2.findContours(masked_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
image_g = cv2.cvtColor(image_g, cv2.COLOR_GRAY2RGB)
masked_image = cv2.cvtColor(masked_image, cv2.COLOR_GRAY2RGB)
line_image = np.zeros((masked_image.shape[0], masked_image.shape[1], 3), dtype=np.uint8)
numberOfMeaningfulContours = 0
totalLineLength = 0
for contour in contours:
# Calculate the length of the contour
length = cv2.arcLength(contour, True)
# Calculate the area of the contour
area = cv2.contourArea(contour)
# Filter out small contours (adjust the area threshold as needed)
if area > 0:
x, y, width, height = cv2.boundingRect(contour)
if filterVertical:
if height > 50:
# Draw the contour on the original image
cv2.drawContours(line_image, [contour], -1, (0, 0, 255), 2)
numberOfMeaningfulContours += 1;
totalLineLength += width * mmInPx;
# Print or store the length and area
print(f"Length: {length}, Area: {area}")
else:
if width > 15:
# Draw the contour on the original image
cv2.drawContours(line_image, [contour], -1, (0, 0, 255), 2)
numberOfMeaningfulContours += 1;
totalLineLength += width * mmInPx;
# Print or store the length and area
print(f"Length: {length}, Area: {area}")
α = 1
β = 0.14
γ = 0
# Resultant weighted image is calculated as follows: original_img * α + img * β + γ
image_with_lines = cv2.addWeighted(image_g, α, line_image, β, γ)
if showProcessingImages:
cv2.imshow('Processed Image', image_with_lines)
averageLineLength = 0
if numberOfMeaningfulContours > 0:
averageLineLength = totalLineLength / numberOfMeaningfulContours
return image_with_lines, numberOfMeaningfulContours, totalLineLength, averageLineLength