杰瑞科技汇

python approxpolydp

Of course! Let's dive deep into cv2.approxPolyDP, a very useful function in OpenCV for shape detection.

python approxpolydp-图1
(图片来源网络,侵删)

What is approxPolyDP?

approxPolyDP is an OpenCV function that approximates a polygonal curve(s) with another polygonal curve(s) with a specified precision.

In simpler terms, it takes a complex, potentially jagged shape (a contour) and replaces it with a simpler polygon that has fewer sides. The key is that the simpler polygon is "close enough" to the original shape.

The name comes from the Douglas-Peucker algorithm, which is the algorithm this function implements.


The Core Idea: Douglas-Peucker Algorithm

Imagine you have a coastline on a map. You want to simplify it for a smaller-scale map. You don't need every tiny bay and inlet, just the general shape.

python approxpolydp-图2
(图片来源网络,侵删)

The Douglas-Peucker algorithm works like this:

  1. Start with Extremes: Draw a straight line between the first and last points of your curve.
  2. Find the Farthest Point: Find the point in the curve that is the farthest away from this straight line.
  3. Check the Tolerance:
    • If this farthest distance is less than a given tolerance (or epsilon), then the line is a good enough approximation. You can discard all the intermediate points.
    • If the distance is greater than the tolerance, that point is important. You split the original curve into two smaller curves: one from the start to this farthest point, and another from this farthest point to the end.
  4. Recurse: Repeat the process for each of these new smaller curves until all important points are found.

The result is a simplified polygon that captures the essential shape of the original curve.


The Function Signature

cv2.approxPolyDP(curve, epsilon, closed[, approxCurve])

Parameters:

  • curve (Input): This is the input contour. It must be a 2D NumPy array of type int32 or float32, where each row is a point (x, y). You typically get this from cv2.findContours().
  • epsilon (Input): This is the most important parameter. It's the maximum distance between the original contour and the approximated polygon. It's a value between 0 and 1, representing a percentage of the original contour's perimeter.
    • A small epsilon will result in a polygon with many vertices, closely following the original shape.
    • A large epsilon will result in a polygon with very few vertices, creating a more abstract, simpler shape.
  • closed (Input): A boolean flag.
    • If True, the approximated curve is closed (i.e., the first and last points are connected).
    • If False, the curve is open.
    • For shape detection, you almost always want this to be True.
  • approxCurve (Output): This is an optional output parameter. It's a NumPy array that will store the vertices of the approximated polygon.

Return Value:

python approxpolydp-图3
(图片来源网络,侵删)

The function returns a NumPy array containing the vertices of the approximated polygon. Each vertex is a point (x, y).


Practical Example: Detecting Shapes

This is the most common use case for approxPolyDP. We'll find contours in an image of basic shapes and then classify them based on the number of vertices in their approximated polygon.

Step 1: Setup

You'll need an image with clear shapes. Let's create one using NumPy and OpenCV.

import cv2
import numpy as np
# Create a blank white image
image = np.ones((300, 400, 3), dtype="uint8") * 255
# Draw different shapes
# 1. Rectangle
cv2.rectangle(image, (50, 50), (150, 150), (0, 0, 0), 5)
# 2. Circle (approximated by many points)
cv2.circle(image, (300, 100), 50, (0, 0, 0), 5)
# 3. Triangle
pts = np.array([[250, 200], [300, 250], [200, 250]], np.int32)
cv2.fillPoly(image, [pts], (0, 0, 0))
# 4. Pentagon
pts = np.array([[50, 200], [100, 180], [120, 220], [80, 250], [20, 220]], np.int32)
cv2.fillPoly(image, [pts], (0, 0, 0))
# 5. Hexagon
pts = np.array([[250, 200], [300, 200], [325, 250], [300, 300], [250, 300], [225, 250]], np.int32)
cv2.fillPoly(image, [pts], (0, 0, 0))
# Show the image
cv2.imshow("Original Shapes", image)
cv2.waitKey(0)

Step 2: Preprocessing and Finding Contours

To find shapes, we need to convert the image to grayscale, apply a threshold, and then find the contours.

# Convert to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Apply a threshold to get a binary image
# (This will be black shapes on a white background)
_, thresh = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV)
# Find contours in the thresholded image
# cv2.RETR_TREE retrieves all contours and creates a hierarchy
# cv2.CHAIN_APPROX_SIMPLE compresses horizontal, vertical, and diagonal segments
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(f"Found {len(contours)} contours.")

Step 3: Approximate and Classify Each Contour

Now we loop through each contour, approximate it, and classify the shape.

# Loop over all the contours
for cnt in contours:
    # Calculate the perimeter of the contour
    peri = cv2.arcLength(cnt, True)
    # Approximate the contour to a polygon
    # Epsilon is a percentage of the perimeter. 2% is a good starting point.
    # A smaller epsilon will capture more detail, a larger one will simplify more.
    approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
    # Draw the approximated contour on the original image
    cv2.drawContours(image, [approx], -1, (0, 255, 0), 3)
    # Classify the shape based on the number of vertices
    vertices = len(approx)
    if vertices == 3:
        shape_name = "Triangle"
    elif vertices == 4:
        # To distinguish a square from a rectangle, we can check aspect ratio
        x, y, w, h = cv2.boundingRect(approx)
        aspect_ratio = float(w) / h
        if 0.95 <= aspect_ratio <= 1.05:
            shape_name = "Square"
        else:
            shape_name = "Rectangle"
    elif vertices == 5:
        shape_name = "Pentagon"
    elif vertices == 6:
        shape_name = "Hexagon"
    else:
        # For a circle, the number of vertices will be high
        shape_name = "Circle"
    # Get the center of the contour to place the text
    M = cv2.moments(cnt)
    if M["m00"] != 0:
        cX = int(M["m10"] / M["m00"])
        cY = int(M["m01"] / M["m00"])
    else:
        cX, cY = 0, 0
    # Put the shape name on the image
    cv2.putText(image, shape_name, (cX - 40, cY), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
# Show the final image with classifications
cv2.imshow("Shape Detection", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Expected Output:

You will see the original shapes with a green polygon drawn on top of them, and the name of the shape (e.g., "Rectangle", "Circle", "Triangle") written in the middle.


Key Takeaways and Tips

  1. epsilon is Crucial: The value of epsilon directly controls the level of simplification. You might need to experiment with it for your specific images. A common practice is to use a percentage of the contour's perimeter (02 * peri).
  2. Use with findContours: approxPolyDP is almost always used as a post-processing step after cv2.findContours.
  3. Classification: The number of vertices returned by approxPolyDP is the primary way to classify basic geometric shapes.
  4. Order of Points: The vertices in the output array approx are in the order they appear along the contour. This can be useful for other tasks, like calculating the area or finding a bounding box.
  5. closed=True: For closed shapes, always set this to True. If you set it to False, it will try to approximate an open polyline, which is rarely what you want for shape detection.
分享:
扫描分享到社交APP
上一篇
下一篇