import gradio as gr from ultralytics import YOLO import cv2 import numpy as np from PIL import Image import tempfile import os import functools # ------------------- Load Model ------------------- @functools.lru_cache(maxsize=1) def load_model(): model_path = "best.pt" # file must exist in repo root or /app directory if not os.path.exists(model_path): raise FileNotFoundError("Model file 'best.pt' not found in the Space directory.") return YOLO(model_path) # Load once globally model = load_model() # ------------------- Wound Descriptions -------------------DO NOT chanage class name (It matched with data.yalm data set) wound_descriptions = { "wound_hesitation": "บาดแผลลังเล (Hesitation): มักพบในผู้พยายามทำร้ายตนเอง มีลักษณะเป็นแผลตื้นหลายแผลขนานหรือเกือบขนานกัน อยู่ใกล้แผลหลัก หรือบริเวณที่ทำตนเองได้ง่าย เช่น ข้อมือด้านใน", "wound_laceration": "บาดแผลฉีกขาดขอบไม่เรียบ (Laceration): เกิดจากวัตถุแข็งไม่มีคมกระแทก มีลักษณะขอบไม่เรียบ มีถลอกและฟกช้ำที่ขอบแผล และมี tissue bridging/undermining", "wound_open_fracture": "บาดแผลกระดูกหักแบบเปิด (Open fracture): เกิดจากกระดูกหักทิ่มออกมานอกผิวหนัง ในกรณีถูกรถชน สามารถประมวลเหตุการณ์ชนได้ (reconstruction)", "wound_burn": "บาดแผลไหม้ (Fire burn): บาดแผลที่เกิดจากการไหม้ ให้ดูความลึกและการกระจายของบาดแผล (pattern) ว่าสอดคล้องกับเหตุการณ์หรือไม่", "wound_hanging": "บาดแผลกดรัดบริเวณลำคอ (Hanging): เกิดจากน้ำหนักร่างกายทำให้เกิดบาดแผลกดรัดที่คอ โดยทั่วไปหากพบลักษณะการกดรัดเฉียงขึ้น จะเป็นลักษณะของ hanging ซึ่งต้องดูประกอบกับหลักฐานอื่น", "wound_strangulation": "บาดแผลกดรัดบริเวณลำคอ (Strangulation): เกิดจากแรงกระทำที่ไม่ใช่น้ำหนักร่างกาย ทำให้เกิดกดรัดที่คอ มีลักษณะการกดรัดแนวขวาง ซึ่งหากพบลักษณะบาดแผลกดรัดสองรูปแบบให้นึกถึงการฆาตกรรมอำพราง", "gsw_entrance": "บาดแผลทางเข้ากระสุนปืน (gunshot wound entrance): ลักษณะบาดแผลกระสุนปืนจะมีลักษณะเฉพาะ คือ punch-out lesion ซึ่งทางเข้าอาจพบองค์ประกอบการยิง เช่น เขม่าดินปืนดังภาพ (soot/gun powder tatooing)", "gsw_exit": "บาดแผลทางออกกระสุนปืน (gunshot wound exit): ลักษณะบาดแผลกระสุนปืนจะมีลักษณะเฉพาะ คือ punch-out lesion โดยทางออกจะไม่พบองค์ประกบการยิอง และโดยทั่วไปจะขนาดใหญ่กว่าทางเข้า อาจะมีรูปร่างแฉกคล้ายบาดแผลฉีกขาดขอบไม่เรียบ" } # ------------------- Detection Function ------------------- def detect(image, conf): if image is None: return None, "No image uploaded.", None img = np.array(image) img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # Convert RGB → BGR for YOLO results = model(img_bgr, conf=conf) annotated_bgr = results[0].plot() # returns BGR annotated_rgb = cv2.cvtColor(annotated_bgr, cv2.COLOR_BGR2RGB) detected_classes = set() for r in results[0].boxes.cls.cpu().numpy(): cls_name = results[0].names[int(r)] detected_classes.add(cls_name) # Add wound descriptions if detected_classes: desc_texts = [f"**{cls}**: {wound_descriptions.get(cls, '(No description available)')}" for cls in detected_classes] desc_str = "\n\n".join(desc_texts) else: desc_str = "No wounds detected." # Save annotated image for download temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png") Image.fromarray(annotated_rgb).save(temp_file.name) return annotated_rgb, desc_str, temp_file.name # ------------------- Footer ------------------- footer = """ ---
Forensic Education Version 2.0.0 | © 2025 BH
📄 User Manual | 👍 Feedback Please | 📚 more about forensic wounds
""" # Create main Gradio Interface demo = gr.Interface( fn=detect, inputs=[ gr.Image(type="pil", label="📸 Upload wound image"), gr.Slider(0, 1, 0.25, step=0.05, label="Confidence threshold"), ], outputs=[ gr.Image(label="Detection result"), gr.Markdown(label="Detected wound details"), gr.File(label="Download annotated image"), ], title="🤕 Forensic Wound Detection (YOLOv8n)", description="Upload an image to detect and describe forensic wound types using YOLOv8n. Model trained for educational forensic simulation.", flagging_mode="never" ) # Add footer below the app with gr.Blocks() as app: demo.render() gr.Markdown(footer) if __name__ == "__main__": app.launch()