fix: align CV rules and refresh final documentation
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,8 +36,8 @@ Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `meas
|
||||
## Classification rules (Track 3)
|
||||
|
||||
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
|
||||
2. Else if circularity exceeds the official threshold **0.8** → **D**
|
||||
(web twin uses strict `K > 0.8`; this prototype uses `circle_ratio >= 0.8`)
|
||||
2. Else if `circle_ratio > 0.8` → **D**
|
||||
(`K == 0.8` is **not** circular — same strict rule as web `classifier.ts`)
|
||||
3. Else → **B**
|
||||
|
||||
Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames.
|
||||
|
||||
@@ -79,13 +79,14 @@ def classify(
|
||||
"""
|
||||
Порядок ТЗ:
|
||||
1) габариты → иначе C (приоритет над кругом)
|
||||
2) если r_in/r_out >= 0.8 в любом сечении → D
|
||||
2) если r_in/r_out > 0.8 в любом сечении → D
|
||||
(K == 0.8 НЕ круг — как в web classifier.ts)
|
||||
3) иначе → B
|
||||
"""
|
||||
dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm)
|
||||
passes = check_size(dims, min_mm, max_mm)
|
||||
ratio = float(measurement.circle_ratio)
|
||||
circular = ratio >= float(circle_ratio_threshold)
|
||||
circular = ratio > float(circle_ratio_threshold)
|
||||
clipped = bool(getattr(measurement, "clipped_by_frame", False))
|
||||
|
||||
if not passes or clipped:
|
||||
@@ -112,7 +113,7 @@ def classify(
|
||||
circle_ratio=ratio,
|
||||
passes_size=True,
|
||||
is_circular=True,
|
||||
reason=f"круг в сечении: r_in/r_out={ratio:.3f} >= {circle_ratio_threshold}",
|
||||
reason=f"круг в сечении: r_in/r_out={ratio:.3f} > {circle_ratio_threshold}",
|
||||
)
|
||||
|
||||
return ClassificationResult(
|
||||
@@ -121,7 +122,7 @@ def classify(
|
||||
circle_ratio=ratio,
|
||||
passes_size=True,
|
||||
is_circular=False,
|
||||
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} < {circle_ratio_threshold}",
|
||||
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} <= {circle_ratio_threshold}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -113,12 +113,13 @@ class DecisionStabilizer:
|
||||
top_m = _median(self._tops)
|
||||
sec_m = _median(self._secs)
|
||||
ratio_p75 = _percentile(self._ratios, 75)
|
||||
# устойчивый круг: медиана >= 0.8 ИЛИ (медиана сечений >= 0.8 и ≥ половины окна сильные)
|
||||
# устойчивый круг: медиана > 0.8 ИЛИ (медиана сечений > 0.8 и ≥ половины окна сильные)
|
||||
# K == 0.8 официально НЕ круг (строгое > threshold)
|
||||
sec_strong = (
|
||||
sum(1 for x in self._secs if x >= self.enter_circle) / max(1, len(self._secs))
|
||||
sum(1 for x in self._secs if x > self.enter_circle) / max(1, len(self._secs))
|
||||
)
|
||||
circular = ratio >= self.enter_circle or (
|
||||
sec_m >= self.enter_circle and sec_strong >= 0.55
|
||||
circular = ratio > self.enter_circle or (
|
||||
sec_m > self.enter_circle and sec_strong >= 0.55
|
||||
)
|
||||
ratio_show = max(ratio, sec_m) if circular else ratio
|
||||
|
||||
@@ -153,7 +154,7 @@ class DecisionStabilizer:
|
||||
reason=(
|
||||
f"круг: med={ratio:.3f} p75={ratio_p75:.3f} "
|
||||
f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} "
|
||||
f">= {self.enter_circle}"
|
||||
f"> {self.enter_circle}"
|
||||
),
|
||||
)
|
||||
else:
|
||||
@@ -165,7 +166,7 @@ class DecisionStabilizer:
|
||||
is_circular=False,
|
||||
reason=(
|
||||
f"не круг: med={ratio:.3f} sec={sec_m:.3f} "
|
||||
f"strong={sec_strong:.0%} < {self.enter_circle}"
|
||||
f"strong={sec_strong:.0%} <= {self.enter_circle}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -43,11 +43,40 @@ def test_lying_bottle_must_be_D_not_B():
|
||||
assert r.category.zone == "D"
|
||||
|
||||
|
||||
def test_border_circle():
|
||||
r = classify_from_dims(100, 50, 50, circle_ratio=0.8)
|
||||
def test_strict_circle_boundary():
|
||||
"""Официально: круг только при K > 0.8. K == 0.8 → B (не D)."""
|
||||
below = classify_from_dims(100, 50, 50, circle_ratio=0.799999)
|
||||
assert below.category == Category.SUITABLE
|
||||
assert below.category.zone == "B"
|
||||
assert below.is_circular is False
|
||||
|
||||
exact = classify_from_dims(100, 50, 50, circle_ratio=0.8)
|
||||
assert exact.category == Category.SUITABLE
|
||||
assert exact.category.zone == "B"
|
||||
assert exact.is_circular is False
|
||||
|
||||
above = classify_from_dims(100, 50, 50, circle_ratio=0.800001)
|
||||
assert above.category == Category.NEED_PACK
|
||||
assert above.category.zone == "D"
|
||||
assert above.is_circular is True
|
||||
|
||||
|
||||
def test_normal_non_circular_B():
|
||||
r = classify_from_dims(120, 80, 40, circle_ratio=0.5)
|
||||
assert r.category == Category.SUITABLE
|
||||
assert r.category.zone == "B"
|
||||
|
||||
|
||||
def test_normal_circular_above_threshold_D():
|
||||
r = classify_from_dims(100, 50, 50, circle_ratio=0.81)
|
||||
assert r.category == Category.NEED_PACK
|
||||
r2 = classify_from_dims(100, 50, 50, circle_ratio=0.799)
|
||||
assert r2.category == Category.SUITABLE
|
||||
assert r.category.zone == "D"
|
||||
|
||||
|
||||
def test_oversized_circular_still_C():
|
||||
r = classify_from_dims(500, 100, 100, circle_ratio=0.95)
|
||||
assert r.category == Category.OVERSIZE
|
||||
assert r.category.zone == "C"
|
||||
|
||||
|
||||
def test_border_dims_strict():
|
||||
@@ -80,7 +109,10 @@ if __name__ == "__main__":
|
||||
test_too_small()
|
||||
test_need_pack_cylinder()
|
||||
test_lying_bottle_must_be_D_not_B()
|
||||
test_border_circle()
|
||||
test_strict_circle_boundary()
|
||||
test_normal_non_circular_B()
|
||||
test_normal_circular_above_threshold_D()
|
||||
test_oversized_circular_still_C()
|
||||
test_border_dims_strict()
|
||||
test_dims_order_independent()
|
||||
print("OK: all classification tests passed")
|
||||
|
||||
Reference in New Issue
Block a user