"""Flask web app for crack detection and measurement.""" from __future__ import annotations import base64 import io import os import cv2 import numpy as np from flask import Flask, jsonify, render_template, request, send_file from PIL import Image import aruco_scale import crack_pipeline app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 25 * 1024 * 1024 # 25 MB upload cap def _read_image(file_storage) -> np.ndarray: """Decode an uploaded file into an RGB numpy array.""" image = Image.open(io.BytesIO(file_storage.read())).convert("RGB") return np.array(image) def _encode_png(image_rgb: np.ndarray) -> str: """Encode an RGB array as a base64 PNG data URI.""" bgr = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR) ok, buf = cv2.imencode(".png", bgr) if not ok: raise RuntimeError("PNG encoding failed") return "data:image/png;base64," + base64.b64encode(buf).decode("ascii") @app.route("/") def index(): return render_template("index.html") @app.route("/health") def health(): return jsonify(status="ok") @app.route("/marker") def marker(): """Serve the printable ArUco marker PDF (generated by make_marker.py).""" size = int(aruco_scale.DEFAULT_MARKER_LENGTH_MM) pdf = os.path.join(os.path.dirname(__file__), f"aruco_marker_{size}mm_A4.pdf") if not os.path.exists(pdf): return jsonify(error="Marker PDF missing - run: python make_marker.py"), 404 return send_file(pdf, mimetype="application/pdf") @app.route("/analyze", methods=["POST"]) def analyze(): """Analyse an upload and return only the computer-vision result. Scale always comes from the ArUco marker (no manual options); the model threshold is fixed. The response carries the CV overlay and the CV measurement (intensity-based, refined within the ML-detected region). """ if "image" not in request.files or request.files["image"].filename == "": return jsonify(error="No image uploaded"), 400 try: image_rgb = _read_image(request.files["image"]) except Exception: return jsonify(error="Could not read the uploaded image"), 400 result = crack_pipeline.analyze( image_rgb, use_aruco=True, marker_length_mm=aruco_scale.DEFAULT_MARKER_LENGTH_MM, ) overlay = result["overlay"] return jsonify( overlay=_encode_png(overlay), measurements=result["measurements"], aruco=result["aruco"], image_size={"width": overlay.shape[1], "height": overlay.shape[0]}, ) if __name__ == "__main__": # Port 5000 is taken by macOS AirPlay Receiver, so default to 5001. port = int(os.environ.get("PORT", 5001)) print("Loading crack segmentation model...") crack_pipeline.get_model() print(f"Model ready. Open http://127.0.0.1:{port}") app.run(host="127.0.0.1", port=port, debug=False)