Spaces:
Sleeping
Sleeping
File size: 9,408 Bytes
d3e4c8a | 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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | """
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)
|