Question: 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?

Answer: 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