mobadara commited on
Commit
6231b6a
·
1 Parent(s): 25429d7

simulated A/B testing

Browse files
Files changed (2) hide show
  1. backend/ml_model.py +67 -1
  2. backend/routers/predict.py +16 -6
backend/ml_model.py CHANGED
@@ -58,6 +58,72 @@ except Exception as e:
58
 
59
 
60
  # Image processing transformation pipeline
 
 
 
 
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
-
 
 
 
 
 
 
58
 
59
 
60
  # Image processing transformation pipeline
61
+ transform_pipeline = transforms.Compose([
62
+ transforms.Resize((224, 224)),
63
+ transforms.ToTensor(),
64
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
65
+ ])
66
 
67
+ def run_diagnostic_inference(image_bytes: bytes) -> dict:
68
+ """
69
+ uns an image buffer through the DenseNet pipeline, generates diagnostic probabilities,
70
+ and constructs a visual Grad-CAM interpretability overlay.
71
+
72
+ Args:
73
+ image_bytes (bytes): The input image in bytes format.
74
+
75
+ Returns:
76
+ dict: A dictionary containing the predicted class, confidence score, and the Grad-CAM overlay image in bytes format.
77
+ """
78
+ if model is None:
79
+ raise RuntimeError('Model is not loaded. Cannot perform inference.')
80
+
81
+ # Open image from raw stream byte and convert to standard RGB format
82
+ image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
83
+ # Generate tensor batch for PyTorch model execution
84
+ tensor_img = transform_pipeline(image).unsqueeze(0).to(DEVICE)
85
+ # Perform class inference
86
+ with torch.no_grad():
87
+ outputs = model(tensor_img)
88
+ probabilities = torch.softmax(outputs, dim=1).cpu().numpy()[0]
89
+
90
+ # Class 0: Normal | Class 1: Pneumonia
91
+ normal_prob = float(probabilities[0])
92
+ pneumonia_prob = float(probabilities[1])
93
+
94
+ prediction_class = 'Pneumonia' if pneumonia_prob > normal_prob else 'Normal'
95
+ confidence_score = max(normal_prob, pneumonia_prob)
96
+
97
+ logging.info(f'Inference completed: Predicted class - {prediction_class}, Confidence score - {confidence_score:.4f}')
98
+
99
+ # Execute Grad-CAM calculation
100
+ # Target the last dense block layer of DenseNet-121 for tracking applications
101
+
102
+ target_layers = [model.features.denseblock4.denselayer16]
103
+ # Prepare standard float array of the image for overlay mapping
104
+ rgb_img_np = np.array(image.resize((224, 224)), dtype=np.float32) / 255.0
105
+
106
+ cam = GradCAM(model=model, target_layers=target_layers)
107
+
108
+ # Target the predicted class index specifically to see its activation layout
109
+ target_category = 1 if prediction_class == "Pneumonia" else 0
110
+ targets = [ClassifierOutputTarget(target_category)]
111
+
112
+ # Compute visual grayscale activation map
113
+ grayscale_cam = cam(input_tensor=tensor_img, targets=targets)[0, :]
114
+
115
+ # Blend the grayscale map into a colored heatmap directly onto our structural input image
116
+ cam_image_overlay = show_cam_on_image(rgb_img_np, grayscale_cam, use_rgb=True)
117
+
118
+ # Step C: Encode Heatmap into binary buffer for Cloudinary output conversion
119
+ # Convert RGB array back to standard BGR image array layout for OpenCV conversion
120
+ cam_image_bgr = cv2.cvtColor((cam_image_overlay * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
121
+ _, buffer = cv2.imencode('.jpg', cam_image_bgr)
122
+ heatmap_bytes = buffer.tobytes()
123
 
124
+ return {
125
+ 'prediction': prediction_class,
126
+ 'confidence': confidence_score,
127
+ 'probabilities': {'Normal': normal_prob, 'Pneumonia': pneumonia_prob},
128
+ 'heatmap_bytes': heatmap_bytes
129
+ }
backend/routers/predict.py CHANGED
@@ -1,5 +1,7 @@
1
  from fastapi import APIRouter, File, UploadFile, HTTPException
2
- from ..schema import PredictionResponse, DiagnosticRecordSchema
 
 
3
  from ..database import upload_image_to_cloud, diagnostic_records
4
  from ..ml_model import run_diagnostic_inference
5
 
@@ -12,15 +14,22 @@ router = APIRouter(
12
  # Enforce strict typing on the response using response_model
13
  @router.post('/predict', response_model=PredictionResponse)
14
  async def predict_xray(file: UploadFile = File(...)):
15
- '''
16
  Analyzes an uploaded X-ray, uploads to Cloudinary, saves to MongoDB,
17
  and returns strictly typed results.
18
- '''
19
  if not file.content_type.startswith('image/'):
20
  raise HTTPException(status_code=400, detail='Invalid file format. Please upload an image.')
21
 
22
  try:
23
  image_bytes = await file.read()
 
 
 
 
 
 
 
24
  ai_result = run_diagnostic_inference(image_bytes)
25
 
26
  original_url = upload_image_to_cloud(image_bytes, folder_name='raw_xrays')
@@ -32,7 +41,8 @@ async def predict_xray(file: UploadFile = File(...)):
32
  heatmap_image_url=heatmap_url,
33
  prediction=ai_result['prediction'],
34
  confidence_score=ai_result['confidence'],
35
- probabilities=ai_result['probabilities']
 
36
  )
37
 
38
  # model_dump() converts the Pydantic model safely to a dictionary for MongoDB
@@ -54,9 +64,9 @@ async def predict_xray(file: UploadFile = File(...)):
54
 
55
  @router.put('/override/{record_id}')
56
  async def submit_physician_override(record_id: str, override_data: OverrideRequest):
57
- '''
58
  Updates a specific diagnostic record with the physician's feedback.
59
- '''
60
  try:
61
  # Update the MongoDB document matching the ID
62
  update_result = await diagnostic_records.update_one(
 
1
  from fastapi import APIRouter, File, UploadFile, HTTPException
2
+ from bson import ObjectId
3
+ import random
4
+ from ..schema import PredictionResponse, DiagnosticRecordSchema, OverrideRequest
5
  from ..database import upload_image_to_cloud, diagnostic_records
6
  from ..ml_model import run_diagnostic_inference
7
 
 
14
  # Enforce strict typing on the response using response_model
15
  @router.post('/predict', response_model=PredictionResponse)
16
  async def predict_xray(file: UploadFile = File(...)):
17
+ """
18
  Analyzes an uploaded X-ray, uploads to Cloudinary, saves to MongoDB,
19
  and returns strictly typed results.
20
+ """
21
  if not file.content_type.startswith('image/'):
22
  raise HTTPException(status_code=400, detail='Invalid file format. Please upload an image.')
23
 
24
  try:
25
  image_bytes = await file.read()
26
+
27
+ # Select a model with given weights
28
+ assigned_model = random.choices(['DenseNet121_v1', 'ResNet50_Challenger'],
29
+ weights=[1, 0], # To train another model in the future
30
+ k=1)[0] # For now, we only have DenseNet121_v1, so it gets 100% of the traffic. This is where A/B testing logic would go in the future.
31
+
32
+
33
  ai_result = run_diagnostic_inference(image_bytes)
34
 
35
  original_url = upload_image_to_cloud(image_bytes, folder_name='raw_xrays')
 
41
  heatmap_image_url=heatmap_url,
42
  prediction=ai_result['prediction'],
43
  confidence_score=ai_result['confidence'],
44
+ probabilities=ai_result['probabilities'],
45
+ model_variant=assigned_model
46
  )
47
 
48
  # model_dump() converts the Pydantic model safely to a dictionary for MongoDB
 
64
 
65
  @router.put('/override/{record_id}')
66
  async def submit_physician_override(record_id: str, override_data: OverrideRequest):
67
+ """
68
  Updates a specific diagnostic record with the physician's feedback.
69
+ """
70
  try:
71
  # Update the MongoDB document matching the ID
72
  update_result = await diagnostic_records.update_one(