File size: 3,089 Bytes
70ebc90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import cv2
import numpy as np
import os

def order_points(pts):
    """Order contour points as TL, TR, BR, BL consistently."""
    rect = np.zeros((4, 2), dtype="float32")
    s = pts.sum(axis=1)
    diff = np.diff(pts, axis=1)
    rect[0] = pts[np.argmin(s)]     # top-left
    rect[2] = pts[np.argmax(s)]     # bottom-right
    rect[1] = pts[np.argmin(diff)]  # top-right
    rect[3] = pts[np.argmax(diff)]  # bottom-left
    return rect

def detect_reference(image_path, ref_size_mm=20.0, save_path=None):
    """
    Detects a near-square reference object in the image and calculates px/mm.

    Args:
        image_path (str): Path to the input image.
        ref_size_mm (float): Real-world size of reference square side in mm.
        save_path (str): Folder to save annotated image. If None, no image saved.

    Returns:
        tuple: (status, px_per_mm, ref_square_points)
               status = 'success' or 'failed'
    """
    img = cv2.imread(image_path)
    if img is None:
        return "failed", None, None

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    # Adaptive threshold (better for uneven lighting)
    thresh = cv2.adaptiveThreshold(
        blur, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY_INV,
        51, 10
    )

    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    ref_square = None
    px_per_mm = None

    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area < 500:  # skip noise
            continue

        epsilon = 0.02 * cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)

        if len(approx) == 4:  # quadrilateral
            box = approx.reshape(-1, 2)
            (tl, tr, br, bl) = order_points(box)
            width = np.linalg.norm(tr - tl)
            height = np.linalg.norm(bl - tl)
            aspect_ratio = min(width, height) / max(width, height)

            if aspect_ratio > 0.9:  # near-square
                ref_square = np.array([tl, tr, br, bl], dtype=np.int32)
                side_px = (width + height) / 2.0
                px_per_mm = side_px / ref_size_mm
                break

    if ref_square is not None:
        cv2.drawContours(img, [ref_square], -1, (0, 255, 0), 3)
        cx = int(np.mean(ref_square[:, 0]))
        cy = int(np.mean(ref_square[:, 1]))
        cv2.putText(img, f"Ref {ref_size_mm}mm", (cx - 60, cy - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

        if save_path is not None:
            os.makedirs(save_path, exist_ok=True)
            out_file = os.path.join(save_path, os.path.basename(image_path))
            cv2.imwrite(out_file, img)

        return "success", px_per_mm, ref_square
    else:
        return "failed", None, None

# ---------------- Test run ----------------
if __name__ == "__main__":
    status, px_per_mm, pts = detect_reference(
        r"Results\Detection\capture_1757686922_0_annotated.jpg",
        ref_size_mm=20.0,
        save_path=r"Results\Reference"
    )