thehammadishaq commited on
Commit
3aab522
·
1 Parent(s): c8dfd87

updated Dicom

Browse files
Files changed (1) hide show
  1. app.py +303 -29
app.py CHANGED
@@ -120,57 +120,316 @@ def process_predictions(predictions):
120
  decoded.append([(class_names[i], float(pred[i])) for i in top_indices])
121
  return decoded
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  def preprocess_dicom(file_bytes):
124
- """Process DICOM format images for the model."""
125
- # Save the bytes to a temporary file
126
- temp_file = "temp_dicom.dcm"
127
- with open(temp_file, "wb") as f:
128
- f.write(file_bytes)
 
 
129
 
130
  try:
131
- # Read the DICOM file
132
- dicom_data = pydicom.dcmread(temp_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- # Convert DICOM pixel data to numpy array
135
- img = dicom_data.pixel_array
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  # DICOM images are often 16-bit or higher, normalize to 8-bit for visualization
 
 
 
138
  if img.dtype != np.uint8:
139
- # Scale to [0, 255] for 8-bit representation
140
- img = img.astype(np.float32)
141
- img = (img - img.min()) / (img.max() - img.min()) * 255.0
142
- img = img.astype(np.uint8)
143
-
144
- # Check if grayscale and convert to RGB
145
- if len(img.shape) == 2:
146
- img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
147
- elif len(img.shape) == 3 and img.shape[2] == 1:
148
- img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
149
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  # Resize for model input
 
151
  img = cv2.resize(img, (540, 540), interpolation=cv2.INTER_AREA)
 
152
 
153
  return img
 
 
 
154
  finally:
155
- # Clean up temporary file
156
- if os.path.exists(temp_file):
157
- os.remove(temp_file)
 
 
 
 
158
 
159
  def preprocess_image(file_bytes, content_type=None):
160
  """Process images for the model, handling both DICOM and standard formats."""
 
 
 
 
 
161
  # Check if the file is a DICOM file
162
- if content_type and ('dicom' in content_type.lower() or content_type.lower() == 'application/octet-stream'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  try:
164
  return preprocess_dicom(file_bytes)
165
  except Exception as e:
166
  print(f"DICOM processing error: {e}")
167
  # Fall back to standard image processing if DICOM processing fails
168
- pass
169
 
170
  # Process as standard image format
171
- img = cv2.imdecode(np.frombuffer(file_bytes, np.uint8), cv2.IMREAD_COLOR)
172
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
173
- return cv2.resize(img, (540, 540), interpolation=cv2.INTER_AREA)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  @app.post("/analyze")
176
  @limiter.limit("5/minute")
@@ -211,7 +470,22 @@ async def analyze_image(
211
  "heatmap_format": "base64 encoded PNG"
212
  }
213
  except Exception as e:
214
- raise HTTPException(500, f"Analysis failed: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  @app.get("/health")
217
  async def health_check():
 
120
  decoded.append([(class_names[i], float(pred[i])) for i in top_indices])
121
  return decoded
122
 
123
+ def dump_file_sample(file_bytes, filename="debug_file_sample.bin"):
124
+ """Save a sample of file bytes for debugging"""
125
+ try:
126
+ sample_size = min(512, len(file_bytes))
127
+ with open(filename, "wb") as f:
128
+ f.write(file_bytes[:sample_size])
129
+ print(f"Saved {sample_size} bytes sample to {filename}")
130
+
131
+ # Try to print first few bytes as hex
132
+ hex_sample = ' '.join([f'{b:02x}' for b in file_bytes[:16]])
133
+ print(f"First 16 bytes: {hex_sample}")
134
+ except Exception as e:
135
+ print(f"Failed to save debug sample: {e}")
136
+
137
  def preprocess_dicom(file_bytes):
138
+ """Process DICOM format images for the model with robust error handling."""
139
+ # Create unique temporary filenames to avoid conflicts
140
+ import tempfile
141
+ temp_dir = tempfile.gettempdir()
142
+ uid = str(uuid.uuid4())[:8]
143
+ temp_file = os.path.join(temp_dir, f"temp_dicom_{uid}.dcm")
144
+ temp_img_file = os.path.join(temp_dir, f"temp_dicom_img_{uid}.png")
145
 
146
  try:
147
+ print(f"Processing DICOM file of size {len(file_bytes)} bytes")
148
+
149
+ # Write bytes to temporary file
150
+ with open(temp_file, "wb") as f:
151
+ f.write(file_bytes)
152
+
153
+ # Read the DICOM file with force=True to ignore errors
154
+ try:
155
+ # Use defer_size=True to avoid reading large data elements
156
+ # until explicitly accessed
157
+ dicom_data = pydicom.dcmread(temp_file, force=True, defer_size=None)
158
+
159
+ # Check transfer syntax
160
+ if hasattr(dicom_data, 'file_meta') and hasattr(dicom_data.file_meta, 'TransferSyntaxUID'):
161
+ ts_uid = str(dicom_data.file_meta.TransferSyntaxUID)
162
+ print(f"DICOM file read successfully. Transfer syntax: {ts_uid}")
163
+ else:
164
+ print("DICOM file read but no transfer syntax found - assuming default Implicit VR Little Endian")
165
+ except Exception as e:
166
+ print(f"Error reading DICOM file: {e}")
167
+ raise ValueError(f"Failed to read DICOM file: {e}")
168
 
169
+ # Verify pixel data exists
170
+ if not hasattr(dicom_data, 'PixelData'):
171
+ print("PixelData attribute missing")
172
+ # Try to check for alternate pixel data representations
173
+ alt_pixel_attrs = ['FloatPixelData', 'DoubleFloatPixelData']
174
+ has_pixel_data = False
175
+ for attr in alt_pixel_attrs:
176
+ if hasattr(dicom_data, attr):
177
+ has_pixel_data = True
178
+ print(f"Found alternate pixel data: {attr}")
179
+ break
180
+
181
+ if not has_pixel_data:
182
+ raise ValueError("DICOM file does not contain any pixel data")
183
+
184
+ # Print DICOM image properties for diagnosis
185
+ print(f"DICOM properties:")
186
+ for attr in ['BitsAllocated', 'BitsStored', 'HighBit', 'SamplesPerPixel', 'Rows', 'Columns']:
187
+ if hasattr(dicom_data, attr):
188
+ print(f" {attr}: {getattr(dicom_data, attr)}")
189
+ else:
190
+ print(f" {attr}: Not specified")
191
+
192
+ # Algorithm to try multiple methods to extract pixel data
193
+ img = None
194
+ methods_tried = []
195
+
196
+ # Method 1: Direct pixel_array access with exception handling
197
+ if img is None:
198
+ try:
199
+ methods_tried.append("Direct pixel_array")
200
+ img = dicom_data.pixel_array
201
+ if img.size > 0:
202
+ print(f"Successfully extracted pixel data via pixel_array: shape={img.shape}, dtype={img.dtype}")
203
+ else:
204
+ img = None
205
+ raise ValueError("Extracted pixel array is empty")
206
+ except Exception as e:
207
+ print(f"Method 1 (direct pixel_array) failed: {e}")
208
+ img = None
209
+
210
+ # Method 2: Save and reload through PNG for compressed images
211
+ if img is None:
212
+ try:
213
+ methods_tried.append("PNG intermediate")
214
+ print("Trying PNG intermediate method...")
215
+ dicom_data.save_as(temp_img_file)
216
+
217
+ # Try with IMREAD_UNCHANGED first to preserve bit depth
218
+ img = cv2.imread(temp_img_file, cv2.IMREAD_UNCHANGED)
219
+ if img is None or img.size == 0:
220
+ # Fall back to IMREAD_GRAYSCALE
221
+ img = cv2.imread(temp_img_file, cv2.IMREAD_GRAYSCALE)
222
+
223
+ if img is not None and img.size > 0:
224
+ print(f"Successfully extracted pixel data via PNG: shape={img.shape}, dtype={img.dtype}")
225
+ else:
226
+ img = None
227
+ raise ValueError("PNG conversion resulted in empty image")
228
+ except Exception as e:
229
+ print(f"Method 2 (PNG intermediate) failed: {e}")
230
+ img = None
231
+
232
+ # Method 3: PIL intermediate
233
+ if img is None:
234
+ try:
235
+ methods_tried.append("PIL intermediate")
236
+ print("Trying PIL intermediate method...")
237
+ from PIL import Image
238
+ dicom_data.save_as(temp_img_file)
239
+ pil_img = Image.open(temp_img_file)
240
+ img = np.array(pil_img)
241
+
242
+ if img is not None and img.size > 0:
243
+ print(f"Successfully extracted pixel data via PIL: shape={img.shape}, dtype={img.dtype}")
244
+ else:
245
+ img = None
246
+ raise ValueError("PIL conversion resulted in empty image")
247
+ except Exception as e:
248
+ print(f"Method 3 (PIL intermediate) failed: {e}")
249
+ img = None
250
+
251
+ # If all methods failed, create a diagnostic image
252
+ if img is None:
253
+ print(f"All pixel data extraction methods failed: {', '.join(methods_tried)}")
254
+ # Create a diagnostic image
255
+ img = np.ones((540, 540), dtype=np.uint8) * 128
256
+ # Add text about the error
257
+ img_with_text = np.ones((540, 540, 3), dtype=np.uint8) * 128
258
+ error_text = "Failed to extract DICOM pixel data"
259
+ cv2.putText(img_with_text, error_text, (50, 270), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
260
+ # Return the diagnostic image
261
+ print("Returning diagnostic image due to extraction failure")
262
+ return img_with_text
263
 
264
  # DICOM images are often 16-bit or higher, normalize to 8-bit for visualization
265
+ print(f"Original image: shape={img.shape}, dtype={img.dtype}, min={np.min(img)}, max={np.max(img)}")
266
+
267
+ # 1. Normalize pixel values to 8-bit range
268
  if img.dtype != np.uint8:
269
+ try:
270
+ # Calculate data range for proper normalization
271
+ img_min = float(np.min(img))
272
+ img_max = float(np.max(img))
273
+
274
+ # Only normalize if we have a non-zero range
275
+ if img_max > img_min:
276
+ # Convert to float32 first for better precision
277
+ img = img.astype(np.float32)
278
+ # Scale to range [0, 255]
279
+ img = 255.0 * (img - img_min) / (img_max - img_min)
280
+ # Convert to uint8
281
+ img = img.astype(np.uint8)
282
+ print(f"Normalized to 8-bit: new range=[{np.min(img)}, {np.max(img)}]")
283
+ else:
284
+ # Handle uniform pixel values
285
+ img = np.full(img.shape, 128, dtype=np.uint8)
286
+ print("Image has uniform pixel values, using mid-gray")
287
+ except Exception as e:
288
+ print(f"Error during normalization: {e}")
289
+ # Create a valid grayscale image in case of error
290
+ img = np.full(img.shape if len(img.shape) >= 2 else (540, 540), 128, dtype=np.uint8)
291
+
292
+ # 2. Handle color conversion based on image dimensions
293
+ try:
294
+ # Check image dimensions
295
+ if len(img.shape) == 2:
296
+ # Single channel (grayscale) image - convert to 3-channel
297
+ print("Converting grayscale to RGB using manual conversion")
298
+ h, w = img.shape
299
+ rgb_img = np.zeros((h, w, 3), dtype=np.uint8)
300
+ rgb_img[:, :, 0] = img # R
301
+ rgb_img[:, :, 1] = img # G
302
+ rgb_img[:, :, 2] = img # B
303
+ img = rgb_img
304
+ elif len(img.shape) == 3:
305
+ if img.shape[2] == 1:
306
+ # Single channel image in 3D array
307
+ print("Converting single-channel 3D array to RGB")
308
+ h, w, _ = img.shape
309
+ img_2d = img.reshape(h, w)
310
+ rgb_img = np.zeros((h, w, 3), dtype=np.uint8)
311
+ rgb_img[:, :, 0] = img_2d
312
+ rgb_img[:, :, 1] = img_2d
313
+ rgb_img[:, :, 2] = img_2d
314
+ img = rgb_img
315
+ elif img.shape[2] == 3:
316
+ # Already RGB, make sure it's the right color space
317
+ print("Image already has 3 channels, ensuring RGB color space")
318
+ # No conversion needed if already RGB
319
+ elif img.shape[2] == 4:
320
+ # RGBA image - remove alpha channel
321
+ print("Converting RGBA to RGB by removing alpha channel")
322
+ img = img[:, :, :3]
323
+ else:
324
+ # Unusual number of channels, convert to grayscale then RGB
325
+ print(f"Unusual channel count ({img.shape[2]}), converting to grayscale then RGB")
326
+ if np.max(img) > 0: # Avoid division by zero
327
+ # Average across channels and normalize
328
+ gray = np.mean(img, axis=2).astype(np.uint8)
329
+ h, w = gray.shape
330
+ rgb_img = np.zeros((h, w, 3), dtype=np.uint8)
331
+ rgb_img[:, :, 0] = gray
332
+ rgb_img[:, :, 1] = gray
333
+ rgb_img[:, :, 2] = gray
334
+ img = rgb_img
335
+ else:
336
+ # Create a valid RGB image if all pixels are zero
337
+ h, w = img.shape[:2]
338
+ img = np.full((h, w, 3), 128, dtype=np.uint8)
339
+ else:
340
+ # Invalid dimensions, create fallback image
341
+ print(f"Invalid image dimensions: {img.shape}")
342
+ img = np.full((540, 540, 3), 128, dtype=np.uint8)
343
+ cv2.putText(img, "Invalid image dimensions", (50, 270),
344
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
345
+ except Exception as e:
346
+ print(f"Error during color conversion: {e}")
347
+ # Create a valid RGB image in case of error
348
+ img = np.full((540, 540, 3), 128, dtype=np.uint8)
349
+ cv2.putText(img, f"Error: {str(e)[:30]}", (50, 270),
350
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
351
+
352
+ # 3. Add final validation and cleanup
353
+ print(f"After color conversion: shape={img.shape}, dtype={img.dtype}")
354
+
355
+ # Final validation
356
+ if img is None or img.size == 0 or len(img.shape) < 2:
357
+ raise ValueError("Image processing resulted in invalid image")
358
+
359
  # Resize for model input
360
+ print(f"Final image shape before resize: {img.shape}")
361
  img = cv2.resize(img, (540, 540), interpolation=cv2.INTER_AREA)
362
+ print(f"Resized image shape: {img.shape}")
363
 
364
  return img
365
+ except Exception as e:
366
+ print(f"DICOM processing failed: {e}")
367
+ raise
368
  finally:
369
+ # Clean up temporary files
370
+ for temp_file_path in [temp_file, temp_img_file]:
371
+ if os.path.exists(temp_file_path):
372
+ try:
373
+ os.remove(temp_file_path)
374
+ except Exception as e:
375
+ print(f"Failed to remove temporary file {temp_file_path}: {e}")
376
 
377
  def preprocess_image(file_bytes, content_type=None):
378
  """Process images for the model, handling both DICOM and standard formats."""
379
+ print(f"Preprocessing image with content type: {content_type}, size: {len(file_bytes)} bytes")
380
+
381
+ # Save a debug sample of the file bytes
382
+ dump_file_sample(file_bytes)
383
+
384
  # Check if the file is a DICOM file
385
+ is_likely_dicom = False
386
+
387
+ # Check content type for DICOM indicators
388
+ if content_type and ('dicom' in content_type.lower() or
389
+ content_type.lower() == 'application/octet-stream' or
390
+ content_type.lower() == 'application/dicom'):
391
+ is_likely_dicom = True
392
+
393
+ # Also check file signature (DICOM files usually start with "DICM" at byte offset 128)
394
+ if len(file_bytes) > 132:
395
+ dicom_signature = file_bytes[128:132]
396
+ if dicom_signature == b'DICM':
397
+ is_likely_dicom = True
398
+ print("DICOM signature detected in file")
399
+
400
+ if is_likely_dicom:
401
  try:
402
  return preprocess_dicom(file_bytes)
403
  except Exception as e:
404
  print(f"DICOM processing error: {e}")
405
  # Fall back to standard image processing if DICOM processing fails
406
+ print("Falling back to standard image processing")
407
 
408
  # Process as standard image format
409
+ try:
410
+ print("Processing as standard image format")
411
+ img = cv2.imdecode(np.frombuffer(file_bytes, np.uint8), cv2.IMREAD_COLOR)
412
+
413
+ # Validate image was successfully decoded
414
+ if img is None or img.size == 0:
415
+ print("Standard image decoding failed - creating fallback image")
416
+ # Create a fallback image for debugging
417
+ img = np.ones((540, 540, 3), dtype=np.uint8) * 128
418
+ # Add diagnostic pattern
419
+ cv2.line(img, (0, 0), (540, 540), (200, 100, 100), 10)
420
+ cv2.line(img, (540, 0), (0, 540), (100, 200, 100), 10)
421
+ return img
422
+
423
+ # If we got a valid image, proceed with color conversion
424
+ print(f"Standard image decoded successfully: shape={img.shape}, dtype={img.dtype}")
425
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
426
+ return cv2.resize(img, (540, 540), interpolation=cv2.INTER_AREA)
427
+ except Exception as e:
428
+ print(f"Standard image processing error: {e}")
429
+ # Create fallback image as last resort
430
+ img = np.ones((540, 540, 3), dtype=np.uint8) * 128
431
+ cv2.putText(img, "Error: " + str(e)[:30], (50, 270), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
432
+ return img
433
 
434
  @app.post("/analyze")
435
  @limiter.limit("5/minute")
 
470
  "heatmap_format": "base64 encoded PNG"
471
  }
472
  except Exception as e:
473
+ error_message = str(e)
474
+ print(f"Analysis failed with error: {error_message}")
475
+
476
+ # Return a more detailed error message
477
+ if "empty()" in error_message and "cvtColor" in error_message:
478
+ raise HTTPException(
479
+ status_code=500,
480
+ detail=f"Failed to process image: The image data is empty or corrupt. Please check your DICOM file format. Original error: {error_message}"
481
+ )
482
+ elif "DICOM" in error_message:
483
+ raise HTTPException(
484
+ status_code=422,
485
+ detail=f"DICOM processing error: {error_message}. Please ensure your DICOM file contains valid pixel data."
486
+ )
487
+ else:
488
+ raise HTTPException(500, f"Analysis failed: {error_message}")
489
 
490
  @app.get("/health")
491
  async def health_check():