Spaces:
Sleeping
Sleeping
Commit
·
3aab522
1
Parent(s):
c8dfd87
updated Dicom
Browse files
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 |
-
#
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
|
| 130 |
try:
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
#
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
# DICOM images are often 16-bit or higher, normalize to 8-bit for visualization
|
|
|
|
|
|
|
|
|
|
| 138 |
if img.dtype != np.uint8:
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 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
|
| 156 |
-
|
| 157 |
-
os.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 169 |
|
| 170 |
# Process as standard image format
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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():
|