zazaman commited on
Commit
3470407
·
verified ·
1 Parent(s): fdd1c58

Patch Space for WoundDoc mobile app API integration

Browse files
Files changed (5) hide show
  1. Dockerfile +18 -0
  2. README.md +4 -4
  3. app.py +191 -0
  4. requirements.txt +9 -0
  5. wound_segmentation_model.h5 +3 -0
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /code
4
+
5
+ # Install system dependencies for OpenCV
6
+ RUN apt-get update && apt-get install -y \
7
+ libgl1 \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY ./requirements.txt /code/requirements.txt
12
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
13
+
14
+ COPY . /code/
15
+
16
+ EXPOSE 7860
17
+
18
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: Woundsized
3
- emoji:
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
 
1
  ---
2
+ title: Wound Size Analysis
3
+ emoji: 📏
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  ---
app.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import os
4
+ import re
5
+ import uuid
6
+ from typing import Any, Dict
7
+
8
+ import cv2
9
+ import gradio as gr
10
+ import numpy as np
11
+ import tensorflow as tf
12
+ from fastapi import FastAPI, HTTPException
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import RedirectResponse
15
+ from PIL import Image
16
+ from pydantic import BaseModel
17
+
18
+ # --- CONFIGURATION ---
19
+ MODEL_PATH = "wound_segmentation_model.h5"
20
+ IMG_HEIGHT = 256
21
+ IMG_WIDTH = 256
22
+
23
+ # --- ARUCO SETTINGS ---
24
+ ARUCO_DICT_TYPE = cv2.aruco.DICT_4X4_50
25
+ MARKER_SIZE_CM = 2.0 # Assumes a 2x2 cm marker
26
+
27
+ # --- LEGACY FALLBACK SETTINGS ---
28
+ LOWER_BLUE = np.array([95, 60, 100])
29
+ UPPER_BLUE = np.array([120, 255, 255])
30
+
31
+ TISSUE_TYPES = [
32
+ {'name': 'Eschar / Necrotic Tissue', 'display_name': 'eschar_necrotic', 'id': 1, 'color': (128, 128, 128), 'lower': np.array([0, 0, 0]), 'upper': np.array([179, 255, 67])},
33
+ {'name': 'Healthy Granulation Tissue', 'display_name': 'healthy_granulation', 'id': 5, 'color': (0, 0, 255), 'lower': np.array([0, 205, 100]), 'upper': np.array([10, 255, 200])},
34
+ {'name': 'Infected / Pseudomonas', 'display_name': 'infected', 'id': 11, 'color': (0, 255, 0), 'lower': np.array([20, 105, 0]), 'upper': np.array([40, 205, 150])},
35
+ {'name': 'Slough / Fibrinous', 'display_name': 'slough', 'id': 25, 'color': (255, 255, 0), 'lower': np.array([50, 55, 150]), 'upper': np.array([70, 155, 255])},
36
+ {'name': 'Epithelializing Tissue', 'display_name': 'epithelializing', 'id': 29, 'color': (255, 0, 255), 'lower': np.array([140, 1, 203]), 'upper': np.array([160, 101, 255])},
37
+ ]
38
+
39
+ # --- HELPERS ---
40
+ def iou(y_true, y_pred, smooth=1e-6):
41
+ y_true_f = tf.keras.backend.flatten(y_true)
42
+ y_pred_f = tf.keras.backend.flatten(y_pred)
43
+ intersection = tf.keras.backend.sum(y_true_f * y_pred_f)
44
+ union = tf.keras.backend.sum(y_true_f) + tf.keras.backend.sum(y_pred_f) - intersection
45
+ return (intersection + smooth) / (union + smooth)
46
+
47
+ # --- MODEL LOADING ---
48
+ try:
49
+ if os.path.exists(MODEL_PATH):
50
+ model = tf.keras.models.load_model(MODEL_PATH, custom_objects={"iou": iou})
51
+ print("--- Segmentation model loaded. ---")
52
+ else:
53
+ model = None
54
+ print(f"--- WARNING: Model not found at {MODEL_PATH} ---")
55
+ except Exception as e:
56
+ model = None
57
+ print(f"--- ERROR: {e} ---")
58
+
59
+ def calculate_wound_size_analysis(input_image_np: np.ndarray) -> Dict[str, Any]:
60
+ if model is None:
61
+ return {"status": "error", "message": "Model not loaded."}
62
+
63
+ original_img = cv2.cvtColor(input_image_np, cv2.COLOR_RGB2BGR)
64
+ img = cv2.resize(original_img, (IMG_WIDTH, IMG_HEIGHT))
65
+ img_array = np.expand_dims(img, axis=0) / 255.0
66
+
67
+ predicted_mask = model.predict(img_array, verbose=0)[0]
68
+ predicted_mask_binary = (predicted_mask > 0.5).astype(np.uint8) * 255
69
+ predicted_mask_resized = cv2.resize(
70
+ predicted_mask_binary,
71
+ (original_img.shape[1], original_img.shape[0]),
72
+ )
73
+
74
+ if cv2.countNonZero(predicted_mask_resized) == 0:
75
+ return {"status": "error", "message": "No wound detected."}
76
+
77
+ total_area_cm2 = 0.0
78
+ ref_contour = None
79
+ applied_perspective = False
80
+
81
+ # 1. ARUCO DETECTION
82
+ try:
83
+ dictionary = cv2.aruco.getPredefinedDictionary(ARUCO_DICT_TYPE)
84
+ parameters = cv2.aruco.DetectorParameters()
85
+ detector = cv2.aruco.ArucoDetector(dictionary, parameters)
86
+ corners, ids, _ = detector.detectMarkers(original_img)
87
+
88
+ if ids is not None and len(ids) > 0:
89
+ src_pts = corners[0][0].astype(np.float32)
90
+ side = 100.0
91
+ dst_pts = np.array([[0,0],[side,0],[side,side],[0,side]], dtype=np.float32)
92
+ M = cv2.getPerspectiveTransform(src_pts, dst_pts)
93
+ warped_mask = cv2.warpPerspective(predicted_mask_resized, M, (1000, 1000))
94
+
95
+ pixel_width_cm = MARKER_SIZE_CM / side
96
+ pixels_per_cm2 = (1.0 / pixel_width_cm) ** 2
97
+ total_area_cm2 = float(cv2.countNonZero(warped_mask) / pixels_per_cm2)
98
+ applied_perspective = True
99
+ ref_contour = src_pts.astype(np.int32).reshape((-1, 1, 2))
100
+ except Exception as e:
101
+ print(f"ArUco error: {e}")
102
+
103
+ # 2. BLUE SQUARE FALLBACK
104
+ if not applied_perspective:
105
+ try:
106
+ hsv = cv2.cvtColor(cv2.GaussianBlur(original_img, (5,5), 0), cv2.COLOR_BGR2HSV)
107
+ blue_mask = cv2.inRange(hsv, LOWER_BLUE, UPPER_BLUE)
108
+ blue_mask = cv2.morphologyEx(blue_mask, cv2.MORPH_OPEN, np.ones((5,5)))
109
+ contours, _ = cv2.findContours(blue_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
110
+ if contours:
111
+ ref_contour = max(contours, key=cv2.contourArea)
112
+ pixels_per_cm2 = cv2.contourArea(ref_contour) / 4.0
113
+ total_area_cm2 = float(cv2.countNonZero(predicted_mask_resized) / pixels_per_cm2)
114
+ except Exception as e:
115
+ print(f"Fallback error: {e}")
116
+
117
+ # 3. TISSUE SECTION ANALYSIS
118
+ overlay_img = original_img.copy()
119
+ wound_only_img = cv2.bitwise_and(original_img, original_img, mask=predicted_mask_resized)
120
+ hsv_wound = cv2.cvtColor(wound_only_img, cv2.COLOR_BGR2HSV)
121
+
122
+ tissue_areas = {}
123
+ total_wound_pixels = cv2.countNonZero(predicted_mask_resized)
124
+
125
+ for tissue in TISSUE_TYPES:
126
+ color_mask = cv2.inRange(hsv_wound, tissue["lower"], tissue["upper"])
127
+ tissue_mask = cv2.bitwise_and(color_mask, color_mask, mask=predicted_mask_resized)
128
+
129
+ pixel_count = cv2.countNonZero(tissue_mask)
130
+ percentage = (pixel_count / total_wound_pixels) if total_wound_pixels > 0 else 0
131
+
132
+ # Calculate absolute area
133
+ area_cm2 = round(total_area_cm2 * percentage, 3)
134
+ if area_cm2 > 0:
135
+ tissue_areas[tissue['name']] = {
136
+ "area_cm2": area_cm2,
137
+ "percentage": round(percentage * 100, 1)
138
+ }
139
+
140
+ # Draw for overlay
141
+ cnts, _ = cv2.findContours(tissue_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
142
+ cv2.drawContours(overlay_img, cnts, -1, tissue["color"], 2)
143
+
144
+ # 4. FINAL OVERLAY
145
+ if ref_contour is not None:
146
+ color = (0, 0, 255) if applied_perspective else (0, 255, 0)
147
+ cv2.drawContours(overlay_img, [ref_contour], -1, color, 3)
148
+
149
+ final_rgb = cv2.cvtColor(overlay_img, cv2.COLOR_BGR2RGB)
150
+
151
+ return {
152
+ "status": "success",
153
+ "total_area_cm2": round(total_area_cm2, 2),
154
+ "measurement_method": "ArUco (Perspective Corrected)" if applied_perspective else "Blue Square Fallback",
155
+ "tissue_sections": tissue_areas,
156
+ "overlay": final_rgb
157
+ }
158
+
159
+ def gradio_fn(img):
160
+ if img is None: return None, "Please upload an image."
161
+ res = calculate_wound_size_analysis(img)
162
+ if res["status"] == "error": return None, res["message"]
163
+
164
+ sections_text = f"### Total Wound Area: {res['total_area_cm2']} cm²\n"
165
+ sections_text += f"**Method:** {res['measurement_method']}\n\n"
166
+ sections_text += "| Tissue Type | Area (cm²) | Percentage |\n| :--- | :--- | :--- |\n"
167
+ for name, data in res["tissue_sections"].items():
168
+ sections_text += f"| {name} | {data['area_cm2']} | {data['percentage']}% |\n"
169
+
170
+ return res["overlay"], sections_text
171
+
172
+ # Create Gradio Interface
173
+ demo = gr.Interface(
174
+ fn=gradio_fn,
175
+ inputs=gr.Image(label="Upload Wound Image with ArUco Marker"),
176
+ outputs=[
177
+ gr.Image(label="Analysis Overlay"),
178
+ gr.Markdown(label="Size & Section Report")
179
+ ],
180
+ title="Wound Size & Section Analyzer",
181
+ description="Upload a photo with a 2x2cm ArUco marker (DICT_4X4_50). The tool will correct perspective and calculate absolute areas for each wound section."
182
+ )
183
+
184
+ # Initialize FastAPI and Mount Gradio
185
+ app = FastAPI()
186
+ demo.queue()
187
+ app = gr.mount_gradio_app(app, demo, path="/")
188
+
189
+ if __name__ == "__main__":
190
+ import uvicorn
191
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==4.44.1
2
+ fastapi>=0.110.0
3
+ pydantic>=2.0.0
4
+ opencv-python-headless>=4.8.0.76
5
+ numpy<2
6
+ Pillow
7
+ tensorflow==2.15.0
8
+ uvicorn
9
+ huggingface_hub==0.23.5
wound_segmentation_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79aaba3bd74139da9b53a11a81854f21cc32f5e63884abaa194867270e974cfa
3
+ size 2998256