cv2.findContours fails to detect black rectangle aligned with x-axis

Hi everyone

I’m working with OpenCV in Python to detect a large black rectangle in an image. Everything works fine when the rectangle is rotated at an angle, but when it’s aligned exactly with the x-axis (i.e., perfectly horizontal), it doesn’t get detected as a contour at all. As a result, cv2.minAreaRect() never even runs because there’s no contour to process.

I’ve tried converting the image to grayscale and applying both thresholding and edge detection with cv2.Canny(). Visually, the rectangle is clearly present and distinct from the background. I’m also using cv2.findContours() with the RETR_EXTERNAL and CHAIN_APPROX_SIMPLE flags.

This issue only occurs when the rectangle is perfectly horizontal. If I rotate the same shape slightly, it gets detected correctly. I suspect it might have something to do with how OpenCV approximates contours or handles horizontal/vertical edges.

Has anyone run into this before? I’d really appreciate any insights into why this happens and how to make findContours more reliable for this case. Could it be related to resolution, anti-aliasing, or internal filtering?

Thanks in advance for any help — I shared a code snippet if that helps!

def detect_tunnel(image, aruco, binary_matrix, grid_size=5):
    # Convert image to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # Adaptive thresholding
    thresh = cv2.adaptiveThreshold(
        gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 5
    )

    # Morphological operations to remove noise
    kernel = np.ones((5, 5), np.uint8)
    cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

    # Find contours of the cleaned image
    contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Filter out small contours based on a minimum area
    min_area = 10000  # Adjust based on image size
    large_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > min_area]
    length = len(large_contours)
    print('length',length)
    for cnt in large_contours:

        image_copy=image.copy()
        cv2.drawContours(image_copy, cnt, 0, (0, 255, 0), 3)
        cv2.imshow("Tunnel large", image_copy)
        cv2.waitKey(0)
        cv2.destroyAllWindows() 

    #more than one contour detected
    if length != 0: 
        # Initialize variables for tunnel detection
        tunnel = None

        tunnel_cand=[]
        # Search for the best candidate rectangle representing the tunnel
        for contour in large_contours:
            rect = cv2.minAreaRect(contour)  # Gets a rotated rectangle        
            center, (width, height), angle = rect       
            area = width * height
            aspect_ratio = max(width, height) / min(width, height)
            
            #test which rectangles are captured
            box = cv2.boxPoints(rect)
            box = np.intp(box)
            image_copy1 = image.copy()
            cv2.drawContours(image_copy1, [box], 0, (0, 255, 0), 3)
            cv2.imshow("Tunnel contour", image_copy1)
            cv2.waitKey(0)
            cv2.destroyAllWindows() 
            
            #filter the contours for black pixels
            # Create a mask for the rotated rectangle
            mask = np.zeros(binary_matrix.shape, dtype=np.uint8)
            cv2.fillPoly(mask, [box // grid_size], 1)

            # Calculate black pixel ratio inside the mask
            masked_black = binary_matrix[mask == 1]
            if masked_black.size == 0:
                return False
            black_ratio = np.sum(masked_black == 1) / masked_black.size
            print('black_ratio',black_ratio)
            if black_ratio > 0.6:
                tunnel_cand.append(contour)
                print('ik heb iter')
                image_copy2=image.copy()
                cv2.drawContours(image_copy2, [box], 0, (0, 255, 0), 3)
                cv2.imshow("Tunnel zwart", image_copy2)
                cv2.waitKey(0)
                cv2.destroyAllWindows()

You should reduce your code into just a couple of lines that test findCountours directly from what it returns, and not after lots of further processing, which, as far as we know, might as well - or actually more probably - cause the issue

black = 0 = false = background. if you want to grasp that rectangle, it has to be foreground, which is the opposite.

threshold the image to obtain a mask (binary image), using appropriate flags (THRESH_BINARY_INV) to the threshold() call, such that the rectangle is foreground.

Canny is a newbie trap. avoid it. show the source image. then we can tell you if Canny is warranted.

crosspost:

https://stackoverflow.com/questions/79615444/cv2-findcontours-fails-to-detect-black-rectangle-aligned-with-x-axis

Thank you for your answers.
I directly plotted the contours that are detected and with a simple tunnel (black rectangle, first picture) contours() can’t detect it but when I apply the same code for a dubbel tunnel (second picture), that also stands parallel to the x-axis (horizontal), he can detect it (don’t mind the black spots). What is the raison for this?

def detect_tunnel(image, aruco, binary_matrix, grid_size=5):
# Convert image to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Adaptive thresholding
thresh = cv2.adaptiveThreshold(
    gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 5
)

# Morphological operations to remove noise
kernel = np.ones((5, 5), np.uint8)
cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# Find contours of the cleaned image
contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:

    image_copy=image.copy()
    cv2.drawContours(image_copy, [cnt], 0, (0, 255, 0), 3)
    cv2.imshow("contours", image_copy)
    cv2.waitKey(0)
    cv2.destroyAllWindows()