afms-backend / app.py
ShaikFayaz042
Fixing output2
c45740c
Raw
History Blame Contribute Delete
13.9 kB
import base64
import shutil
import traceback
from pathlib import Path
import cv2
import gradio as gr
import numpy as np
import yaml
from PIL import Image
from ultralytics import YOLO
from src.utils.detect_objects import run_modelA
from src.utils.generate_masks import run_modelB, seg_model as default_seg_model
from src.utils.reference import detect_reference
from src.measure.measure_tool import process_measurements
from src.utils.match_spec import run_spec_match
from src.utils.visualize_all import visualize_detections
ROOT = Path(__file__).resolve().parent
CONFIG_PATH = ROOT / "config" / "settings.yaml"
MODELS_DIR = ROOT / "models"
DATASETS_DIR = ROOT / "data" / "datasets"
INPUTS_DIR = ROOT / "inputs"
OUTPUTS_DIR = ROOT / "outputs"
DEFAULT_PX_PER_MM = 10.0
def load_settings():
if not CONFIG_PATH.exists():
raise FileNotFoundError(f"Missing config file: {CONFIG_PATH}")
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
settings = load_settings()
DETECTION_CFG = settings.get("detection", {})
REFERENCE_CFG = settings.get("reference", {})
REFERENCE_SIZE_MM = float(REFERENCE_CFG.get("size_mm", 20.0))
DETECTION_DEVICE = DETECTION_CFG.get("device", "cpu")
DETECTION_IMAGE_SIZE = int(DETECTION_CFG.get("image_size", 640))
DETECTION_CONF = float(DETECTION_CFG.get("confidence_threshold", 0.25))
DETECTION_IOU = float(DETECTION_CFG.get("iou_threshold", 0.5))
MODEL_A_PATH = MODELS_DIR / "model_a.pt"
MODEL_B_PATH = MODELS_DIR / "model_b.pt"
def ensure_directories():
for d in [
INPUTS_DIR,
OUTPUTS_DIR / "1_captured_images",
OUTPUTS_DIR / "2_reference",
OUTPUTS_DIR / "3_detection" / "labels",
OUTPUTS_DIR / "4_segmentation" / "masks",
OUTPUTS_DIR / "4_segmentation" / "overlay",
OUTPUTS_DIR / "5_measured",
OUTPUTS_DIR / "6_results" / "spec_match_report",
OUTPUTS_DIR / "6_results" / "output_images",
]:
d.mkdir(parents=True, exist_ok=True)
def clean_measured_folder():
measured_dir = OUTPUTS_DIR / "5_measured"
if measured_dir.exists():
for f in measured_dir.glob("*"):
if f.is_file():
f.unlink()
def pil_to_bgr(image):
if isinstance(image, np.ndarray):
rgb = image
elif isinstance(image, Image.Image):
rgb = np.array(image)
else:
raise ValueError("Unsupported image type")
if rgb.ndim == 2:
rgb = cv2.cvtColor(rgb, cv2.COLOR_GRAY2RGB)
if rgb.shape[2] == 4:
rgb = cv2.cvtColor(rgb, cv2.COLOR_RGBA2RGB)
return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
def save_input_image(image, filename="input.jpg"):
img_bgr = pil_to_bgr(image)
path = INPUTS_DIR / filename
cv2.imwrite(str(path), img_bgr)
return path
def encode_image_base64(image):
success, buffer = cv2.imencode(".jpg", image)
if not success:
raise RuntimeError("Could not encode image to JPEG")
return base64.b64encode(buffer.tobytes()).decode("utf-8")
def build_report_text(spec_results, ref_ok, px_per_mm, fallback_used):
lines = []
# Reference status
if ref_ok:
lines.append(f"✅ Reference detected → {px_per_mm:.3f} px/mm")
else:
if fallback_used:
lines.append(f"⚠️ Reference NOT found. Using default calibration: {px_per_mm:.3f} px/mm")
else:
lines.append("❌ Reference square missing and no fallback active.")
if not spec_results:
lines.append("No components measured.")
return "\n".join(lines)
# Number of components
lines.append(f"\nno of components detected - {len(spec_results)}")
lines.append("=" * 50)
for idx, res in enumerate(spec_results, 1):
comp = res['class'].upper()
measured = res.get('measured', {})
ref = res.get('reference', {})
# Determine nominal display
nominal = "N/A"
if ref:
# Try common keys
nominal = ref.get('Nominal Dia', ref.get('Nominal_M', ref.get('Screw Size', 'N/A')))
elif measured:
nominal = measured.get('Nominal_Dia', measured.get('Nominal_M', 'N/A'))
lines.append(f"{idx}) 🔩 {comp} | {nominal}")
# Measured dimensions (only those that appear in the measured dict)
if measured:
lines.append(" Measured values:")
for k, v in measured.items():
# Skip internal/verbose keys
if k in ['class', 'file']:
continue
lines.append(f" {k}: {v}")
else:
lines.append(" No measured dimensions.")
# Reference (ISO) dimensions
if ref:
lines.append(" Standard Specifications (ISO):")
for k, v in ref.items():
# Skip keys that are not human‑readable or already shown
if k in ['Min_Standard_Length', 'Max_Standard_Length', 'Preferred_Standards',
'Standard_Length_mm', 'Length_Deviation_mm', 'Length_Valid', 'Length_In_Range',
'file', 'class']:
continue
lines.append(f" {k}: {v}")
else:
lines.append(" No ISO match found.")
lines.append("-" * 40)
lines.append("=" * 50)
return "\n".join(lines)
def build_visualization(image_path, detections, mask_array, measurement_results, spec_results):
image = cv2.imread(str(image_path))
if image is None:
raise RuntimeError(f"Unable to read image: {image_path}")
h, w = image.shape[:2]
det_list, mask_list, meas_list = [], [], []
spec_map = {}
for s in spec_results:
stem = Path(s["file"]).stem
# Remove "_measured" suffix to match obj_id format
key = stem.replace("_measured", "") if "_measured" in stem else stem
spec_map[key] = s.get("reference", {})
for idx, det in enumerate(detections):
det_list.append({"label": det["class_name"], "bbox": det["xyxy"]})
x1, y1, x2, y2 = map(int, det["xyxy"])
full_mask = np.zeros((h, w), dtype=np.uint8)
if mask_array is not None and y2 > y1 and x2 > x1:
cropped = mask_array[y1:y2, x1:x2]
if cropped.size > 0:
mask_crop = (cropped > 127).astype(np.uint8) if cropped.max() > 1 else (cropped > 0).astype(np.uint8)
full_mask[y1:y2, x1:x2] = mask_crop
mask_list.append(full_mask)
cls = det["class_name"].lower()
obj_id = f"{cls}_{idx+1}"
meas = measurement_results.get(obj_id, {}).copy()
matched = spec_map.get(obj_id, {})
combined = {**meas, **matched}
# Normalize keys for visualize_all.py
if cls == "nut":
# Ensure AF is available
if "AF" not in combined and "AF_mm" in combined:
combined["AF"] = combined["AF_mm"]
# Ensure Nominal_Dia and Nominal_M are set from CSV "Nominal Dia" column
if "Nominal Dia" in combined:
if "Nominal_Dia" not in combined:
combined["Nominal_Dia"] = combined["Nominal Dia"]
if "Nominal_M" not in combined:
combined["Nominal_M"] = combined["Nominal Dia"]
# Also handle Nut Size key from CSV
if "Nut Size" in matched and "Nut Size" not in combined:
combined["Nut Size"] = matched["Nut Size"]
elif cls == "washer":
# Ensure OD and ID are available
if "OD" not in combined and "OD_mm" in combined:
combined["OD"] = combined["OD_mm"]
if "ID" not in combined and "ID_mm" in combined:
combined["ID"] = combined["ID_mm"]
# Ensure Nominal_Dia and Nominal_M are set from CSV "Nominal Dia" column
if "Nominal Dia" in combined:
if "Nominal_Dia" not in combined:
combined["Nominal_Dia"] = combined["Nominal Dia"]
if "Nominal_M" not in combined:
combined["Nominal_M"] = combined["Nominal Dia"]
elif cls == "bolt":
# Map CSV column "Bolt Size" to Nominal_M and Nominal_Dia
if "Bolt Size" in matched:
combined["Nominal_M"] = matched["Bolt Size"]
combined["Nominal_Dia"] = matched["Bolt Size"]
# Also handle "Nominal Dia" if present
if "Nominal Dia" in combined:
if "Nominal_M" not in combined:
combined["Nominal_M"] = combined["Nominal Dia"]
if "Nominal_Dia" not in combined:
combined["Nominal_Dia"] = combined["Nominal Dia"]
# Ensure Length_mm
if "Length_mm" not in combined and "Length" in combined:
combined["Length_mm"] = combined["Length"]
elif cls == "screw":
# Ensure Nominal_Dia and Nominal_M are set from CSV "Nominal Dia" column
if "Nominal Dia" in combined:
if "Nominal_Dia" not in combined:
combined["Nominal_Dia"] = combined["Nominal Dia"]
if "Nominal_M" not in combined:
combined["Nominal_M"] = combined["Nominal Dia"]
# Ensure Length_mm is available
if "Length_mm" not in combined and "Length" in combined:
combined["Length_mm"] = combined["Length"]
# Also handle Screw Size key from CSV if needed
if "Screw Size" in matched and "Screw Size" not in combined:
combined["Screw Size"] = matched["Screw Size"]
meas_list.append(combined)
return visualize_detections(image, det_list, mask_list, meas_list)
def predict(image):
ensure_directories()
clean_measured_folder()
fallback_used = False
if image is None:
return {"success": False, "output_image": None, "report_text": "No image provided."}
try:
input_path = save_input_image(image, "input.jpg")
ref_status, px_per_mm, _ = detect_reference(
image_path=str(input_path),
ref_size_mm=REFERENCE_SIZE_MM,
save_path=str(OUTPUTS_DIR / "2_reference"),
)
if ref_status != "success":
px_per_mm = DEFAULT_PX_PER_MM
fallback_used = True
if not MODEL_A_PATH.exists() or not MODEL_B_PATH.exists():
raise FileNotFoundError("YOLO models not found in models/")
detection_model = YOLO(str(MODEL_A_PATH))
detections, label_path = run_modelA(
image_path=str(input_path),
device=DETECTION_DEVICE,
imgsz=DETECTION_IMAGE_SIZE,
conf_thr=DETECTION_CONF,
iou_thr=DETECTION_IOU,
save_annotated=True,
outdir=str(OUTPUTS_DIR / "3_detection"),
save_labels=True,
model=detection_model,
)
if not detections:
encoded = encode_image_base64(pil_to_bgr(image))
return {
"success": True,
"output_image": encoded,
"report_text": "No fasteners detected.",
}
mask_array = None
if label_path:
mask_result = run_modelB(
img_path=str(input_path),
label_txt_path=str(label_path),
seg_model=default_seg_model,
masks_dir=str(OUTPUTS_DIR / "4_segmentation" / "masks"),
images_dir=str(OUTPUTS_DIR / "4_segmentation" / "overlay"),
)
mask_array = mask_result.get("mask_array")
if mask_array is None:
img_cv = cv2.imread(str(input_path))
h, w = img_cv.shape[:2]
mask_array = np.zeros((h, w), dtype=np.uint8)
measurement_results = process_measurements(
image_path=str(input_path),
detections=detections,
label_path=str(label_path) if label_path else None,
mask_data=mask_array,
px_per_mm=px_per_mm,
)
spec_results = []
if measurement_results:
spec_results = run_spec_match(
measurements_dir=str(OUTPUTS_DIR / "5_measured"),
reference_csv_dict={
"washer": str(DATASETS_DIR / "washers_dataset.csv"),
"bolt": str(DATASETS_DIR / "bolts_dataset.csv"),
"nut": str(DATASETS_DIR / "nuts_dataset.csv"),
"screw": str(DATASETS_DIR / "screws_dataset.csv"),
},
output_txt=str(OUTPUTS_DIR / "6_results" / "spec_match_report" / "spec_match_report.txt"),
)
output_image = build_visualization(
image_path=str(input_path),
detections=detections,
mask_array=mask_array,
measurement_results=measurement_results,
spec_results=spec_results,
)
report = build_report_text(spec_results, ref_status == "success", px_per_mm, fallback_used)
return {
"success": True,
"output_image": encode_image_base64(output_image),
"report_text": report,
}
except Exception as e:
traceback.print_exc()
return {"success": False, "output_image": None, "report_text": f"Error: {str(e)}"}
ensure_directories()
with gr.Blocks() as demo:
gr.Markdown("# 🔧 AFMS — Automated Fastener Measurement System")
gr.Markdown("Upload an image containing fasteners and a **20mm reference square**.")
with gr.Row():
image_input = gr.Image(type="pil", label="Input Image")
output_json = gr.JSON(label="Results")
submit = gr.Button("Run Detection", variant="primary")
submit.click(fn=predict, inputs=[image_input], outputs=[output_json])
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)