Spaces:
Running
Running
| from flask import Flask, request, jsonify | |
| import numpy as np | |
| import cv2 | |
| # import your existing code | |
| from main import FingerQualityAssessor # or wherever the class lives | |
| from quality_analyzer import QualityConfig | |
| app = Flask(__name__) | |
| # Create once (so model/config not recreated per request) | |
| CONFIG = QualityConfig( | |
| target_width=640, | |
| blur_min=60.0, | |
| illum_min=50.0, | |
| illum_max=200.0, | |
| coverage_min=0.10, | |
| orientation_max_deviation=45.0, | |
| vertical_expected=True, | |
| overall_quality_threshold=0.70, | |
| ) | |
| ASSESSOR = FingerQualityAssessor(CONFIG) | |
| def _decode_uploaded_image(file_storage): | |
| # file_storage is a Werkzeug FileStorage from request.files | |
| data = file_storage.read() | |
| if not data: | |
| raise ValueError("Empty file") | |
| nparr = np.frombuffer(data, np.uint8) | |
| img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # BGR image | |
| if img is None: | |
| raise ValueError("Could not decode image (invalid/unsupported format)") | |
| return img | |
| def root(): | |
| return jsonify({ | |
| "status": "ok", | |
| "message": "Finger Quality API is running", | |
| "endpoints": ["/api/v1/finger-quality"] | |
| }), 200 | |
| def health(): | |
| return "OK", 200 | |
| def finger_quality(): | |
| """ | |
| Expects: multipart/form-data with file field name 'image' | |
| Returns: JSON with result + feedback | |
| """ | |
| if "image" not in request.files: | |
| return jsonify({"error": "Missing file field 'image'"}), 400 # jsonify returns application/json response [web:7] | |
| try: | |
| bgr = _decode_uploaded_image(request.files["image"]) | |
| result, feedback, _debug = ASSESSOR.assess(bgr, draw_debug=False) | |
| # Build JSON-safe dict (do not return numpy types/tuples blindly) | |
| payload = { | |
| "result": { | |
| "blur_score": float(result.blur_score), | |
| "illumination_score": float(result.illumination_score), | |
| "coverage_ratio": float(result.coverage_ratio), | |
| "orientation_angle_deg": float(result.orientation_angle_deg), | |
| "blur_pass": bool(result.blur_pass), | |
| "illumination_pass": bool(result.illumination_pass), | |
| "coverage_pass": bool(result.coverage_pass), | |
| "orientation_pass": bool(result.orientation_pass), | |
| "quality_score": float(result.quality_score), | |
| "overall_pass": bool(result.overall_pass), | |
| "bbox": list(result.bbox) if result.bbox is not None else None, | |
| "contour_area": float(result.contour_area), | |
| }, | |
| "feedback": { | |
| "is_acceptable": bool(feedback.is_acceptable), | |
| "messages": [ | |
| { | |
| "severity": m.severity, | |
| "category": m.category, | |
| "message": m.message, | |
| } | |
| for m in (feedback.messages or []) | |
| ], | |
| }, | |
| } | |
| return jsonify(payload), 200 # jsonify formats JSON response conveniently [web:7] | |
| except ValueError as e: | |
| return jsonify({"error": str(e)}), 400 # jsonify is fine for structured errors [web:7] | |
| except Exception as e: | |
| # avoid leaking internals in production; log e server-side | |
| return jsonify({"error": "Internal server error"}), 500 # jsonify response [web:7] | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=7860) | |