Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- measure_finger.py +16 -0
- src/debug_observer.py +7 -0
- src/ring_size.py +54 -0
- web_demo/app.py +6 -1
measure_finger.py
CHANGED
|
@@ -31,6 +31,7 @@ from src.confidence import (
|
|
| 31 |
compute_overall_confidence,
|
| 32 |
)
|
| 33 |
from src.debug_observer import draw_comprehensive_edge_overlay
|
|
|
|
| 34 |
|
| 35 |
# Calibration coefficients (from regression on 60 measurements)
|
| 36 |
_CALIBRATION_PATH = Path(__file__).parent / "src" / "calibration.json"
|
|
@@ -703,6 +704,11 @@ def measure_finger(
|
|
| 703 |
width_data["median_width_cm"] = cal_cm
|
| 704 |
width_data["raw_width_cm"] = raw_cm
|
| 705 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
debug_image = draw_comprehensive_edge_overlay(
|
| 707 |
full_image=image_canonical,
|
| 708 |
edge_data=edge_data,
|
|
@@ -792,6 +798,13 @@ def main() -> int:
|
|
| 792 |
else:
|
| 793 |
result["calibration_applied"] = False
|
| 794 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
# Save output
|
| 796 |
save_output(result, args.output)
|
| 797 |
print(f"Results saved to: {args.output}")
|
|
@@ -804,6 +817,9 @@ def main() -> int:
|
|
| 804 |
print(f"Finger diameter: {result['finger_outer_diameter_cm']} cm")
|
| 805 |
if result.get("raw_diameter_cm"):
|
| 806 |
print(f" (raw: {result['raw_diameter_cm']} cm, calibrated)")
|
|
|
|
|
|
|
|
|
|
| 807 |
print(f"Confidence: {result['confidence']}")
|
| 808 |
return 0
|
| 809 |
|
|
|
|
| 31 |
compute_overall_confidence,
|
| 32 |
)
|
| 33 |
from src.debug_observer import draw_comprehensive_edge_overlay
|
| 34 |
+
from src.ring_size import recommend_ring_size
|
| 35 |
|
| 36 |
# Calibration coefficients (from regression on 60 measurements)
|
| 37 |
_CALIBRATION_PATH = Path(__file__).parent / "src" / "calibration.json"
|
|
|
|
| 704 |
width_data["median_width_cm"] = cal_cm
|
| 705 |
width_data["raw_width_cm"] = raw_cm
|
| 706 |
|
| 707 |
+
# Add ring size for overlay
|
| 708 |
+
rec = recommend_ring_size(cal_cm)
|
| 709 |
+
if rec:
|
| 710 |
+
width_data["ring_size_rec"] = rec
|
| 711 |
+
|
| 712 |
debug_image = draw_comprehensive_edge_overlay(
|
| 713 |
full_image=image_canonical,
|
| 714 |
edge_data=edge_data,
|
|
|
|
| 798 |
else:
|
| 799 |
result["calibration_applied"] = False
|
| 800 |
|
| 801 |
+
# Ring size recommendation (from calibrated diameter)
|
| 802 |
+
diameter = result.get("finger_outer_diameter_cm")
|
| 803 |
+
if diameter is not None:
|
| 804 |
+
rec = recommend_ring_size(diameter)
|
| 805 |
+
if rec:
|
| 806 |
+
result["ring_size"] = rec
|
| 807 |
+
|
| 808 |
# Save output
|
| 809 |
save_output(result, args.output)
|
| 810 |
print(f"Results saved to: {args.output}")
|
|
|
|
| 817 |
print(f"Finger diameter: {result['finger_outer_diameter_cm']} cm")
|
| 818 |
if result.get("raw_diameter_cm"):
|
| 819 |
print(f" (raw: {result['raw_diameter_cm']} cm, calibrated)")
|
| 820 |
+
rec = result.get("ring_size")
|
| 821 |
+
if rec:
|
| 822 |
+
print(f"Ring size: best match {rec['best_match']}, recommended {rec['range_min']}-{rec['range_max']}")
|
| 823 |
print(f"Confidence: {result['confidence']}")
|
| 824 |
return 0
|
| 825 |
|
src/debug_observer.py
CHANGED
|
@@ -1203,9 +1203,16 @@ def draw_comprehensive_edge_overlay(
|
|
| 1203 |
if raw_cm is not None:
|
| 1204 |
width_label += f" (raw {raw_cm:.3f})"
|
| 1205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1206 |
annotations = [
|
| 1207 |
f"Sobel Edge Detection Results:",
|
| 1208 |
width_label,
|
|
|
|
| 1209 |
f" Std Dev: {std_px:.2f} px",
|
| 1210 |
f" Valid Edges: {np.sum(valid_rows)}/{len(valid_rows)} ({valid_pct:.1f}%)",
|
| 1211 |
f" Measurements: {num_samples}",
|
|
|
|
| 1203 |
if raw_cm is not None:
|
| 1204 |
width_label += f" (raw {raw_cm:.3f})"
|
| 1205 |
|
| 1206 |
+
# Ring size recommendation
|
| 1207 |
+
ring_rec = width_data.get("ring_size_rec")
|
| 1208 |
+
ring_label = ""
|
| 1209 |
+
if ring_rec:
|
| 1210 |
+
ring_label = f" Ring Size: best {ring_rec['best_match']}, try {ring_rec['range_min']}-{ring_rec['range_max']}"
|
| 1211 |
+
|
| 1212 |
annotations = [
|
| 1213 |
f"Sobel Edge Detection Results:",
|
| 1214 |
width_label,
|
| 1215 |
+
ring_label,
|
| 1216 |
f" Std Dev: {std_px:.2f} px",
|
| 1217 |
f" Valid Edges: {np.sum(valid_rows)}/{len(valid_rows)} ({valid_pct:.1f}%)",
|
| 1218 |
f" Measurements: {num_samples}",
|
src/ring_size.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ring size recommendation from calibrated finger width."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, Optional, Tuple
|
| 4 |
+
|
| 5 |
+
# China standard ring size chart: size → inner diameter (mm)
|
| 6 |
+
RING_SIZE_CHART = {
|
| 7 |
+
6: 16.9,
|
| 8 |
+
7: 17.7,
|
| 9 |
+
8: 18.6,
|
| 10 |
+
9: 19.4,
|
| 11 |
+
10: 20.3,
|
| 12 |
+
11: 21.1,
|
| 13 |
+
12: 21.9,
|
| 14 |
+
13: 22.7,
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
# Sorted sizes for lookup
|
| 18 |
+
_SORTED_SIZES = sorted(RING_SIZE_CHART.items(), key=lambda x: x[1])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def recommend_ring_size(diameter_cm: float) -> Optional[Dict]:
|
| 22 |
+
"""Recommend ring size from calibrated finger outer diameter.
|
| 23 |
+
|
| 24 |
+
Returns dict with:
|
| 25 |
+
- best_match: nearest ring size (int)
|
| 26 |
+
- best_match_inner_mm: inner diameter of best match
|
| 27 |
+
- range_min / range_max: recommended 2-size range
|
| 28 |
+
- diameter_mm: input converted to mm
|
| 29 |
+
Returns None if diameter is out of reasonable range.
|
| 30 |
+
"""
|
| 31 |
+
diameter_mm = diameter_cm * 10.0
|
| 32 |
+
|
| 33 |
+
if diameter_mm < 14.0 or diameter_mm > 26.0:
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
# Find nearest size
|
| 37 |
+
best_size, best_inner = min(_SORTED_SIZES, key=lambda x: abs(x[1] - diameter_mm))
|
| 38 |
+
|
| 39 |
+
# Find second nearest size
|
| 40 |
+
second_size, second_inner = min(
|
| 41 |
+
(s for s in _SORTED_SIZES if s[0] != best_size),
|
| 42 |
+
key=lambda x: abs(x[1] - diameter_mm),
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
range_min = min(best_size, second_size)
|
| 46 |
+
range_max = max(best_size, second_size)
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
"best_match": best_size,
|
| 50 |
+
"best_match_inner_mm": best_inner,
|
| 51 |
+
"range_min": range_min,
|
| 52 |
+
"range_max": range_max,
|
| 53 |
+
"diameter_mm": round(diameter_mm, 2),
|
| 54 |
+
}
|
web_demo/app.py
CHANGED
|
@@ -20,6 +20,7 @@ ROOT_DIR = Path(__file__).resolve().parents[1]
|
|
| 20 |
sys.path.insert(0, str(ROOT_DIR))
|
| 21 |
|
| 22 |
from measure_finger import measure_finger, apply_calibration
|
|
|
|
| 23 |
|
| 24 |
APP_ROOT = Path(__file__).resolve().parent
|
| 25 |
UPLOAD_DIR = APP_ROOT / "uploads"
|
|
@@ -127,8 +128,12 @@ def _run_measurement(
|
|
| 127 |
raw_diameter = result.get("finger_outer_diameter_cm")
|
| 128 |
if raw_diameter is not None:
|
| 129 |
result["raw_diameter_cm"] = round(raw_diameter, 4)
|
| 130 |
-
|
|
|
|
| 131 |
result["calibration_applied"] = True
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
result_json_name = f"{run_id}__result.json"
|
| 134 |
result_json_path = RESULTS_DIR / result_json_name
|
|
|
|
| 20 |
sys.path.insert(0, str(ROOT_DIR))
|
| 21 |
|
| 22 |
from measure_finger import measure_finger, apply_calibration
|
| 23 |
+
from src.ring_size import recommend_ring_size
|
| 24 |
|
| 25 |
APP_ROOT = Path(__file__).resolve().parent
|
| 26 |
UPLOAD_DIR = APP_ROOT / "uploads"
|
|
|
|
| 128 |
raw_diameter = result.get("finger_outer_diameter_cm")
|
| 129 |
if raw_diameter is not None:
|
| 130 |
result["raw_diameter_cm"] = round(raw_diameter, 4)
|
| 131 |
+
calibrated = round(apply_calibration(raw_diameter), 4)
|
| 132 |
+
result["finger_outer_diameter_cm"] = calibrated
|
| 133 |
result["calibration_applied"] = True
|
| 134 |
+
rec = recommend_ring_size(calibrated)
|
| 135 |
+
if rec:
|
| 136 |
+
result["ring_size"] = rec
|
| 137 |
|
| 138 |
result_json_name = f"{run_id}__result.json"
|
| 139 |
result_json_path = RESULTS_DIR / result_json_name
|