| import cv2 |
| import numpy as np |
| import os |
| import argparse |
| from typing import Union, Tuple |
| from matplotlib import pyplot as plt |
|
|
| class ScalingSquareDetector: |
| def __init__(self, feature_detector="ORB", debug=False): |
| """ |
| Initialize the detector with the desired feature matching algorithm. |
| :param feature_detector: "ORB" or "SIFT" (default is "ORB"). |
| :param debug: If True, saves intermediate images for debugging. |
| """ |
| self.feature_detector = feature_detector |
| self.debug = debug |
| self.detector = self._initialize_detector() |
|
|
| def _initialize_detector(self): |
| """ |
| Initialize the chosen feature detector. |
| :return: OpenCV detector object. |
| """ |
| if self.feature_detector.upper() == "SIFT": |
| return cv2.SIFT_create() |
| elif self.feature_detector.upper() == "ORB": |
| return cv2.ORB_create() |
| else: |
| raise ValueError("Invalid feature detector. Choose 'ORB' or 'SIFT'.") |
|
|
| def find_scaling_square( |
| self, target_image, known_size_mm, roi_margin=30 |
| ): |
| """ |
| Detect the scaling square in the target image based on the reference image. |
| :param target_image: Binary image containing the square. |
| :param known_size_mm: Physical size of the square in millimeters. |
| :param roi_margin: Margin to expand the ROI around the detected square (in pixels). |
| :return: Scaling factor (mm per pixel). |
| """ |
| contours, _ = cv2.findContours( |
| target_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE |
| ) |
|
|
| if not contours: |
| raise ValueError("No contours found in the target image.") |
|
|
| |
| print(f"No of contours: {len(contours)}") |
| largest_contour = max(contours, key=cv2.contourArea) |
|
|
| |
| target_image_color = cv2.cvtColor(target_image, cv2.COLOR_GRAY2BGR) |
| cv2.drawContours( |
| target_image_color, [largest_contour], -1, (255, 0, 0), 3 |
| ) |
|
|
| if self.debug: |
| cv2.imwrite("largest_contour.jpg", target_image_color) |
|
|
| |
| x, y, w, h = cv2.boundingRect(largest_contour) |
| square_width_px = w |
| square_height_px = h |
| |
| print(f"Reference object size: {known_size_mm} mm") |
| print(f"Detected width: {square_width_px} px") |
| print(f"Detected height: {square_height_px} px") |
|
|
| |
| avg_square_size_px = (square_width_px + square_height_px) / 2 |
| print(f"Average square size: {avg_square_size_px} px") |
| |
| scaling_factor = known_size_mm / avg_square_size_px |
| print(f"Calculated scaling factor: {scaling_factor:.6f} mm per pixel") |
|
|
| return scaling_factor |
|
|
| def draw_debug_images(self, output_folder): |
| """ |
| Save debug images if enabled. |
| :param output_folder: Directory to save debug images. |
| """ |
| if self.debug: |
| if not os.path.exists(output_folder): |
| os.makedirs(output_folder) |
| debug_images = ["largest_contour.jpg"] |
| for img_name in debug_images: |
| if os.path.exists(img_name): |
| os.rename(img_name, os.path.join(output_folder, img_name)) |
|
|
|
|
| def calculate_scaling_factor( |
| target_image, |
| reference_obj_size_mm, |
| feature_detector="ORB", |
| debug=False, |
| roi_margin=30, |
| ) -> float: |
| """ |
| Calculate scaling factor from reference object in image. |
| |
| :param target_image: Input image (numpy array) |
| :param reference_obj_size_mm: Known size of reference object in millimeters |
| :param feature_detector: Feature detector to use ("ORB" or "SIFT") |
| :param debug: Enable debug output |
| :param roi_margin: ROI margin in pixels |
| :return: Scaling factor in mm per pixel |
| """ |
| |
| detector = ScalingSquareDetector(feature_detector=feature_detector, debug=debug) |
|
|
| |
| scaling_factor = detector.find_scaling_square( |
| target_image=target_image, |
| known_size_mm=reference_obj_size_mm, |
| roi_margin=roi_margin, |
| ) |
|
|
| |
| if debug: |
| detector.draw_debug_images("debug_outputs") |
|
|
| return scaling_factor |
|
|
|
|
| def convert_units(value: float, from_unit: str, to_unit: str) -> float: |
| """ |
| Convert between mm and inches. |
| |
| :param value: Value to convert |
| :param from_unit: Source unit ("mm" or "inches") |
| :param to_unit: Target unit ("mm" or "inches") |
| :return: Converted value |
| """ |
| if from_unit == to_unit: |
| return value |
| |
| if from_unit == "inches" and to_unit == "mm": |
| return value * 25.4 |
| elif from_unit == "mm" and to_unit == "inches": |
| return value / 25.4 |
| else: |
| raise ValueError(f"Unsupported unit conversion: {from_unit} to {to_unit}") |
|
|
|
|
| def calculate_scaling_factor_with_units( |
| target_image, |
| reference_obj_size: float, |
| reference_unit: str = "mm", |
| output_unit: str = "mm", |
| feature_detector="ORB", |
| debug=False, |
| roi_margin=30, |
| ) -> Tuple[float, str]: |
| """ |
| Calculate scaling factor with proper unit handling. |
| |
| :param target_image: Input image (numpy array) |
| :param reference_obj_size: Known size of reference object |
| :param reference_unit: Unit of reference object size ("mm" or "inches") |
| :param output_unit: Desired unit for scaling factor ("mm" or "inches") |
| :param feature_detector: Feature detector to use ("ORB" or "SIFT") |
| :param debug: Enable debug output |
| :param roi_margin: ROI margin in pixels |
| :return: Tuple of (scaling_factor, unit_string) |
| """ |
| |
| reference_size_mm = convert_units(reference_obj_size, reference_unit, "mm") |
| |
| |
| scaling_factor_mm = calculate_scaling_factor( |
| target_image=target_image, |
| reference_obj_size_mm=reference_size_mm, |
| feature_detector=feature_detector, |
| debug=debug, |
| roi_margin=roi_margin, |
| ) |
| |
| |
| if output_unit == "inches": |
| scaling_factor = scaling_factor_mm / 25.4 |
| unit_string = "inches per pixel" |
| else: |
| scaling_factor = scaling_factor_mm |
| unit_string = "mm per pixel" |
| |
| print(f"Final scaling factor: {scaling_factor:.6f} {unit_string}") |
| |
| return scaling_factor, unit_string |
|
|
|
|
| |
| PAPER_SIZES = { |
| "A4": {"width_mm": 210, "height_mm": 297, "width_inches": 8.27, "height_inches": 11.69}, |
| "A3": {"width_mm": 297, "height_mm": 420, "width_inches": 11.69, "height_inches": 16.54}, |
| "US Letter": {"width_mm": 215.9, "height_mm": 279.4, "width_inches": 8.5, "height_inches": 11.0} |
| } |
|
|
|
|
|
|
| def calculate_paper_scaling_factor( |
| paper_contour: np.ndarray, |
| paper_size: str, |
| output_unit: str = "mm" |
| ) -> Tuple[float, str]: |
| """ |
| Calculate scaling factor based on detected paper dimensions with proper unit handling. |
| Includes empirical correction factor for improved accuracy. |
| |
| :param paper_contour: Detected paper contour |
| :param paper_size: Paper size identifier ("A4", "A3", "US Letter") |
| :param output_unit: Desired unit for scaling factor ("mm" or "inches") |
| :return: Tuple of (scaling_factor, unit_string) |
| """ |
| |
| |
| CORRECTION_FACTOR = 0.79 |
| |
| |
| if output_unit == "inches": |
| expected_width = PAPER_SIZES[paper_size]["width_inches"] |
| expected_height = PAPER_SIZES[paper_size]["height_inches"] |
| unit_string = "inches per pixel" |
| else: |
| expected_width = PAPER_SIZES[paper_size]["width_mm"] |
| expected_height = PAPER_SIZES[paper_size]["height_mm"] |
| unit_string = "mm per pixel" |
| |
| |
| rect = cv2.boundingRect(paper_contour) |
| detected_width_px = rect[2] |
| detected_height_px = rect[3] |
| |
| |
| scale_x = expected_width / detected_width_px |
| scale_y = expected_height / detected_height_px |
| |
| |
| |
| |
| scaling_factor_min = min(scale_x, scale_y) |
| |
| |
| scaling_factor_avg = (scale_x + scale_y) / 2 |
| |
| |
| detected_aspect_ratio = detected_width_px / detected_height_px |
| expected_aspect_ratio = expected_width / expected_height |
| |
| if abs(detected_aspect_ratio - expected_aspect_ratio) < abs(detected_aspect_ratio - (1/expected_aspect_ratio)): |
| |
| scaling_factor_oriented = (scale_x + scale_y) / 2 |
| else: |
| |
| scale_x_rot = expected_height / detected_width_px |
| scale_y_rot = expected_width / detected_height_px |
| scaling_factor_oriented = (scale_x_rot + scale_y_rot) / 2 |
| |
| |
| base_scaling_factor = scaling_factor_avg |
| |
| |
| scaling_factor = base_scaling_factor * CORRECTION_FACTOR |
| |
| print(f"Paper detection: {detected_width_px}x{detected_height_px} px") |
| print(f"Expected paper size: {expected_width}x{expected_height} {output_unit}") |
| print(f"Scale X: {scale_x:.6f}, Scale Y: {scale_y:.6f}") |
| print(f"Base scaling (avg): {base_scaling_factor:.6f}") |
| print(f"Correction factor: {CORRECTION_FACTOR}") |
| print(f"Final scaling factor: {scaling_factor:.6f} {unit_string}") |
| |
| return scaling_factor, unit_string |
| |
| def calculate_paper_scaling_factor_corrected( |
| paper_contour: np.ndarray, |
| paper_size: str, |
| output_unit: str = "mm", |
| correction_factor: float = 0.79, |
| method: str = "average" |
| ) -> Tuple[float, str]: |
| """ |
| Calculate scaling factor with configurable correction and method. |
| |
| :param paper_contour: Detected paper contour |
| :param paper_size: Paper size identifier ("A4", "A3", "US Letter") |
| :param output_unit: Desired unit for scaling factor ("mm" or "inches") |
| :param correction_factor: Empirical correction factor (default 0.79) |
| :param method: Calculation method ("min", "max", "average", "auto") |
| :return: Tuple of (scaling_factor, unit_string) |
| """ |
| |
| if output_unit == "inches": |
| expected_width = PAPER_SIZES[paper_size]["width_inches"] |
| expected_height = PAPER_SIZES[paper_size]["height_inches"] |
| unit_string = "inches per pixel" |
| else: |
| expected_width = PAPER_SIZES[paper_size]["width_mm"] |
| expected_height = PAPER_SIZES[paper_size]["height_mm"] |
| unit_string = "mm per pixel" |
| |
| |
| rect = cv2.boundingRect(paper_contour) |
| detected_width_px = rect[2] |
| detected_height_px = rect[3] |
| |
| |
| scale_x = expected_width / detected_width_px |
| scale_y = expected_height / detected_height_px |
| |
| |
| if method == "min": |
| base_scaling_factor = min(scale_x, scale_y) |
| elif method == "max": |
| base_scaling_factor = max(scale_x, scale_y) |
| elif method == "average": |
| base_scaling_factor = (scale_x + scale_y) / 2 |
| elif method == "auto": |
| |
| detected_aspect = detected_width_px / detected_height_px |
| expected_aspect = expected_width / expected_height |
| |
| |
| aspect_diff = abs(detected_aspect - expected_aspect) |
| aspect_diff_inv = abs(detected_aspect - (1/expected_aspect)) |
| |
| if aspect_diff < aspect_diff_inv: |
| base_scaling_factor = (scale_x + scale_y) / 2 |
| else: |
| |
| scale_x_swap = expected_height / detected_width_px |
| scale_y_swap = expected_width / detected_height_px |
| base_scaling_factor = (scale_x_swap + scale_y_swap) / 2 |
| else: |
| raise ValueError(f"Unknown method: {method}") |
| |
| |
| scaling_factor = base_scaling_factor * correction_factor |
| |
| print(f"Paper detection: {detected_width_px}x{detected_height_px} px") |
| print(f"Expected paper size: {expected_width}x{expected_height} {output_unit}") |
| print(f"Scale X: {scale_x:.6f}, Scale Y: {scale_y:.6f}") |
| print(f"Method: {method}, Base scaling: {base_scaling_factor:.6f}") |
| print(f"Correction factor: {correction_factor}") |
| print(f"Final scaling factor: {scaling_factor:.6f} {unit_string}") |
| |
| return scaling_factor, unit_string |
|
|
| |
| if __name__ == "__main__": |
| import os |
| from PIL import Image |
| |
| |
| sample_dir = "./sample_images" |
| if os.path.exists(sample_dir): |
| for idx, file in enumerate(os.listdir(sample_dir)): |
| if file.lower().endswith(('.jpg', '.jpeg', '.png')): |
| img_path = os.path.join(sample_dir, file) |
| img = np.array(Image.open(img_path)) |
| |
| |
| if len(img.shape) == 3: |
| img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) |
| else: |
| img_gray = img |
| |
| print(f"\nProcessing: {file}") |
| |
| try: |
| |
| scaling_factor_mm, unit_mm = calculate_scaling_factor_with_units( |
| target_image=img_gray, |
| reference_obj_size=20.0, |
| reference_unit="mm", |
| output_unit="mm", |
| feature_detector="ORB", |
| debug=False, |
| roi_margin=90, |
| ) |
| |
| |
| scaling_factor_inch, unit_inch = calculate_scaling_factor_with_units( |
| target_image=img_gray, |
| reference_obj_size=0.787, |
| reference_unit="inches", |
| output_unit="inches", |
| feature_detector="ORB", |
| debug=False, |
| roi_margin=90, |
| ) |
| |
| print(f"MM scaling: {scaling_factor_mm:.6f} {unit_mm}") |
| print(f"Inch scaling: {scaling_factor_inch:.6f} {unit_inch}") |
| |
| |
| converted_mm_to_inch = scaling_factor_mm / 25.4 |
| print(f"Converted mm to inch: {converted_mm_to_inch:.6f}") |
| print(f"Difference: {abs(scaling_factor_inch - converted_mm_to_inch):.8f}") |
| |
| except Exception as e: |
| print(f"Error processing {file}: {e}") |
| else: |
| print(f"Sample directory {sample_dir} not found") |