Smaron commited on
Commit
fbfb124
·
verified ·
1 Parent(s): 98f00d1

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +258 -0
  2. best_model_weights (2).h5 +3 -0
  3. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import numpy as np
5
+ import cv2
6
+ import tensorflow as tf
7
+ from tensorflow import keras
8
+ from tensorflow.keras import layers
9
+ from PIL import Image
10
+ import matplotlib.cm as cm
11
+ from flask import Flask, render_template, request, jsonify
12
+
13
+ app = Flask(__name__)
14
+
15
+ GLOBAL_MODEL = None
16
+ GRAD_MODEL = None
17
+
18
+ def rebuild_and_load_model(weights_path):
19
+ """Rebuilds the topology and applies an aggressive structural fallback strategy
20
+ if a shape/axis mismatch occurs during weight mapping."""
21
+ global GLOBAL_MODEL, GRAD_MODEL
22
+
23
+ keras.backend.clear_session()
24
+
25
+ if not os.path.exists(weights_path):
26
+ raise FileNotFoundError(f"No weights file found at target path: {weights_path}")
27
+
28
+ # 1. Build standard outer model architecture
29
+ base_model = tf.keras.applications.EfficientNetB0(
30
+ include_top=False, weights='imagenet', input_shape=(224, 224, 3)
31
+ )
32
+ base_model.trainable = False
33
+
34
+ inputs = keras.Input(shape=(224, 224, 3))
35
+ x = base_model(inputs, training=False)
36
+ x = layers.GlobalAveragePooling2D()(x)
37
+ x = layers.Dropout(0.2)(x)
38
+ outputs = layers.Dense(4, activation='softmax')(x)
39
+
40
+ model = keras.Model(inputs, outputs)
41
+
42
+ # 2. Aggressive Fallback Weight Loading Strategy
43
+ try:
44
+ print("--> Strategy A: Attempting full top-level model weight mapping...")
45
+ model.load_weights(weights_path)
46
+ print("--> Strategy A Successful: Full model weights matched.")
47
+ except ValueError as e:
48
+ print(f"--> Strategy A Failed due to axis/shape mismatch: {e}")
49
+ print("--> Strategy B: Attempting direct injection into the inner Backbone layer...")
50
+ try:
51
+ # This handles cases where only the 'base_model' weights were exported during training
52
+ base_model.load_weights(weights_path)
53
+ print("--> Strategy B Successful: Backbone layers updated successfully.")
54
+ except Exception as strategy_b_error:
55
+ print(f"--> Strategy B Failed: {strategy_b_error}")
56
+ print("--> Strategy C: Attempting relaxed name-matching mapping fallback...")
57
+ try:
58
+ model.load_weights(weights_path, by_name=True)
59
+ print("--> Strategy C Successful: Named layers updated.")
60
+ except Exception as strategy_c_error:
61
+ raise ValueError(
62
+ f"All structural loading mapping routes failed.\n"
63
+ f"Primary Error: {e}\n"
64
+ f"Sub-Graph Error: {strategy_b_error}"
65
+ )
66
+
67
+ GLOBAL_MODEL = model
68
+
69
+ # 3. Build Safe Grad-CAM tensor mapping
70
+ backbone_output_tensor = None
71
+ for i, layer in enumerate(model.layers):
72
+ if 'efficientnet' in layer.name.lower():
73
+ backbone_output_tensor = model.layers[i+1].input
74
+ break
75
+
76
+ if backbone_output_tensor is None:
77
+ raise ValueError("Could not trace EfficientNet backbone connections cleanly.")
78
+
79
+ GRAD_MODEL = keras.Model(inputs=model.inputs, outputs=[backbone_output_tensor, model.output])
80
+ print(f"--> App Pipeline fully mounted and online from path: {weights_path}")
81
+
82
+ def get_base64_image(img_ndarray):
83
+ """Converts standard BGR/RGB image array to clean Base64 strings."""
84
+ encoding_buffer = cv2.imencode('.png', img_ndarray)[1]
85
+ base64_string = base64.b64encode(encoding_buffer).decode('utf-8')
86
+ return f"data:image/png;base64,{base64_string}"
87
+
88
+ @app.route('/', methods=['GET'])
89
+ def index():
90
+ return render_template('index.html')
91
+
92
+ @app.route('/configure', methods=['POST'])
93
+ def configure():
94
+ try:
95
+ data = request.get_json()
96
+ model_path = data.get('model_path', 'best_model_weights.h5')
97
+ rebuild_and_load_model(model_path)
98
+ return jsonify({"status": "success", "message": f"Successfully mounted weights: {os.path.basename(model_path)}"})
99
+ except Exception as e:
100
+ return jsonify({"status": "error", "message": str(e)}), 400
101
+
102
+ # =========================================================================
103
+ # CONFIGURATION: CLASS BOUNDARY THRESHOLDS (Scale: 0.0 to 1.0)
104
+ # =========================================================================
105
+ CLASS_THRESHOLDS = {
106
+ 0: 0.85, # Class 0 requires a harsh 85% confidence threshold
107
+ 1: 0.60, # Class 1 requires 60%
108
+ 2: 0.70, # Class 2 requires 70%
109
+ 3: 0.50 # Class 3 requires 50%
110
+ }
111
+
112
+ @app.route('/predict', methods=['POST'])
113
+ def predict():
114
+ global GLOBAL_MODEL, GRAD_MODEL
115
+
116
+ if GLOBAL_MODEL is None or GRAD_MODEL is None:
117
+ return jsonify({"status": "error", "message": "Model uninitialized. Mount a valid weights file first."}), 400
118
+
119
+ if 'image' not in request.files:
120
+ return jsonify({"status": "error", "message": "No file uploaded."}), 400
121
+
122
+ file = request.files['image']
123
+ if file.filename == '':
124
+ return jsonify({"status": "error", "message": "Empty file."}), 400
125
+
126
+ try:
127
+ in_memory_file = io.BytesIO(file.read())
128
+ pil_img = Image.open(in_memory_file).convert('RGB')
129
+
130
+ orig_w, orig_h = pil_img.size
131
+ raw_cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
132
+
133
+ resized_img = pil_img.resize((224, 224))
134
+ img_array = np.array(resized_img).astype(np.float32)
135
+ img_tensor = np.expand_dims(img_array, axis=0)
136
+
137
+ # Calculate Grad-CAM maps
138
+ with tf.GradientTape() as tape:
139
+ last_conv_layer_outputs, predictions = GRAD_MODEL(img_tensor)
140
+ pred_idx = tf.argmax(predictions[0])
141
+ top_class_channel = predictions[:, pred_idx]
142
+
143
+ grads = tape.gradient(top_class_channel, last_conv_layer_outputs)
144
+ pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
145
+
146
+ last_conv_layer_outputs = last_conv_layer_outputs[0]
147
+ heatmap = last_conv_layer_outputs @ pooled_grads[..., tf.newaxis]
148
+ heatmap = tf.squeeze(heatmap)
149
+
150
+ heatmap = tf.maximum(heatmap, 0) / (tf.math.reduce_max(heatmap) + 1e-10)
151
+ heatmap_np = heatmap.numpy()
152
+
153
+ heatmap_resized = cv2.resize(heatmap_np, (orig_w, orig_h))
154
+ heatmap_colored = cm.jet(heatmap_resized)[..., :3] * 255
155
+ heatmap_colored = cv2.cvtColor(heatmap_colored.astype(np.uint8), cv2.COLOR_RGB2BGR)
156
+
157
+ alpha = 0.4
158
+ superimposed_img = cv2.addWeighted(heatmap_colored, alpha, raw_cv_img, (1.0 - alpha), 0)
159
+
160
+ # --- THRESHOLD EVALUATION BLOCK ---
161
+ raw_confidence = float(predictions[0][pred_idx])
162
+ target_idx = int(pred_idx)
163
+ required_minimum = CLASS_THRESHOLDS.get(target_idx, 0.50)
164
+
165
+ confidence_percentage = raw_confidence * 100
166
+ threshold_percentage = required_minimum * 100
167
+
168
+ if raw_confidence < required_minimum:
169
+ predicted_label = f"Uncertain / Low Confidence (Best Guess: Class {target_idx})"
170
+ passed_threshold = False
171
+ else:
172
+ predicted_label = f"Class {target_idx}"
173
+ passed_threshold = True
174
+
175
+ return jsonify({
176
+ "status": "success",
177
+ "prediction": predicted_label,
178
+ "confidence": f"{confidence_percentage:.2f}%",
179
+ "passed_threshold": passed_threshold,
180
+ "threshold_required": f"{threshold_percentage:.2f}%",
181
+ "original_image": get_base64_image(raw_cv_img),
182
+ "gradcam_image": get_base64_image(superimposed_img)
183
+ })
184
+
185
+ except Exception as e:
186
+ return jsonify({"status": "error", "message": str(e)}), 500
187
+
188
+ # @app.route('/predict', methods=['POST'])
189
+ # def predict():
190
+ # global GLOBAL_MODEL, GRAD_MODEL
191
+
192
+ # if GLOBAL_MODEL is None or GRAD_MODEL is None:
193
+ # return jsonify({"status": "error", "message": "Model uninitialized. Mount a valid weights file first."}), 400
194
+
195
+ # if 'image' not in request.files:
196
+ # return jsonify({"status": "error", "message": "No file uploaded."}), 400
197
+
198
+ # file = request.files['image']
199
+ # if file.filename == '':
200
+ # return jsonify({"status": "error", "message": "Empty file."}), 400
201
+
202
+ # try:
203
+ # in_memory_file = io.BytesIO(file.read())
204
+ # pil_img = Image.open(in_memory_file).convert('RGB')
205
+
206
+ # orig_w, orig_h = pil_img.size
207
+ # raw_cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
208
+
209
+ # resized_img = pil_img.resize((224, 224))
210
+ # img_array = np.array(resized_img).astype(np.float32)
211
+ # img_tensor = np.expand_dims(img_array, axis=0)
212
+
213
+ # # Calculate Grad-CAM maps
214
+ # with tf.GradientTape() as tape:
215
+ # last_conv_layer_outputs, predictions = GRAD_MODEL(img_tensor)
216
+ # pred_idx = tf.argmax(predictions[0])
217
+ # top_class_channel = predictions[:, pred_idx]
218
+
219
+ # grads = tape.gradient(top_class_channel, last_conv_layer_outputs)
220
+ # pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
221
+
222
+ # last_conv_layer_outputs = last_conv_layer_outputs[0]
223
+ # heatmap = last_conv_layer_outputs @ pooled_grads[..., tf.newaxis]
224
+ # heatmap = tf.squeeze(heatmap)
225
+
226
+ # heatmap = tf.maximum(heatmap, 0) / (tf.math.reduce_max(heatmap) + 1e-10)
227
+ # heatmap_np = heatmap.numpy()
228
+
229
+ # heatmap_resized = cv2.resize(heatmap_np, (orig_w, orig_h))
230
+ # heatmap_colored = cm.jet(heatmap_resized)[..., :3] * 255
231
+ # heatmap_colored = cv2.cvtColor(heatmap_colored.astype(np.uint8), cv2.COLOR_RGB2BGR)
232
+
233
+ # alpha = 0.4
234
+ # superimposed_img = cv2.addWeighted(heatmap_colored, alpha, raw_cv_img, (1.0 - alpha), 0)
235
+
236
+ # confidence = float(predictions[0][pred_idx]) * 100
237
+ # predicted_label = f"Class {pred_idx}"
238
+
239
+ # return jsonify({
240
+ # "status": "success",
241
+ # "prediction": predicted_label,
242
+ # "confidence": f"{confidence:.2f}%",
243
+ # "original_image": get_base64_image(raw_cv_img),
244
+ # "gradcam_image": get_base64_image(superimposed_img)
245
+ # })
246
+
247
+ # except Exception as e:
248
+ # return jsonify({"status": "error", "message": str(e)}), 500
249
+
250
+ if __name__ == '__main__':
251
+ default_weights = 'best_model_weights (2).h5'
252
+ if os.path.exists(default_weights):
253
+ try:
254
+ rebuild_and_load_model(default_weights)
255
+ except Exception as e:
256
+ print(f"Automatic startup loading bypassed: {e}")
257
+
258
+ app.run(debug=True, port=5000)
best_model_weights (2).h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:916a19e43ecd983622f2847238fe004ddb105d6a350245dbfbba20bb8beaa6c5
3
+ size 16487384
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Flask==3.0.2
2
+ tensorflow-cpu==2.15.0 # CRITICAL: Barebones CPU distribution saves ~400MB RAM
3
+ numpy==1.26.4
4
+ opencv-python-headless==4.9.0.80 # CRITICAL: Bypasses server GUI package dependencies
5
+ Pillow==10.2.0
6
+ matplotlib==3.8.3
7
+ gunicorn==21.2.0 # High-performance production web server