Abs6187 commited on
Commit
e64dd2d
·
verified ·
1 Parent(s): b2adcbf

Upload 7 files

Browse files
Files changed (7) hide show
  1. .gitattributes +53 -53
  2. Dockerfile +27 -0
  3. README.md +8 -13
  4. app.py +413 -0
  5. report_analyzer_app.py +120 -0
  6. requirements.txt +13 -0
  7. train.py +65 -0
.gitattributes CHANGED
@@ -1,53 +1,53 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- testimages/003f0afdcd15.png filter=lfs diff=lfs merge=lfs -text
37
- testimages/005b95c28852.png filter=lfs diff=lfs merge=lfs -text
38
- testimages/dyedlifted1.jpg filter=lfs diff=lfs merge=lfs -text
39
- testimages/dyedlifted2.jpg filter=lfs diff=lfs merge=lfs -text
40
- testimages/dyedresection1.jpg filter=lfs diff=lfs merge=lfs -text
41
- testimages/dyedresection2.jpg filter=lfs diff=lfs merge=lfs -text
42
- testimages/esophagitis1.jpg filter=lfs diff=lfs merge=lfs -text
43
- testimages/esophagitis2.jpg filter=lfs diff=lfs merge=lfs -text
44
- testimages/normalceacum1.jpg filter=lfs diff=lfs merge=lfs -text
45
- testimages/normalceacum2.jpg filter=lfs diff=lfs merge=lfs -text
46
- testimages/normalpylorus1.jpg filter=lfs diff=lfs merge=lfs -text
47
- testimages/normalpylorus2.jpg filter=lfs diff=lfs merge=lfs -text
48
- testimages/normalzline1.jpg filter=lfs diff=lfs merge=lfs -text
49
- testimages/normalzline2.jpg filter=lfs diff=lfs merge=lfs -text
50
- testimages/polypus1.jpg filter=lfs diff=lfs merge=lfs -text
51
- testimages/polypus2.jpg filter=lfs diff=lfs merge=lfs -text
52
- testimages/ulcerative1.jpg filter=lfs diff=lfs merge=lfs -text
53
- testimages/ulcerative2.jpg filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ testimages/003f0afdcd15.png filter=lfs diff=lfs merge=lfs -text
37
+ testimages/005b95c28852.png filter=lfs diff=lfs merge=lfs -text
38
+ testimages/dyedlifted1.jpg filter=lfs diff=lfs merge=lfs -text
39
+ testimages/dyedlifted2.jpg filter=lfs diff=lfs merge=lfs -text
40
+ testimages/dyedresection1.jpg filter=lfs diff=lfs merge=lfs -text
41
+ testimages/dyedresection2.jpg filter=lfs diff=lfs merge=lfs -text
42
+ testimages/esophagitis1.jpg filter=lfs diff=lfs merge=lfs -text
43
+ testimages/esophagitis2.jpg filter=lfs diff=lfs merge=lfs -text
44
+ testimages/normalceacum1.jpg filter=lfs diff=lfs merge=lfs -text
45
+ testimages/normalceacum2.jpg filter=lfs diff=lfs merge=lfs -text
46
+ testimages/normalpylorus1.jpg filter=lfs diff=lfs merge=lfs -text
47
+ testimages/normalpylorus2.jpg filter=lfs diff=lfs merge=lfs -text
48
+ testimages/normalzline1.jpg filter=lfs diff=lfs merge=lfs -text
49
+ testimages/normalzline2.jpg filter=lfs diff=lfs merge=lfs -text
50
+ testimages/polypus1.jpg filter=lfs diff=lfs merge=lfs -text
51
+ testimages/polypus2.jpg filter=lfs diff=lfs merge=lfs -text
52
+ testimages/ulcerative1.jpg filter=lfs diff=lfs merge=lfs -text
53
+ testimages/ulcerative2.jpg filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a Python 3.10 runtime
2
+ FROM python:3.10-slim
3
+
4
+ # Install Tesseract OCR Engine and other required system dependencies
5
+ # (Keeping this in case you switch back later)
6
+ RUN apt-get update && apt-get install -y \
7
+ tesseract-ocr \
8
+ build-essential \
9
+ ffmpeg \
10
+ libsm6 \
11
+ libxext6 \
12
+ libgl1 \
13
+ libglib2.0-0 \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Set the working directory
17
+ WORKDIR /code
18
+
19
+ # Copy and install Python packages
20
+ COPY ./requirements.txt /code/requirements.txt
21
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
22
+
23
+ # Copy the rest of the application code
24
+ COPY . /code/
25
+
26
+ # Command to run your ORIGINAL app.py file
27
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,13 +1,8 @@
1
- ---
2
- title: AI Medical
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.44.1
8
- app_file: app.py
9
- pinned: false
10
- short_description: New Medical repository
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ metadatatitle: Medical Report Analyzer
3
+ emoji: 🩺
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ # --- FIX: Disable GPU to prevent CUDA initialization errors ---
3
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
4
+
5
+ from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_from_directory
6
+ from flask_pymongo import PyMongo
7
+ from flask_bcrypt import Bcrypt
8
+ import tensorflow as tf
9
+ from tensorflow.keras.models import load_model
10
+ from tensorflow.keras.preprocessing import image
11
+ import numpy as np
12
+ import cv2
13
+ import google.generativeai as genai
14
+ from dotenv import load_dotenv
15
+ import certifi
16
+ import uuid
17
+ import secrets
18
+ import logging
19
+
20
+ # -------------------- Setup & Config --------------------
21
+
22
+ # Load environment variables
23
+ load_dotenv()
24
+
25
+ app = Flask(__name__)
26
+
27
+ # Configurations
28
+ app.config["MONGO_URI"] = os.getenv("MONGODB_URI") or os.getenv("MONGO_URI")
29
+ # Keep your format, just ensure it's set once per process
30
+ app.config['SECRET_KEY'] = os.getenv("SECRET_KEY") or secrets.token_hex(16)
31
+
32
+ # Slightly safer cookie defaults without changing your session usage
33
+ app.config.setdefault("SESSION_COOKIE_HTTPONLY", True)
34
+ app.config.setdefault("SESSION_COOKIE_SAMESITE", "Lax")
35
+
36
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
37
+
38
+ # Basic logging
39
+ logging.basicConfig(level=logging.INFO)
40
+ logger = logging.getLogger("app")
41
+
42
+ # Initialize extensions
43
+ # If MONGO_URI missing, still construct PyMongo but avoid immediate use crashes
44
+ try:
45
+ if app.config["MONGO_URI"]:
46
+ mongo = PyMongo(app, tlsCAFile=certifi.where())
47
+ else:
48
+ logger.warning("MONGO_URI not set. MongoDB operations will fail.")
49
+ mongo = PyMongo(app, tlsCAFile=certifi.where())
50
+ except Exception as e:
51
+ logger.error(f"Mongo initialization error: {e}")
52
+ # Keep an object to avoid NameError later
53
+ mongo = None
54
+
55
+ bcrypt = Bcrypt(app)
56
+
57
+ # Configure Gemini
58
+ gemini_model = "gemini-2.0-flash"
59
+ if GEMINI_API_KEY:
60
+ try:
61
+ genai.configure(api_key=GEMINI_API_KEY)
62
+ # Keep your model name
63
+ gemini_model = genai.GenerativeModel('gemini-2.0-flash')
64
+ except Exception as e:
65
+ logger.error(f"Gemini initialization error: {e}")
66
+ else:
67
+ logger.warning("GEMINI_API_KEY/GOOGLE_API_KEY not set. /chat will return a friendly error.")
68
+
69
+ # --- Model Configuration ---
70
+ MODEL_CONFIG = {
71
+ "Pneumonia": {
72
+ "path": "model/best_pneumonia_model.h5",
73
+ "labels": ["Normal", "Pneumonia"],
74
+ "last_conv_layer": "relu",
75
+ "input_size": (224, 224)
76
+ },
77
+ "Tuberculosis": {
78
+ "path": "model/best_tuberculosis_model.h5",
79
+ "labels": ["Normal", "Tuberculosis"],
80
+ "last_conv_layer": "relu",
81
+ "input_size": (224, 224)
82
+ },
83
+ "Brain Tumor": {
84
+ "path": "model/best_braintumor_model.h5",
85
+ "labels": ["glioma", "meningioma", "notumor", "pituitary"],
86
+ "last_conv_layer": "relu",
87
+ "input_size": (224, 224)
88
+ },
89
+ "Skin Cancer": {
90
+ "path": "model/best_skincancer_model.h5",
91
+ "labels": ["Actinic keratoses", "Basal cell carcinoma", "Benign keratosis-like lesions",
92
+ "Dermatofibroma", "Melanoma", "Melanocytic nevi", "Vascular lesions"],
93
+ "last_conv_layer": "relu",
94
+ "input_size": (224, 224)
95
+ },
96
+ "Kvasir": {
97
+ "path": "model/best_kvasir_model.h5",
98
+ "labels": ["dyed-lifted-polyps", "dyed-resection-margins", "esophagitis",
99
+ "normal-cecum", "normal-pylorus", "normal-z-line", "polyps", "ulcerative-colitis"],
100
+ "last_conv_layer": "relu",
101
+ "input_size": (224, 224)
102
+ }
103
+ }
104
+
105
+ # --- Model Loading ---
106
+ models = {}
107
+
108
+ def load_all_models():
109
+ """Loads all models from the 'model' directory based on MODEL_CONFIG."""
110
+ for name, config in MODEL_CONFIG.items():
111
+ try:
112
+ model_path = config["path"]
113
+ if os.path.exists(model_path):
114
+ models[name] = load_model(model_path, compile=False)
115
+ logger.info(f"Successfully loaded {name} model from {model_path}.")
116
+ else:
117
+ logger.warning(f"Model file not found at {model_path}")
118
+ except Exception as e:
119
+ logger.error(f"Error loading model {name}: {e}")
120
+
121
+ # Load models on application startup
122
+ load_all_models()
123
+
124
+ # --- Image Preprocessing ---
125
+ def preprocess_image(img_path, target_size=(224, 224)):
126
+ """Preprocesses the image for model prediction."""
127
+ img = image.load_img(img_path, target_size=target_size)
128
+ img_array = image.img_to_array(img)
129
+ # Handle grayscale or alpha automatically by broadcasting if needed
130
+ if img_array.ndim == 2:
131
+ img_array = np.stack([img_array]*3, axis=-1)
132
+ elif img_array.shape[-1] == 4:
133
+ img_array = img_array[..., :3]
134
+ img_array = np.expand_dims(img_array, axis=0)
135
+ img_array = img_array.astype("float32") / 255.0
136
+ return img_array
137
+
138
+ # --- Grad-CAM Utilities ---
139
+ def _safe_get_layer(model, layer_name):
140
+ """Return layer if exists; else None."""
141
+ try:
142
+ return model.get_layer(layer_name)
143
+ except Exception:
144
+ return None
145
+
146
+ def find_last_conv_layer(model):
147
+ """Finds the name of the last convolutional layer in a model."""
148
+ logger.info("--- DEBUG: Searching for last convolutional layer ---")
149
+ for layer in reversed(model.layers):
150
+ if isinstance(layer, (tf.keras.layers.Conv2D, tf.keras.layers.DepthwiseConv2D)):
151
+ # 4D output: (batch, h, w, channels)
152
+ try:
153
+ out_shape = layer.output_shape
154
+ except Exception:
155
+ out_shape = None
156
+ if out_shape and len(out_shape) == 4:
157
+ logger.info(f"Found candidate last conv layer: {layer.name}")
158
+ return layer.name
159
+ raise ValueError("Could not automatically find a convolutional layer in the model.")
160
+
161
+ def get_gradcam_heatmap(model, img_array, last_conv_layer_name, pred_index=None):
162
+ """Generates a Grad-CAM heatmap."""
163
+ # If configured layer isn't present, auto-detect
164
+ if not _safe_get_layer(model, last_conv_layer_name):
165
+ last_conv_layer_name = find_last_conv_layer(model)
166
+
167
+ conv_layer = model.get_layer(last_conv_layer_name)
168
+ grad_model = tf.keras.models.Model(
169
+ [model.inputs], [conv_layer.output, model.output]
170
+ )
171
+
172
+ with tf.GradientTape() as tape:
173
+ conv_outputs, preds = grad_model(img_array, training=False)
174
+
175
+ if isinstance(preds, (list, tuple)):
176
+ preds = preds[0]
177
+
178
+ # Ensure preds is a tensor
179
+ preds = tf.convert_to_tensor(preds)
180
+
181
+ # If model is binary with single logit/sigmoid output
182
+ if preds.shape.rank is not None and preds.shape[-1] == 1:
183
+ class_channel = preds[:, 0]
184
+ else:
185
+ if pred_index is None:
186
+ pred_index = tf.argmax(preds[0])
187
+ class_channel = preds[:, pred_index]
188
+
189
+ grads = tape.gradient(class_channel, conv_outputs)
190
+ if grads is None:
191
+ # Fallback: no gradient (e.g., custom layers). Return uniform zeros heatmap.
192
+ heatmap = tf.zeros(conv_outputs.shape[1:3], dtype=tf.float32)
193
+ return heatmap.numpy()
194
+
195
+ pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
196
+ conv_outputs = conv_outputs[0]
197
+ heatmap = tf.tensordot(conv_outputs, pooled_grads, axes=(2, 0))
198
+
199
+ heatmap = tf.maximum(heatmap, 0)
200
+ denom = tf.math.reduce_max(heatmap)
201
+ heatmap = heatmap / (denom + 1e-8)
202
+ return heatmap.numpy()
203
+
204
+ def save_gradcam_image(img_path, heatmap, output_path, threshold=0.6, alpha=0.4):
205
+ """
206
+ Saves the Grad-CAM image by highlighting only the most important areas
207
+ with light red spots.
208
+ """
209
+ img = cv2.imread(img_path)
210
+ if img is None:
211
+ raise ValueError("Failed to read image with OpenCV.")
212
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Work with RGB
213
+
214
+ heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
215
+
216
+ # Create a mask where the heatmap is above the threshold
217
+ mask = heatmap > threshold
218
+
219
+ # Create a red overlay
220
+ overlay = np.zeros_like(img, dtype=np.uint8)
221
+ overlay[mask] = [255, 0, 0] # Red color for highlighted spots
222
+
223
+ # Blend the original image with the red overlay using the mask
224
+ superimposed_img = cv2.addWeighted(overlay, alpha, img, 1 - alpha, 0)
225
+
226
+ # Areas outside the mask should be the original image
227
+ superimposed_img[~mask] = img[~mask]
228
+
229
+ superimposed_img = cv2.cvtColor(superimposed_img, cv2.COLOR_RGB2BGR)
230
+ cv2.imwrite(output_path, superimposed_img)
231
+ return output_path
232
+
233
+ # -------------------- Routes --------------------
234
+
235
+ @app.route("/")
236
+ def home():
237
+ return redirect(url_for('index'))
238
+
239
+ @app.route('/tmp/<path:filename>')
240
+ def serve_tmp_file(filename):
241
+ return send_from_directory('/tmp', filename)
242
+
243
+ @app.route('/login', methods=['GET', 'POST'])
244
+ def login():
245
+ # Authentication removed; redirect to main app
246
+ return redirect(url_for('index'))
247
+
248
+ @app.route('/signup', methods=['GET', 'POST'])
249
+ def signup():
250
+ # Authentication removed; redirect to main app
251
+ return redirect(url_for('index'))
252
+
253
+ @app.route('/index')
254
+ def index():
255
+ # Publicly accessible index
256
+ return render_template('index.html')
257
+
258
+ @app.route('/logout')
259
+ def logout():
260
+ # Authentication removed; redirect to main app
261
+ return redirect(url_for('index'))
262
+
263
+ def _postprocess_binary_prediction(raw):
264
+ """
265
+ Normalize binary outputs across shapes:
266
+ - (1,) or (N,) : sigmoid probabilities
267
+ - (1,1) or (N,1) : sigmoid probabilities
268
+ - logits also supported (auto-sigmoid)
269
+ Returns probability in [0,1].
270
+ """
271
+ arr = np.array(raw, dtype=np.float32)
272
+ arr = np.squeeze(arr)
273
+ # If scalar, keep it
274
+ if arr.ndim == 0:
275
+ prob = float(arr)
276
+ # Heuristic: if obviously a logit (|x|>1 and not in [0,1]), apply sigmoid
277
+ if prob < 0.0 or prob > 1.0:
278
+ prob = float(1.0 / (1.0 + np.exp(-prob)))
279
+ return min(max(prob, 0.0), 1.0)
280
+ # If 1D vector, take first
281
+ prob = float(arr[0])
282
+ if prob < 0.0 or prob > 1.0:
283
+ prob = float(1.0 / (1.0 + np.exp(-prob)))
284
+ return min(max(prob, 0.0), 1.0)
285
+
286
+ @app.route("/predict", methods=["POST"])
287
+ def predict():
288
+ if "file" not in request.files:
289
+ return jsonify({"error": "No file part"}), 400
290
+
291
+ file = request.files["file"]
292
+ model_name = request.form.get("model")
293
+
294
+ if not file or file.filename == "":
295
+ return jsonify({"error": "No selected file"}), 400
296
+
297
+ if model_name not in models:
298
+ return jsonify({"error": "Invalid model selected"}), 400
299
+
300
+ try:
301
+ filename = f"{uuid.uuid4()}_{file.filename}"
302
+ filepath = os.path.join("/tmp", filename)
303
+ file.save(filepath)
304
+
305
+ model_config = MODEL_CONFIG[model_name]
306
+ model = models[model_name]
307
+ labels = model_config["labels"]
308
+ input_size = model_config.get("input_size", (224, 224))
309
+
310
+ img_array = preprocess_image(filepath, target_size=input_size)
311
+ prediction = model.predict(img_array, verbose=0)
312
+
313
+ # Ensure numpy array
314
+ prediction = np.array(prediction)
315
+
316
+ # Binary case (2 labels) with single neuron output (logit or sigmoid)
317
+ if len(labels) == 2 and prediction.ndim >= 1 and prediction.shape[-1] in (1,) and prediction.size >= 1:
318
+ prob_pos = _postprocess_binary_prediction(prediction)
319
+ if prob_pos >= 0.5:
320
+ predicted_index = 1
321
+ predicted_label = labels[1]
322
+ confidence = prob_pos
323
+ else:
324
+ predicted_index = 0
325
+ predicted_label = labels[0]
326
+ confidence = 1.0 - prob_pos
327
+ else:
328
+ # Multi-class: softmax or logits
329
+ if prediction.ndim == 2:
330
+ vec = prediction[0]
331
+ else:
332
+ vec = prediction.reshape(-1)
333
+ # If appears to be logits, apply softmax for confidence; otherwise trust as probs
334
+ if np.any(vec < 0) or np.any(vec > 1) or not np.isclose(np.sum(vec), 1.0, atol=1e-3):
335
+ exps = np.exp(vec - np.max(vec))
336
+ probs = exps / (np.sum(exps) + 1e-8)
337
+ else:
338
+ probs = vec
339
+ predicted_index = int(np.argmax(probs))
340
+ predicted_label = labels[predicted_index]
341
+ confidence = float(np.max(probs))
342
+
343
+ gradcam_url = None
344
+ try:
345
+ logger.info(f"--- Generating Grad-CAM for model: {model_name} ---")
346
+ last_conv_layer_name = MODEL_CONFIG[model_name].get('last_conv_layer') or ""
347
+ heatmap = get_gradcam_heatmap(model, img_array, last_conv_layer_name, pred_index=predicted_index)
348
+
349
+ gradcam_filename = f"gradcam_{filename}"
350
+ gradcam_filepath = os.path.join("/tmp", gradcam_filename)
351
+ save_gradcam_image(filepath, heatmap, gradcam_filepath)
352
+ gradcam_url = url_for('serve_tmp_file', filename=gradcam_filename)
353
+ logger.info("--- Successfully generated Grad-CAM image ---")
354
+ except Exception as e:
355
+ logger.error(f"--- Grad-CAM Generation FAILED for model: {model_name} --- Error: {e}")
356
+ try:
357
+ model.summary(print_fn=lambda x: logger.info(x))
358
+ except Exception:
359
+ pass
360
+
361
+ return jsonify({
362
+ "original_image": url_for('serve_tmp_file', filename=filename),
363
+ "gradcam_image": gradcam_url,
364
+ "prediction": str(predicted_label),
365
+ "confidence": float(confidence),
366
+ "model_used": str(model_name)
367
+ })
368
+ except Exception as e:
369
+ logger.exception("Prediction error")
370
+ return jsonify({"error": str(e)}), 500
371
+
372
+ @app.route("/chat", methods=["POST"])
373
+ def chat():
374
+ data = request.get_json(silent=True) or {}
375
+ user_message = data.get("message", "")
376
+ prediction_context = data.get("context") or {}
377
+
378
+ # Guard against missing keys
379
+ model_used = prediction_context.get('model_used', 'Unknown Model')
380
+ pred_label = prediction_context.get('prediction', 'Unknown')
381
+ conf = prediction_context.get('confidence', 0.0)
382
+ try:
383
+ conf_pct = float(conf) * 100.0
384
+ except Exception:
385
+ conf_pct = 0.0
386
+
387
+ prompt = f"""
388
+ You are a helpful medical assistant chatbot.
389
+ A medical image was analyzed with the following results:
390
+ - Model Used: {model_used}
391
+ - Prediction: {pred_label}
392
+ - Confidence Score: {conf_pct:.2f}%
393
+ The user's question is: "{user_message}"
394
+ Based on this context, provide a helpful and informative response.
395
+ Do not provide a diagnosis. Advise the user to consult a medical professional.
396
+ """
397
+
398
+ try:
399
+ if gemini_model is None:
400
+ return jsonify({"error": "Gemini API not configured. Set GEMINI_API_KEY in environment."}), 500
401
+ response = gemini_model.generate_content(prompt)
402
+ # Some SDKs return .text; guard if attribute missing
403
+ text = getattr(response, "text", None)
404
+ if not text:
405
+ # Try to stringify safely
406
+ text = str(response)
407
+ return jsonify({"response": text})
408
+ except Exception as e:
409
+ return jsonify({"error": str(e)}), 500
410
+
411
+ if __name__ == "__main__":
412
+ # Keep your debug flag as-is
413
+ app.run(debug=True)
report_analyzer_app.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, request, jsonify, render_template
3
+ from flask_pymongo import PyMongo
4
+ from flask_bcrypt import Bcrypt
5
+ import secrets
6
+ import google.generativeai as genai
7
+ import markdown
8
+ from PIL import Image
9
+ import pytesseract # For OCR
10
+ import fitz # PyMuPDF for reading PDFs
11
+ import io
12
+
13
+ # --- Basic Flask App Setup ---
14
+ app = Flask(__name__)
15
+
16
+ # --- Configurations ---
17
+ app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or secrets.token_hex(16)
18
+ app.config['MONGO_URI'] = os.getenv('MONGODB_URI') or os.getenv('MONGO_URI')
19
+
20
+ # --- Gemini API Configuration ---
21
+ GOOGLE_API_KEY = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
22
+ try:
23
+ if GOOGLE_API_KEY:
24
+ genai.configure(api_key=GOOGLE_API_KEY)
25
+ else:
26
+ print("Warning: GEMINI_API_KEY/GOOGLE_API_KEY not set; analysis will fail until configured.")
27
+ except Exception as e:
28
+ print(f"Error configuring Gemini API: {e}\nPlease make sure the GEMINI_API_KEY or GOOGLE_API_KEY environment variable is set.")
29
+
30
+ # --- Initialize Extensions ---
31
+ mongo = PyMongo(app)
32
+ bcrypt = Bcrypt(app)
33
+
34
+ # --- User Model for Flask-Login ---
35
+ # Authentication removed: no user model or login manager
36
+
37
+ # --- Helper function for Gemini Analysis ---
38
+ def get_simplified_report(report_text):
39
+ """Sends text to Gemini and returns a simplified, markdown-formatted report."""
40
+ if not report_text or not report_text.strip():
41
+ raise ValueError("Extracted text is empty.")
42
+
43
+ model = genai.GenerativeModel('gemini-2.0-flash')
44
+ prompt = f"""
45
+ You are an expert medical assistant. Your task is to translate the following medical report into simple, clear, and easy-to-understand language for a patient with no medical background.
46
+
47
+ Instructions:
48
+ 1. Start with a one-sentence summary of the main finding.
49
+ 2. Create a "Key Findings" section using bullet points.
50
+ 3. For each technical term or measurement, first state the term from the report, then explain what it means in simple words and whether the result is normal, high, or low.
51
+ 4. Maintain a reassuring and professional tone.
52
+ 5. Conclude with a clear disclaimer: "This is a simplified summary and not a substitute for professional medical advice. Please discuss the full report with your doctor."
53
+ 6. Format the entire output in Markdown.
54
+
55
+ Medical Report to Analyze:
56
+ ---
57
+ {report_text}
58
+ ---
59
+ """
60
+ response = model.generate_content(prompt)
61
+ return markdown.markdown(response.text)
62
+
63
+ # --- Authentication routes removed; app is public ---
64
+
65
+ # --- Main Application Routes ---
66
+ @app.route('/')
67
+ def index():
68
+ # Publicly accessible page
69
+ return render_template('report_analyzer.html')
70
+
71
+ @app.route('/analyze_report_text', methods=['POST'])
72
+ def analyze_report_text():
73
+ """Analyzes a report submitted as plain text."""
74
+ try:
75
+ data = request.get_json()
76
+ report_text = data.get('report_text')
77
+ if not report_text or not report_text.strip():
78
+ return jsonify({'error': 'Report text cannot be empty.'}), 400
79
+
80
+ html_response = get_simplified_report(report_text)
81
+ return jsonify({'simplified_report': html_response})
82
+
83
+ except Exception as e:
84
+ print(f"Error during text report analysis: {e}")
85
+ return jsonify({'error': 'An internal error occurred during report analysis.'}), 500
86
+
87
+ @app.route('/analyze_report_file', methods=['POST'])
88
+ def analyze_report_file():
89
+ """Analyzes a report submitted as a PDF or Image file."""
90
+ try:
91
+ if 'report_file' not in request.files:
92
+ return jsonify({'error': 'No file part in the request.'}), 400
93
+
94
+ file = request.files['report_file']
95
+ if file.filename == '':
96
+ return jsonify({'error': 'No file selected.'}), 400
97
+
98
+ report_text = ""
99
+ # Check file extension
100
+ if file.filename.lower().endswith('.pdf'):
101
+ pdf_document = fitz.open(stream=file.read(), filetype="pdf")
102
+ for page in pdf_document:
103
+ report_text += page.get_text()
104
+ pdf_document.close()
105
+ elif file.filename.lower().endswith(('.png', '.jpg', '.jpeg')):
106
+ image = Image.open(file.stream)
107
+ report_text = pytesseract.image_to_string(image)
108
+ else:
109
+ return jsonify({'error': 'Unsupported file type. Please upload a PDF or an image.'}), 400
110
+
111
+ html_response = get_simplified_report(report_text)
112
+ return jsonify({'simplified_report': html_response})
113
+
114
+ except Exception as e:
115
+ print(f"Error during file report analysis: {e}")
116
+ return jsonify({'error': 'An internal error occurred during file analysis.'}), 500
117
+
118
+ # --- Run the Application ---
119
+ if __name__ == '__main__':
120
+ app.run(port=5001, debug=True)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask_pymongo
3
+ flask_bcrypt
4
+ tensorflow
5
+ numpy
6
+ opencv-python
7
+ google-generativeai
8
+ python-dotenv
9
+ Pillow
10
+ gunicorn
11
+ markdown
12
+ pytesseract
13
+ pymupdf
train.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tensorflow as tf
3
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator
4
+ from tensorflow.keras.applications import MobileNetV2
5
+ from tensorflow.keras import layers, models
6
+ from tensorflow.keras.optimizers import Adam
7
+
8
+ # Updated path
9
+ base_dir = 'data/chest_xray'
10
+ train_dir = os.path.join(base_dir, 'train')
11
+ val_dir = os.path.join(base_dir, 'val')
12
+
13
+ # Parameters
14
+ IMG_SIZE = (224, 224)
15
+ BATCH_SIZE = 32
16
+ EPOCHS = 5 # You can increase later
17
+
18
+ # Data generators
19
+ train_gen = ImageDataGenerator(
20
+ rescale=1./255,
21
+ rotation_range=10,
22
+ zoom_range=0.1,
23
+ horizontal_flip=True
24
+ )
25
+
26
+ val_gen = ImageDataGenerator(rescale=1./255)
27
+
28
+ train_data = train_gen.flow_from_directory(
29
+ train_dir,
30
+ target_size=IMG_SIZE,
31
+ batch_size=BATCH_SIZE,
32
+ class_mode='binary'
33
+ )
34
+
35
+ val_data = val_gen.flow_from_directory(
36
+ val_dir,
37
+ target_size=IMG_SIZE,
38
+ batch_size=BATCH_SIZE,
39
+ class_mode='binary'
40
+ )
41
+
42
+ # MobileNetV2 base
43
+ base_model = MobileNetV2(input_shape=(224, 224, 3), include_top=False, weights='imagenet')
44
+ base_model.trainable = False
45
+
46
+ # Custom head
47
+ model = models.Sequential([
48
+ base_model,
49
+ layers.GlobalAveragePooling2D(),
50
+ layers.Dense(128, activation='relu'),
51
+ layers.Dropout(0.3),
52
+ layers.Dense(1, activation='sigmoid') # Binary classifier
53
+ ])
54
+
55
+ model.compile(optimizer=Adam(learning_rate=0.0001),
56
+ loss='binary_crossentropy',
57
+ metrics=['accuracy'])
58
+
59
+ # Train
60
+ model.fit(train_data, validation_data=val_data, epochs=EPOCHS)
61
+
62
+ # Save model
63
+ os.makedirs("model", exist_ok=True)
64
+ model.save("model/pneumonia_model.h5")
65
+ print("✅ Model saved as model/pneumonia_model.h5")