Ani14 commited on
Commit
c9f8485
·
verified ·
1 Parent(s): 03b28f2

Update predict.py

Browse files
Files changed (1) hide show
  1. predict.py +188 -90
predict.py CHANGED
@@ -1,106 +1,204 @@
1
- from fastapi import FastAPI, File, UploadFile, Response
2
- from fastapi.responses import FileResponse
3
  from tensorflow.keras.preprocessing.image import img_to_array
4
  import tensorflow as tf
5
  import cv2
6
  import numpy as np
7
  import os
8
- from scipy import fftpack
9
- from scipy import ndimage
10
  from ultralytics import YOLO
11
  from PIL import Image
12
  import io
13
 
14
- app = FastAPI()
15
- uploads_dir = 'uploads'
 
 
 
 
16
 
17
- if not os.path.exists(uploads_dir):
18
- os.makedirs(uploads_dir)
 
 
 
 
19
 
20
- # Load the saved models
21
- segmentation_model_path = 'segmentation_model.h5'
22
- segmentation_model = tf.keras.models.load_model(segmentation_model_path)
23
- yolo_model_path = 'best.pt'
24
- yolo_model = YOLO(yolo_model_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- def calculate_moisture_and_texture(image):
27
  gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
28
- fft_image = fftpack.fft2(gray_image)
29
- fft_shifted = fftpack.fftshift(fft_image)
30
- magnitude_spectrum = 20 * np.log(np.abs(fft_shifted))
31
- height, width = magnitude_spectrum.shape
32
- center_x, center_y = width // 2, height // 2
33
- radius = min(center_x, center_y) // 2
34
- moisture_region = magnitude_spectrum[center_y - radius:center_y + radius, center_x - radius:center_x + radius]
35
- moisture_level = np.mean(moisture_region)
36
- return moisture_level
37
-
38
- def calculate_wound_dimensions(mask):
39
- labeled_mask, num_labels = ndimage.label(mask > 0.5)
40
- label_count = np.bincount(labeled_mask.ravel())
41
- wound_label = np.argmax(label_count[1:]) + 1
42
- wound_region = labeled_mask == wound_label
43
- rows = np.any(wound_region, axis=1)
44
- cols = np.any(wound_region, axis=0)
45
- rmin, rmax = np.where(rows)[0][[0, -1]]
46
- cmin, cmax = np.where(cols)[0][[0, -1]]
47
- length_pixels = rmax - rmin
48
- breadth_pixels = cmax - cmin
49
- pixel_to_cm_ratio = 0.1
50
- length_cm = length_pixels * pixel_to_cm_ratio
51
- breadth_cm = breadth_pixels * pixel_to_cm_ratio
52
- depth_cm = np.mean(mask[wound_region]) * pixel_to_cm_ratio
53
- length_cm = round(length_cm, 3)
54
- breadth_cm = round(breadth_cm, 3)
55
- depth_cm = round(depth_cm, 3)
56
- area_cm2 = length_cm * breadth_cm
57
- return length_cm, breadth_cm, depth_cm, area_cm2
58
 
59
  @app.post("/analyze_wound")
60
  async def analyze_wounds(file: UploadFile = File(...)):
61
- if file.filename.lower().endswith(('.png', '.jpg', '.jpeg')):
62
- contents = await file.read()
63
- nparr = np.fromstring(contents, np.uint8)
64
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
65
- results = yolo_model(img)
66
- combined_xmin = float('inf')
67
- combined_ymin = float('inf')
68
- combined_xmax = float('-inf')
69
- combined_ymax = float('-inf')
70
- for detection in results[0].boxes.xyxy.tolist():
71
- xmin, ymin, xmax, ymax = detection
72
- combined_xmin = min(combined_xmin, xmin)
73
- combined_ymin = min(combined_ymin, ymin)
74
- combined_xmax = max(combined_xmax, xmax)
75
- combined_ymax = max(combined_ymax, ymax)
76
- combined_xmin = int(combined_xmin)
77
- combined_ymin = int(combined_ymin)
78
- combined_xmax = int(combined_xmax)
79
- combined_ymax = int(combined_ymax)
80
- combined_img = img[combined_ymin:combined_ymax, combined_xmin:combined_xmax]
81
- combined_img_resized = cv2.resize(combined_img, (224, 224))
82
- img_array = img_to_array(combined_img_resized) / 255.0
83
- img_array = np.expand_dims(img_array, axis=0)
84
- output = segmentation_model.predict(img_array)
85
- predicted_mask = output[0]
86
- mask_overlay = (predicted_mask.squeeze() * 255).astype(np.uint8)
87
- mask_overlay_colored = np.zeros((mask_overlay.shape[0], mask_overlay.shape[1], 3), dtype=np.uint8)
88
- mask_overlay_colored[mask_overlay > 200] = [255, 0, 0] # Red
89
- mask_overlay_colored[(mask_overlay > 100) & (mask_overlay <= 200)] = [0, 255, 0] # Green
90
- mask_overlay_colored[mask_overlay <= 100] = [0, 0, 255] # Blue
91
- mask_overlay_colored = cv2.resize(mask_overlay_colored, (224, 224))
92
- blended_image = cv2.addWeighted(combined_img_resized.astype(np.uint8), 0.6, mask_overlay_colored, 0.4, 0)
93
- segmented_image = Image.fromarray(cv2.cvtColor(blended_image, cv2.COLOR_BGR2RGB))
94
- img_byte_arr = io.BytesIO()
95
- segmented_image.save(img_byte_arr, format='PNG')
96
- img_byte_arr.seek(0)
97
- length_cm, breadth_cm, depth_cm, area_cm2 = calculate_wound_dimensions(predicted_mask)
98
- moisture = calculate_moisture_and_texture(combined_img)
99
- response = Response(img_byte_arr.getvalue(), media_type='image/png')
100
- response.headers['X-Length-Cm'] = str(length_cm)
101
- response.headers['X-Breadth-Cm'] = str(breadth_cm)
102
- response.headers['X-Depth-Cm'] = str(depth_cm)
103
- response.headers['X-Area-Cm2'] = str(area_cm2)
104
- response.headers['X-Moisture'] = str(moisture)
105
- return response
106
- return {'error': 'Invalid file format'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Response
 
2
  from tensorflow.keras.preprocessing.image import img_to_array
3
  import tensorflow as tf
4
  import cv2
5
  import numpy as np
6
  import os
 
 
7
  from ultralytics import YOLO
8
  from PIL import Image
9
  import io
10
 
11
+ # --- Configuration ---
12
+ # IMPORTANT: Calibrate this value for accurate measurements.
13
+ # To calibrate: Take a photo of a ruler next to a wound. In an image editor,
14
+ # measure how many pixels correspond to 1 cm.
15
+ # Example: If 1 centimeter is 25 pixels long in your image, set PIXELS_PER_CM = 25.0.
16
+ PIXELS_PER_CM = 25.0
17
 
18
+ # --- App Initialization ---
19
+ app = FastAPI(
20
+ title="Wound Analysis API",
21
+ description="An API to analyze wound images and return an annotated image with data in headers.",
22
+ version="2.0.0"
23
+ )
24
 
25
+ UPLOADS_DIR = 'uploads'
26
+ if not os.path.exists(UPLOADS_DIR):
27
+ os.makedirs(UPLOADS_DIR)
28
+
29
+ # --- Model Loading ---
30
+ def load_models():
31
+ """Loads the machine learning models from disk."""
32
+ try:
33
+ # Ensure you have the correct paths to your model files
34
+ segmentation_model = tf.keras.models.load_model('segmentation_model.h5')
35
+ yolo_model = YOLO('best.pt')
36
+ print("Models loaded successfully.")
37
+ return segmentation_model, yolo_model
38
+ except Exception as e:
39
+ print(f"FATAL: Could not load models. Error: {e}")
40
+ return None, None
41
+
42
+ segmentation_model, yolo_model = load_models()
43
+
44
+ # --- Computer Vision and Analysis Functions ---
45
+
46
+ def detect_wound_with_yolo(img: np.ndarray):
47
+ """Detects wound using YOLO model and returns the combined bounding box."""
48
+ if yolo_model is None: return None
49
+ results = yolo_model(img)
50
+ boxes = results[0].boxes.xyxy.tolist()
51
+ if not boxes:
52
+ return None
53
+
54
+ combined_xmin = min(box[0] for box in boxes)
55
+ combined_ymin = min(box[1] for box in boxes)
56
+ combined_xmax = max(box[2] for box in boxes)
57
+ combined_ymax = max(box[3] for box in boxes)
58
+
59
+ return int(combined_xmin), int(combined_ymin), int(combined_xmax), int(combined_ymax)
60
+
61
+ def detect_wound_with_cv(img: np.ndarray):
62
+ """Fallback wound detection using traditional CV (HSV color-space analysis)."""
63
+ hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
64
+ lower_range = np.array([0, 40, 40])
65
+ upper_range = np.array([25, 255, 255])
66
+ mask = cv2.inRange(hsv_img, lower_range, upper_range)
67
+
68
+ kernel = np.ones((5, 5), np.uint8)
69
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
70
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
71
+
72
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
73
+ if not contours: return None
74
+
75
+ largest_contour = max(contours, key=cv2.contourArea)
76
+ if cv2.contourArea(largest_contour) < 150: return None
77
+
78
+ return cv2.boundingRect(largest_contour) # Returns (x, y, w, h)
79
+
80
+ def calculate_wound_dimensions(mask: np.ndarray):
81
+ """Calculates area, length, and breadth from a binary segmentation mask."""
82
+ binary_mask = (mask.squeeze() > 0.5).astype(np.uint8) * 255
83
+ contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
84
+ if not contours:
85
+ return 0, 0, 0
86
+
87
+ main_contour = max(contours, key=cv2.contourArea)
88
+
89
+ area_pixels = cv2.contourArea(main_contour)
90
+ area_cm2 = area_pixels / (PIXELS_PER_CM ** 2)
91
+
92
+ if len(main_contour) < 5:
93
+ x, y, w, h = cv2.boundingRect(main_contour)
94
+ length_px, breadth_px = max(w, h), min(w, h)
95
+ else:
96
+ rect = cv2.minAreaRect(main_contour)
97
+ (w, h) = rect[1]
98
+ length_px, breadth_px = max(w, h), min(w, h)
99
+
100
+ length_cm = length_px / PIXELS_PER_CM
101
+ breadth_cm = breadth_px / PIXELS_PER_CM
102
+
103
+ return round(area_cm2, 3), round(length_cm, 3), round(breadth_cm, 3)
104
+
105
+ def calculate_depth_estimate(image: np.ndarray, mask: np.ndarray):
106
+ """Estimates wound depth based on color intensity within the wound area."""
107
+ binary_mask = (mask.squeeze() > 0.5).astype(np.uint8)
108
+ if np.sum(binary_mask) == 0: return 0.0
109
+
110
+ wound_region = cv2.bitwise_and(image, image, mask=binary_mask)
111
+ lab_wound = cv2.cvtColor(wound_region, cv2.COLOR_BGR2LAB)
112
+ a_channel = lab_wound[:, :, 1]
113
+
114
+ redness_values = a_channel[binary_mask == 1]
115
+ if redness_values.size == 0: return 0.0
116
+
117
+ avg_redness = np.mean(redness_values)
118
+ depth_cm = np.interp(avg_redness, [128, 180], [0.1, 2.0])
119
+
120
+ return round(max(0, depth_cm), 3)
121
+
122
+ def calculate_moisture_level(image: np.ndarray, mask: np.ndarray):
123
+ """Calculates a moisture score based on texture analysis within the wound area."""
124
+ binary_mask = (mask.squeeze() > 0.5).astype(np.uint8)
125
+ if np.sum(binary_mask) == 0: return 0.0
126
 
 
127
  gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
128
+ wound_pixels = gray_image[binary_mask == 1]
129
+ if wound_pixels.size < 2: return 0.0
130
+
131
+ texture_metric = np.std(wound_pixels)
132
+ moisture_score = np.interp(texture_metric, [0, 60], [100, 0])
133
+
134
+ return round(np.clip(moisture_score, 0, 100), 3)
135
+
136
+ # --- API Endpoint ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  @app.post("/analyze_wound")
139
  async def analyze_wounds(file: UploadFile = File(...)):
140
+ if yolo_model is None or segmentation_model is None:
141
+ raise HTTPException(status_code=503, detail="Models are not loaded. The service is unavailable.")
142
+
143
+ if not file.filename.lower().endswith(('.png', '.jpg', '.jpeg')):
144
+ # Returning a dictionary for error is consistent with the original code's error path
145
+ return {'error': 'Invalid file format'}
146
+
147
+ contents = await file.read()
148
+ nparr = np.frombuffer(contents, np.uint8)
149
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
150
+ if img is None:
151
+ return {'error': 'Could not decode image file.'}
152
+
153
+ # 1. Detect Wound Bounding Box (YOLO with CV fallback)
154
+ bbox = detect_wound_with_yolo(img)
155
+ if bbox is None:
156
+ cv_bbox = detect_wound_with_cv(img)
157
+ if cv_bbox is None:
158
+ raise HTTPException(status_code=404, detail="Wound not detected.")
159
+ x, y, w, h = cv_bbox
160
+ bbox = (x, y, x + w, y + h)
161
+
162
+ xmin, ymin, xmax, ymax = bbox
163
+ cropped_img = img[ymin:ymax, xmin:xmax]
164
+ if cropped_img.size == 0:
165
+ raise HTTPException(status_code=400, detail="Wound detection resulted in an empty crop.")
166
+
167
+ # 2. Perform Segmentation
168
+ resized_for_model = cv2.resize(cropped_img, (224, 224))
169
+ img_array = img_to_array(resized_for_model) / 255.0
170
+ img_array = np.expand_dims(img_array, axis=0)
171
+ predicted_mask = segmentation_model.predict(img_array)[0]
172
+
173
+ # 3. Calculate Dimensions and Properties
174
+ area_cm2, length_cm, breadth_cm = calculate_wound_dimensions(predicted_mask)
175
+ depth_cm = calculate_depth_estimate(resized_for_model, predicted_mask)
176
+ moisture = calculate_moisture_level(resized_for_model, predicted_mask)
177
+
178
+ # 4. Create Visualization (as in original code)
179
+ mask_overlay = (predicted_mask.squeeze() * 255).astype(np.uint8)
180
+ # Using a colormap for better visualization of the segmentation mask
181
+ mask_overlay_colored = cv2.applyColorMap(mask_overlay, cv2.COLORMAP_JET)
182
+ blended_image = cv2.addWeighted(resized_for_model, 0.6, mask_overlay_colored, 0.4, 0)
183
+
184
+ segmented_image_pil = Image.fromarray(cv2.cvtColor(blended_image, cv2.COLOR_BGR2RGB))
185
+ img_byte_arr = io.BytesIO()
186
+ segmented_image_pil.save(img_byte_arr, format='PNG')
187
+ img_byte_arr.seek(0)
188
+
189
+ # 5. Create Response with headers (as in original code)
190
+ response = Response(img_byte_arr.getvalue(), media_type='image/png')
191
+ response.headers['X-Area-Cm2'] = str(area_cm2)
192
+ response.headers['X-Length-Cm'] = str(length_cm)
193
+ response.headers['X-Breadth-Cm'] = str(breadth_cm)
194
+ response.headers['X-Depth-Cm'] = str(depth_cm)
195
+ response.headers['X-Moisture'] = str(moisture)
196
+
197
+ return response
198
+
199
+ @app.get("/", include_in_schema=False)
200
+ async def root():
201
+ return {"message": "Welcome to the Wound Analysis API. Use the /analyze_wound endpoint to process an image."}
202
+
203
+ # To run this app, save it as `main.py` and execute:
204
+ # uvicorn main:app --reload