#!/usr/bin/env python3 """ OpenCV Smart Crop Validation Script Run this on sample photos of your small components (RAM, SFP, HDD, etc) to validate: 1. Auto-crop accuracy (does it detect the object correctly?) 2. Text orientation detection (is text upright?) 3. Performance (how fast on CPU?) Usage: python scripts/opencv_crop_validation.py path/to/photo.jpg python scripts/opencv_crop_validation.py path/to/photo.jpg --show-preview Output: - Prints timing and crop dimensions - Saves debug images to ./validation_output/ """ import cv2 import numpy as np import sys import argparse import time from pathlib import Path def detect_text_orientation(image, bbox): """Detect if text in bounding box is upright or rotated.""" x, y, w, h = bbox roi = image[y:y+h, x:x+w] if roi.size == 0: return 0, "N/A" # Hough line detection to find text angle gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) if len(roi.shape) == 3 else roi edges = cv2.Canny(gray, 100, 200) lines = cv2.HoughLines(edges, 1, np.pi/180, 50) if lines is None: return 0, "no_text_detected" angles = [] for rho, theta in lines[:, 0]: angle = np.degrees(theta) # Normalize to -45 to 45 range (text is typically horizontal) if angle > 90: angle -= 180 angles.append(angle) if not angles: return 0, "no_text_detected" dominant_angle = np.median(angles) # Classify orientation if abs(dominant_angle) < 15: status = "UPRIGHT" elif abs(dominant_angle - 90) < 15 or abs(dominant_angle + 90) < 15: status = "SIDEWAYS" elif abs(dominant_angle - 180) < 15 or abs(dominant_angle + 180) < 15: status = "UPSIDE_DOWN" else: status = "ROTATED" return dominant_angle, status def smart_crop(image_path, output_dir="validation_output"): """ Run the smart crop pipeline. Returns: dict with timing, dimensions, status """ output_path = Path(output_dir) output_path.mkdir(exist_ok=True) # Load image image = cv2.imread(str(image_path)) if image is None: return {"error": f"Could not load image: {image_path}"} original_h, original_w = image.shape[:2] # Time the pipeline t0 = time.time() # Step 1: Convert to grayscale gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Step 2: Edge detection (Canny) edges = cv2.Canny(gray, 100, 200) # Step 3: Find contours contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return { "error": "No contours detected (image may be blank or very simple)", "original_size": f"{original_w}x{original_h}", "time_ms": round((time.time() - t0) * 1000, 2) } # Step 4: Filter contours by size (ignore tiny labels, keep component) # Require contour area >= 5% of image area (filters out text/labels) min_area = (original_w * original_h) * 0.05 large_contours = [c for c in contours if cv2.contourArea(c) >= min_area] if not large_contours: # Fallback: use largest contour anyway largest_contour = max(contours, key=cv2.contourArea) fallback_note = " (used largest contour; no contours met 5% size filter)" else: largest_contour = max(large_contours, key=cv2.contourArea) fallback_note = "" x, y, w, h = cv2.boundingRect(largest_contour) # Step 5: Add 10% padding pad_x = int(w * 0.1) pad_y = int(h * 0.1) x = max(0, x - pad_x) y = max(0, y - pad_y) w = min(original_w - x, w + pad_x * 2) h = min(original_h - y, h + pad_y * 2) # Step 6: Crop cropped = image[y:y+h, x:x+w] # Step 7: Detect text orientation angle, orientation = detect_text_orientation(image, (x, y, w, h)) t_total = time.time() - t0 # Save debug outputs base_name = Path(image_path).stem # Save cropped image cropped_path = output_path / f"{base_name}_cropped.jpg" cv2.imwrite(str(cropped_path), cropped) # Save original with bounding box overlay debug_image = image.copy() cv2.rectangle(debug_image, (x, y), (x+w, y+h), (0, 255, 0), 3) cv2.putText(debug_image, f"Object: {w}x{h}px", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(debug_image, f"Angle: {angle:.1f}° ({orientation})", (x, y-40), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) bbox_path = output_path / f"{base_name}_bbox.jpg" cv2.imwrite(str(bbox_path), debug_image) result = { "status": "OK", "original_size": f"{original_w}x{original_h}", "crop_size": f"{w}x{h}", "crop_position": f"({x}, {y})", "text_angle_degrees": round(angle, 1), "text_orientation": orientation, "time_ms": round(t_total * 1000, 2), "cropped_image": str(cropped_path), "debug_image": str(bbox_path) } if fallback_note: result["note"] = fallback_note return result def main(): parser = argparse.ArgumentParser( description="Validate OpenCV smart crop on a photo" ) parser.add_argument("image", help="Path to image file") parser.add_argument("--show-preview", action="store_true", help="Display images (requires display)") parser.add_argument("--output-dir", default="validation_output", help="Output directory for debug images") args = parser.parse_args() result = smart_crop(args.image, args.output_dir) # Print results print("\n" + "="*60) print(f"VALIDATION: {args.image}") print("="*60) if "error" in result: print(f"❌ ERROR: {result['error']}") else: print(f"✅ CROP SUCCESS") for key, value in result.items(): if key not in ["error"]: print(f" {key:25} {value}") print("="*60 + "\n") # Show preview if requested if args.show_preview and "cropped_image" in result: try: original = cv2.imread(args.image) cropped = cv2.imread(result["cropped_image"]) debug = cv2.imread(result["debug_image"]) # Resize for display if very large h, w = original.shape[:2] if w > 1920 or h > 1080: scale = min(1920/w, 1080/h) original = cv2.resize(original, (int(w*scale), int(h*scale))) cropped = cv2.resize(cropped, (int(cropped.shape[1]*scale), int(cropped.shape[0]*scale))) debug = cv2.resize(debug, (int(debug.shape[1]*scale), int(debug.shape[0]*scale))) cv2.imshow("Original", original) cv2.imshow("Cropped", cropped) cv2.imshow("Debug (with bounds)", debug) print("Displaying images... Press any key to close") cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: print(f"Could not display preview: {e}") if __name__ == "__main__": main()