inspector-model / saddle_structure_validator.py
eho69's picture
mesh building
d3e4c8a verified
"""
Enhanced Saddle Structure Validator
====================================
Validates saddles based on physical structure:
1. Semicircular top surface
2. Vertical arc dividing the semicircle exactly in the middle
"""
import cv2
import numpy as np
from typing import Tuple, Optional
from dataclasses import dataclass
@dataclass
class SaddleStructure:
"""Detected saddle structure components"""
has_semicircle: bool
semicircle_center: Optional[Tuple[int, int]]
semicircle_radius: Optional[int]
has_middle_arc: bool
arc_center_x: Optional[int]
arc_alignment_score: float
structure_confidence: float
def is_valid_saddle(self, min_confidence: float = 0.7) -> bool:
return (self.has_semicircle and
self.has_middle_arc and
self.structure_confidence >= min_confidence)
class SaddleStructureValidator:
"""
Validates saddle structure using geometric constraints:
1. Semicircular Surface Detection (Hough Circle + contour fallback)
2. Middle Arc Detection (Sobel + Hough Lines + intensity profile)
3. Alignment Validation (arc-semicircle center ±5% tolerance)
"""
def __init__(self,
arc_center_tolerance: float = 0.05,
min_arc_length_ratio: float = 0.6,
min_structure_confidence: float = 0.7):
self.arc_center_tolerance = arc_center_tolerance
self.min_arc_length_ratio = min_arc_length_ratio
self.min_structure_confidence = min_structure_confidence
def validate_structure(self, crop: np.ndarray) -> SaddleStructure:
"""Main validation function - returns SaddleStructure"""
if crop is None or crop.size == 0:
return self._invalid_structure()
h, w = crop.shape[:2]
if h < 20 or w < 20:
return self._invalid_structure()
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) if len(crop.shape) == 3 else crop.copy()
# Step 1: Detect semicircular surface
semi_detected, semi_center, semi_radius = self._detect_semicircular_surface(gray)
# Step 2: Detect middle arc
arc_detected, arc_center_x, arc_angle, arc_length = self._detect_middle_arc(gray)
# Step 3: Validate alignment
alignment_score = 0.0
if semi_detected and arc_detected and semi_center is not None:
expected_center_x = semi_center[0]
center_deviation = abs(arc_center_x - expected_center_x)
max_deviation = w * self.arc_center_tolerance
alignment_score = max(0.0, 1.0 - (center_deviation / max_deviation)) if center_deviation <= max_deviation else 0.0
if arc_length / h < self.min_arc_length_ratio:
alignment_score *= 0.5
if arc_angle is not None and abs(90 - abs(arc_angle)) > 15:
alignment_score *= 0.5
# Step 4: Calculate confidence
if semi_detected and arc_detected:
structure_confidence = alignment_score
elif semi_detected:
structure_confidence = 0.3
elif arc_detected:
structure_confidence = 0.2
else:
structure_confidence = 0.0
return SaddleStructure(
has_semicircle=semi_detected,
semicircle_center=semi_center,
semicircle_radius=semi_radius,
has_middle_arc=arc_detected,
arc_center_x=arc_center_x,
arc_alignment_score=alignment_score,
structure_confidence=structure_confidence
)
def _detect_semicircular_surface(self, gray: np.ndarray) -> Tuple[bool, Optional[Tuple[int, int]], Optional[int]]:
"""Detect semicircular surface on top portion of saddle"""
h, w = gray.shape
upper_region = gray[:int(h * 0.6), :]
blurred = cv2.GaussianBlur(upper_region, (5, 5), 1.5)
edges = cv2.Canny(blurred, 30, 100)
circles = cv2.HoughCircles(
edges, cv2.HOUGH_GRADIENT, dp=1.0, minDist=w // 2,
param1=50, param2=30, minRadius=int(w * 0.3), maxRadius=int(w * 0.8)
)
if circles is None or len(circles[0]) == 0:
return self._detect_semicircle_from_contours(upper_region, w, h)
circles = np.round(circles[0, :]).astype(int)
cx, cy, r = sorted(circles, key=lambda c: c[2], reverse=True)[0]
if abs(cx - w // 2) > w * 0.2 or cy > h * 0.5 or r < w * 0.3 or r > w * 0.8:
return False, None, None
return True, (cx, cy), r
def _detect_semicircle_from_contours(self, upper_region: np.ndarray, full_w: int, full_h: int):
"""Fallback: Detect semicircle using contours"""
contours, _ = cv2.findContours(cv2.Canny(upper_region, 30, 100), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return False, None, None
largest = max(contours, key=cv2.contourArea)
if len(largest) < 5:
return False, None, None
(cx, cy), radius = cv2.minEnclosingCircle(largest)
cx, cy, radius = int(cx), int(cy), int(radius)
if abs(cx - full_w // 2) > full_w * 0.2:
return False, None, None
return True, (cx, cy), radius
def _detect_middle_arc(self, gray: np.ndarray) -> Tuple[bool, Optional[int], Optional[float], float]:
"""Detect vertical arc dividing the saddle in the middle"""
h, w = gray.shape
# Method 1: Vertical edge detection
arc_x, arc_length = self._detect_arc_from_edges(gray)
if arc_x is not None:
return True, arc_x, 90.0, arc_length
# Method 2: Hough Line detection
arc_detected, arc_x, angle, length = self._detect_arc_from_lines(gray)
if arc_detected:
return True, arc_x, angle, length
# Method 3: Intensity profile
arc_x = self._detect_arc_from_intensity(gray)
if arc_x is not None:
return True, arc_x, 90.0, h * 0.8
return False, None, None, 0.0
def _detect_arc_from_edges(self, gray: np.ndarray) -> Tuple[Optional[int], float]:
"""Detect arc using vertical edge detection (Sobel X)"""
h, w = gray.shape
sobelx = np.abs(cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3))
center_start, center_end = int(w * 0.3), int(w * 0.7)
center_region = sobelx[:, center_start:center_end]
vertical_sums = np.sum(center_region, axis=0)
if len(vertical_sums) == 0:
return None, 0.0
peak_idx = np.argmax(vertical_sums)
if vertical_sums[peak_idx] < np.mean(vertical_sums) + np.std(vertical_sums):
return None, 0.0
arc_x = center_start + peak_idx
column = sobelx[:, arc_x]
arc_length = float(np.sum(column > np.percentile(column, 70)))
return arc_x, arc_length
def _detect_arc_from_lines(self, gray: np.ndarray) -> Tuple[bool, Optional[int], Optional[float], float]:
"""Detect arc using Hough Line Transform"""
h, w = gray.shape
edges = cv2.Canny(gray, 50, 150)
lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=int(h * 0.3),
minLineLength=int(h * 0.4), maxLineGap=int(h * 0.2))
if lines is None:
return False, None, None, 0.0
vertical_lines = []
for line in lines:
x1, y1, x2, y2 = line[0]
angle = 90.0 if x2 == x1 else abs(np.degrees(np.arctan2(y2 - y1, x2 - x1)))
if abs(angle - 90) < 15:
line_center_x = (x1 + x2) / 2
if abs(line_center_x - w / 2) < w * 0.3:
length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
vertical_lines.append((line_center_x, angle, length))
if not vertical_lines:
return False, None, None, 0.0
best = max(vertical_lines, key=lambda x: x[2])
return True, int(best[0]), best[1], best[2]
def _detect_arc_from_intensity(self, gray: np.ndarray) -> Optional[int]:
"""Detect arc using intensity profile (dark line in middle)"""
h, w = gray.shape
center_start, center_end = int(w * 0.3), int(w * 0.7)
center_region = gray[:, center_start:center_end]
column_means = np.mean(center_region, axis=0)
if len(column_means) == 0:
return None
darkest_idx = np.argmin(column_means)
darkest_value = column_means[darkest_idx]
if 0 < darkest_idx < len(column_means) - 1:
avg_neighbor = (column_means[darkest_idx - 1] + column_means[darkest_idx + 1]) / 2
if darkest_value < avg_neighbor * 0.9:
return center_start + darkest_idx
return None
def _invalid_structure(self) -> SaddleStructure:
return SaddleStructure(False, None, None, False, None, 0.0, 0.0)