I am new in image processing and I am trying to improve myself by doing some projects and I have a problem about my project. I have an image dataset containing lakes with their corresponding binary mask images. I want to calculate the perimeter (boundary length) and area of the lake in each image using OpenCV.
So far, I have tried Canny Edge Detection and cv2.findContours()
to detect the lake boundaries to find lake's real area and real perimeter, but I am struggling to get only the lake's contour without including unwanted edges (such as image borders or noise).
Here is my current code:
import cv2
import matplotlib.pyplot as plt
import numpy as np
# Load the image
image = cv2.imread("water_body_3.jpg")
# Apply Canny edge detection
edged = cv2.Canny(image, 50, 100)
# Find contours
contours, _ = cv2.findContours(edged, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
largest_contour = max(contours, key=cv2.contourArea)
# Draw the detected contours
cv2.drawContours(image, [largest_contour], -1, (0, 255, 0), 2)
plt.imshow(image, cmap="gray")
plt.title("Canny Edge Detection")
plt.axis("off")
plt.show()
Problems I'm Facing:
The detected contours sometimes include the image border or noise instead of just the lake.
I want to calculate the perimeter and area of the lake, but I am not sure if I am selecting the correct contour.
Expected Output:
Extract only the lake contour from the image.
Compute the perimeter and area .
Question:
How can I ensure that I am only selecting the lake's contour and not unwanted edges?
What is the best way to calculate the lake's area and perimeter
correctly?
Should I preprocess the image differently (e.g., thresholding,
morphological operations)?
Images:
code output image: https://i.sstatic.net/mLCvT5sD.png
mask image that I use in code (water_body_3.jpg): https://i.sstatic.net/psXSsJfg.jpg
original image:https://i.sstatic.net/4v6f4wLj.jpg
Take a look at my code : Its not only a problem of contour detection cause the contour was around your image. that's why before the detection i just draw a rectangle of 1 pixel to the image borders. And then the idea is to detect the biggest white area and making all white in this area to avoid some small noise and make a full black mask of the image then all is clean and it come easy to get what you want!
import cv2
import numpy as np
# Draw a black border of 1 pixel around the image
cv2.rectangle(image, (0, 0), (image.shape[1] - 1, image.shape[0] - 1), (0, 0, 0), thickness=1)
# Convert the image to a binary (black & white) format
# Any pixel value greater than 127 becomes 255 (white), others become 0 (black)
_, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
# Find the contours (external shapes) in the binary image
# RETR_EXTERNAL: Retrieves only the outermost contour
# CHAIN_APPROX_SIMPLE: Compresses horizontal, vertical, and diagonal segments to save memory
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Identify the largest contour based on its area
largest_contour = max(contours, key=cv2.contourArea)
# Calculate the area (in pixels) of the largest contour
area = cv2.contourArea(largest_contour)
# Calculate the perimeter (length of the boundary) of the largest contour
peri = cv2.arcLength(largest_contour, True) # 'True' means it's a closed shape
# Create text labels for area and perimeter, adding "px²" for area and "px" for perimeter
text1 = f"Area: {int(area)} px²"
text2 = f"Perimeter: {int(peri)} px"
# Create a blank mask (same size as the original image) filled with zeros (black)
mask = np.zeros_like(image)
# Fill the largest contour area with white (255) on the mask
cv2.drawContours(mask, [largest_contour], 0, 255, cv2.FILLED)
# Convert the grayscale mask to a BGR (color) image so we can draw in color
image_color = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
# Draw the largest contour filled with green (0, 255, 0) on the color image
cv2.drawContours(image_color, [largest_contour], -1, (0, 255, 0), cv2.FILLED)
# Draw the largest contour with red (0, 0, 255) on the color image
cv2.drawContours(image_color, [largest_contour], -1,(0, 0, 255), 1)
#From here its just the codes to display the info box
(x, y) = (10, 10)
(w, h) = (140, 55)
cv2.rectangle(image_color, (x, y), (x + w, y + h), (255, 255, 255), -1) # -1 fills the rectangle
cv2.putText(image_color, text1, (x + 10, y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 1)
cv2.putText(image_color, text2, (x + 10, y + 45), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 1)
cv2.imwrite("output.jpg", image_color)
cv2.imshow("Result Image", image_color)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result: