Spaces:
Running
Running
fixed missing/colliding labels and updated global model attention
Browse files
app.py
CHANGED
|
@@ -4,11 +4,7 @@ import time
|
|
| 4 |
import cv2
|
| 5 |
import numpy as np
|
| 6 |
|
| 7 |
-
# ==========================================
|
| 8 |
-
# HARDWARE SCHEDULING INTERFACES (BASELINE)
|
| 9 |
-
# ==========================================
|
| 10 |
os.environ['TF_USE_LEGACY_KERAS'] = '1'
|
| 11 |
-
# Initialize clean, un-throttled thread footprints for benchmarking
|
| 12 |
os.environ["OMP_NUM_THREADS"] = "2"
|
| 13 |
os.environ["TF_NUM_INTRAOP_THREADS"] = "2"
|
| 14 |
os.environ["TF_NUM_INTEROP_THREADS"] = "1"
|
|
@@ -25,7 +21,6 @@ from object_detection.builders import model_builder
|
|
| 25 |
|
| 26 |
app = FastAPI()
|
| 27 |
|
| 28 |
-
# Enable smooth frontend cross-origin header interceptions
|
| 29 |
app.add_middleware(
|
| 30 |
CORSMiddleware,
|
| 31 |
allow_origins=["*"],
|
|
@@ -35,27 +30,18 @@ app.add_middleware(
|
|
| 35 |
expose_headers=["X-Processing-Time", "X-Model-Status"]
|
| 36 |
)
|
| 37 |
|
| 38 |
-
# 1. Download Private Models
|
| 39 |
HF_TOKEN = os.getenv("HF_Token")
|
| 40 |
REPO_ID = "SaniaE/Car_Damage_Detection"
|
| 41 |
|
| 42 |
-
|
| 43 |
-
model_dir = snapshot_download(
|
| 44 |
-
repo_id=REPO_ID,
|
| 45 |
-
token=HF_TOKEN,
|
| 46 |
-
local_dir="./models_data"
|
| 47 |
-
)
|
| 48 |
|
| 49 |
PIPELINE_CONFIG = os.path.join(model_dir, "object_detection_model/pipeline.config")
|
| 50 |
CHECKPOINT_PATH = os.path.join(model_dir, "object_detection_model/ckpt-37")
|
| 51 |
LABEL_MAP_PATH = os.path.join(model_dir, "object_detection_model/label_map.pbtxt")
|
| 52 |
CNN_MODEL_PATH = os.path.join(model_dir, "cnn_filter.h5")
|
| 53 |
|
| 54 |
-
# 3. Load Models
|
| 55 |
-
print("Loading CNN Filter...")
|
| 56 |
cnn_filter = tf.keras.models.load_model(CNN_MODEL_PATH, compile=False)
|
| 57 |
|
| 58 |
-
print("Loading Object Detection Model...")
|
| 59 |
configs = config_util.get_configs_from_pipeline_file(PIPELINE_CONFIG)
|
| 60 |
detection_model = model_builder.build(model_config=configs['model'], is_training=False)
|
| 61 |
ckpt = tf.compat.v2.train.Checkpoint(model=detection_model)
|
|
@@ -69,12 +55,15 @@ def detect_fn(image):
|
|
| 69 |
detections = detection_model.postprocess(prediction_dict, shapes)
|
| 70 |
return detections
|
| 71 |
|
| 72 |
-
def
|
| 73 |
-
"""Extracts
|
| 74 |
scores = detections['detection_scores'][0].numpy()
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
@app.get("/")
|
| 80 |
def read_root():
|
|
@@ -83,8 +72,6 @@ def read_root():
|
|
| 83 |
@app.post("/predict")
|
| 84 |
async def predict(file: UploadFile = File(...)):
|
| 85 |
start_time = time.perf_counter()
|
| 86 |
-
|
| 87 |
-
# Read Image
|
| 88 |
contents = await file.read()
|
| 89 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 90 |
image_np = np.array(image_pil)
|
|
@@ -114,9 +101,14 @@ async def predict(file: UploadFile = File(...)):
|
|
| 114 |
ymin, xmin, ymax, xmax = boxes[i]
|
| 115 |
(left, right, top, bottom) = (xmin * width, xmax * width,
|
| 116 |
ymin * height, ymax * height)
|
|
|
|
|
|
|
| 117 |
cv2.rectangle(image_cv, (int(left), int(top)), (int(right), int(bottom)), (255, 255, 0), 2)
|
|
|
|
|
|
|
| 118 |
label = f"{category_index.get(classes[i] + 1, {}).get('name', 'unknown')}: {int(scores[i]*100)}%"
|
| 119 |
-
|
|
|
|
| 120 |
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2)
|
| 121 |
|
| 122 |
_, buffer = cv2.imencode('.jpg', image_cv)
|
|
@@ -132,7 +124,6 @@ async def predict(file: UploadFile = File(...)):
|
|
| 132 |
@app.post("/explain")
|
| 133 |
async def explain(file: UploadFile = File(...)):
|
| 134 |
start_time = time.perf_counter()
|
| 135 |
-
|
| 136 |
contents = await file.read()
|
| 137 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 138 |
image_np = np.array(image_pil).astype(np.float32)
|
|
@@ -145,12 +136,13 @@ async def explain(file: UploadFile = File(...)):
|
|
| 145 |
raw_scores = prediction_dict['class_predictions_with_background'][0]
|
| 146 |
|
| 147 |
detections = detection_model.postprocess(prediction_dict, shapes)
|
| 148 |
-
|
| 149 |
|
| 150 |
-
if
|
| 151 |
elapsed_time = time.perf_counter() - start_time
|
| 152 |
return Response(status_code=204, headers={"X-Processing-Time": f"{elapsed_time:.4f}"})
|
| 153 |
|
|
|
|
| 154 |
loss = tf.reduce_max(raw_scores[:, top_class])
|
| 155 |
|
| 156 |
grads = tape.gradient(loss, input_tensor)
|
|
@@ -171,16 +163,11 @@ async def explain(file: UploadFile = File(...)):
|
|
| 171 |
elapsed_time = time.perf_counter() - start_time
|
| 172 |
print(f"[BENCHMARK] /explain turnaround: {elapsed_time:.4f}s")
|
| 173 |
|
| 174 |
-
return StreamingResponse(
|
| 175 |
-
io.BytesIO(buffer.tobytes()),
|
| 176 |
-
media_type="image/jpeg",
|
| 177 |
-
headers={"X-Processing-Time": f"{elapsed_time:.4f}"}
|
| 178 |
-
)
|
| 179 |
|
| 180 |
@app.post("/explain/tiled")
|
| 181 |
async def explain_tiled(file: UploadFile = File(...)):
|
| 182 |
start_time = time.perf_counter()
|
| 183 |
-
|
| 184 |
contents = await file.read()
|
| 185 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 186 |
image_np = np.array(image_pil).astype(np.float32)
|
|
@@ -193,41 +180,48 @@ async def explain_tiled(file: UploadFile = File(...)):
|
|
| 193 |
|
| 194 |
base_image = cv2.cvtColor(image_np.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
| 195 |
h_img, w_img, _ = base_image.shape
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
cv2.rectangle(base_image, (int(xmin*w_img), int(ymin*h_img)),
|
| 201 |
-
(int(xmax*w_img), int(ymax*h_img)), (255, 255, 0), 2)
|
| 202 |
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
with tf.GradientTape() as tape:
|
| 210 |
-
tape.watch(input_tensor)
|
| 211 |
-
image, shapes = detection_model.preprocess(input_tensor)
|
| 212 |
-
prediction_dict = detection_model.predict(image, shapes)
|
| 213 |
-
raw_scores = prediction_dict['class_predictions_with_background'][0]
|
| 214 |
-
loss = tf.reduce_max(raw_scores[:, target_class])
|
| 215 |
-
|
| 216 |
-
grads = tape.gradient(loss, input_tensor)
|
| 217 |
-
saliency = np.max(np.abs(grads.numpy()), axis=-1)[0]
|
| 218 |
-
|
| 219 |
-
v_min, v_max = np.percentile(saliency, (5, 95))
|
| 220 |
-
saliency = np.clip((saliency - v_min) / (v_max - v_min + 1e-8), 0, 1)
|
| 221 |
-
heatmap = cv2.applyColorMap(np.uint8(255 * saliency), cv2.COLORMAP_JET)
|
| 222 |
-
|
| 223 |
-
overlay = cv2.addWeighted(cv2.cvtColor(image_np.astype(np.uint8), cv2.COLOR_RGB2BGR), 0.6, heatmap, 0.4, 0)
|
| 224 |
-
|
| 225 |
-
class_name = category_index.get(target_class + 1, {}).get('name', 'unknown')
|
| 226 |
-
cv2.putText(overlay, f"Top {i+1}: {class_name}", (10, 30),
|
| 227 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 228 |
-
panels.append(overlay)
|
| 229 |
-
else:
|
| 230 |
-
panels.append(np.zeros_like(base_image))
|
| 231 |
|
| 232 |
top_row = np.hstack((panels[0], panels[1]))
|
| 233 |
bottom_row = np.hstack((panels[2], panels[3]))
|
|
@@ -237,16 +231,11 @@ async def explain_tiled(file: UploadFile = File(...)):
|
|
| 237 |
elapsed_time = time.perf_counter() - start_time
|
| 238 |
print(f"[BENCHMARK] /explain/tiled turnaround: {elapsed_time:.4f}s")
|
| 239 |
|
| 240 |
-
return StreamingResponse(
|
| 241 |
-
io.BytesIO(buffer.tobytes()),
|
| 242 |
-
media_type="image/jpeg",
|
| 243 |
-
headers={"X-Processing-Time": f"{elapsed_time:.4f}"}
|
| 244 |
-
)
|
| 245 |
|
| 246 |
@app.post("/explain/global")
|
| 247 |
async def explain_global(file: UploadFile = File(...)):
|
| 248 |
start_time = time.perf_counter()
|
| 249 |
-
|
| 250 |
contents = await file.read()
|
| 251 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 252 |
image_np = np.array(image_pil).astype(np.float32)
|
|
@@ -259,14 +248,19 @@ async def explain_global(file: UploadFile = File(...)):
|
|
| 259 |
image, shapes = detection_model.preprocess(input_tensor)
|
| 260 |
prediction_dict = detection_model.predict(image, shapes)
|
| 261 |
raw_scores = prediction_dict['class_predictions_with_background'][0]
|
|
|
|
| 262 |
foreground_scores = raw_scores[:, 1:]
|
| 263 |
-
|
|
|
|
|
|
|
| 264 |
|
| 265 |
grads = tape.gradient(loss, input_tensor)
|
| 266 |
saliency = np.max(np.abs(grads.numpy()), axis=-1)[0]
|
| 267 |
|
| 268 |
-
|
|
|
|
| 269 |
saliency = np.clip((saliency - v_min) / (v_max - v_min + 1e-8), 0, 1)
|
|
|
|
| 270 |
|
| 271 |
heatmap = cv2.applyColorMap(np.uint8(255 * saliency), cv2.COLORMAP_JET)
|
| 272 |
overlay = cv2.addWeighted(image_bgr, 0.6, heatmap, 0.4, 0)
|
|
@@ -278,8 +272,4 @@ async def explain_global(file: UploadFile = File(...)):
|
|
| 278 |
elapsed_time = time.perf_counter() - start_time
|
| 279 |
print(f"[BENCHMARK] /explain/global turnaround: {elapsed_time:.4f}s")
|
| 280 |
|
| 281 |
-
return StreamingResponse(
|
| 282 |
-
io.BytesIO(buffer.tobytes()),
|
| 283 |
-
media_type="image/jpeg",
|
| 284 |
-
headers={"X-Processing-Time": f"{elapsed_time:.4f}"}
|
| 285 |
-
)
|
|
|
|
| 4 |
import cv2
|
| 5 |
import numpy as np
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
os.environ['TF_USE_LEGACY_KERAS'] = '1'
|
|
|
|
| 8 |
os.environ["OMP_NUM_THREADS"] = "2"
|
| 9 |
os.environ["TF_NUM_INTRAOP_THREADS"] = "2"
|
| 10 |
os.environ["TF_NUM_INTEROP_THREADS"] = "1"
|
|
|
|
| 21 |
|
| 22 |
app = FastAPI()
|
| 23 |
|
|
|
|
| 24 |
app.add_middleware(
|
| 25 |
CORSMiddleware,
|
| 26 |
allow_origins=["*"],
|
|
|
|
| 30 |
expose_headers=["X-Processing-Time", "X-Model-Status"]
|
| 31 |
)
|
| 32 |
|
|
|
|
| 33 |
HF_TOKEN = os.getenv("HF_Token")
|
| 34 |
REPO_ID = "SaniaE/Car_Damage_Detection"
|
| 35 |
|
| 36 |
+
model_dir = snapshot_download(repo_id=REPO_ID, token=HF_TOKEN, local_dir="./models_data")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
PIPELINE_CONFIG = os.path.join(model_dir, "object_detection_model/pipeline.config")
|
| 39 |
CHECKPOINT_PATH = os.path.join(model_dir, "object_detection_model/ckpt-37")
|
| 40 |
LABEL_MAP_PATH = os.path.join(model_dir, "object_detection_model/label_map.pbtxt")
|
| 41 |
CNN_MODEL_PATH = os.path.join(model_dir, "cnn_filter.h5")
|
| 42 |
|
|
|
|
|
|
|
| 43 |
cnn_filter = tf.keras.models.load_model(CNN_MODEL_PATH, compile=False)
|
| 44 |
|
|
|
|
| 45 |
configs = config_util.get_configs_from_pipeline_file(PIPELINE_CONFIG)
|
| 46 |
detection_model = model_builder.build(model_config=configs['model'], is_training=False)
|
| 47 |
ckpt = tf.compat.v2.train.Checkpoint(model=detection_model)
|
|
|
|
| 55 |
detections = detection_model.postprocess(prediction_dict, shapes)
|
| 56 |
return detections
|
| 57 |
|
| 58 |
+
def get_top_predictions(detections, max_predictions=3):
|
| 59 |
+
"""Extracts top predictions matching criteria."""
|
| 60 |
scores = detections['detection_scores'][0].numpy()
|
| 61 |
+
classes = detections['detection_classes'][0].numpy().astype(int)
|
| 62 |
+
valid_indices = []
|
| 63 |
+
for idx in range(min(len(scores), max_predictions)):
|
| 64 |
+
if scores[idx] > 0.4:
|
| 65 |
+
valid_indices.append((idx, classes[idx]))
|
| 66 |
+
return valid_indices
|
| 67 |
|
| 68 |
@app.get("/")
|
| 69 |
def read_root():
|
|
|
|
| 72 |
@app.post("/predict")
|
| 73 |
async def predict(file: UploadFile = File(...)):
|
| 74 |
start_time = time.perf_counter()
|
|
|
|
|
|
|
| 75 |
contents = await file.read()
|
| 76 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 77 |
image_np = np.array(image_pil)
|
|
|
|
| 101 |
ymin, xmin, ymax, xmax = boxes[i]
|
| 102 |
(left, right, top, bottom) = (xmin * width, xmax * width,
|
| 103 |
ymin * height, ymax * height)
|
| 104 |
+
|
| 105 |
+
# Draw Box
|
| 106 |
cv2.rectangle(image_cv, (int(left), int(top)), (int(right), int(bottom)), (255, 255, 0), 2)
|
| 107 |
+
|
| 108 |
+
# OPTIMIZATION: Bounds-safe layout rendering to prevent clipping
|
| 109 |
label = f"{category_index.get(classes[i] + 1, {}).get('name', 'unknown')}: {int(scores[i]*100)}%"
|
| 110 |
+
text_y = int(top) - 10 if int(top) - 10 > 15 else int(top) + 20
|
| 111 |
+
cv2.putText(image_cv, label, (int(left), text_y),
|
| 112 |
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2)
|
| 113 |
|
| 114 |
_, buffer = cv2.imencode('.jpg', image_cv)
|
|
|
|
| 124 |
@app.post("/explain")
|
| 125 |
async def explain(file: UploadFile = File(...)):
|
| 126 |
start_time = time.perf_counter()
|
|
|
|
| 127 |
contents = await file.read()
|
| 128 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 129 |
image_np = np.array(image_pil).astype(np.float32)
|
|
|
|
| 136 |
raw_scores = prediction_dict['class_predictions_with_background'][0]
|
| 137 |
|
| 138 |
detections = detection_model.postprocess(prediction_dict, shapes)
|
| 139 |
+
valid_preds = get_top_predictions(detections, max_predictions=1)
|
| 140 |
|
| 141 |
+
if not valid_preds:
|
| 142 |
elapsed_time = time.perf_counter() - start_time
|
| 143 |
return Response(status_code=204, headers={"X-Processing-Time": f"{elapsed_time:.4f}"})
|
| 144 |
|
| 145 |
+
_, top_class = valid_preds[0]
|
| 146 |
loss = tf.reduce_max(raw_scores[:, top_class])
|
| 147 |
|
| 148 |
grads = tape.gradient(loss, input_tensor)
|
|
|
|
| 163 |
elapsed_time = time.perf_counter() - start_time
|
| 164 |
print(f"[BENCHMARK] /explain turnaround: {elapsed_time:.4f}s")
|
| 165 |
|
| 166 |
+
return StreamingResponse(io.BytesIO(buffer.tobytes()), media_type="image/jpeg", headers={"X-Processing-Time": f"{elapsed_time:.4f}"})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
@app.post("/explain/tiled")
|
| 169 |
async def explain_tiled(file: UploadFile = File(...)):
|
| 170 |
start_time = time.perf_counter()
|
|
|
|
| 171 |
contents = await file.read()
|
| 172 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 173 |
image_np = np.array(image_pil).astype(np.float32)
|
|
|
|
| 180 |
|
| 181 |
base_image = cv2.cvtColor(image_np.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
| 182 |
h_img, w_img, _ = base_image.shape
|
| 183 |
+
|
| 184 |
+
valid_preds = get_top_predictions(detections, max_predictions=3)
|
| 185 |
+
panels = [base_image]
|
| 186 |
+
|
| 187 |
+
for idx, (target_idx, _) in enumerate(valid_preds):
|
| 188 |
+
ymin, xmin, ymax, xmax = boxes[target_idx]
|
| 189 |
+
cv2.rectangle(base_image, (int(xmin*w_img), int(ymin*h_img)),
|
| 190 |
+
(int(xmax*w_img), int(ymax*h_img)), (255, 255, 0), 2)
|
| 191 |
+
|
| 192 |
+
# OPTIMIZATION: Unified single-pass Gradient Tape for all targets
|
| 193 |
+
with tf.GradientTape(persistent=True) as tape:
|
| 194 |
+
tape.watch(input_tensor)
|
| 195 |
+
image, shapes = detection_model.preprocess(input_tensor)
|
| 196 |
+
prediction_dict = detection_model.predict(image, shapes)
|
| 197 |
+
raw_scores = prediction_dict['class_predictions_with_background'][0]
|
| 198 |
|
| 199 |
+
losses = []
|
| 200 |
+
for _, target_class in valid_preds:
|
| 201 |
+
losses.append(tf.reduce_max(raw_scores[:, target_class]))
|
|
|
|
|
|
|
| 202 |
|
| 203 |
+
original_bgr_raw = cv2.cvtColor(image_np.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
| 204 |
+
|
| 205 |
+
for idx, loss in enumerate(losses):
|
| 206 |
+
target_class = valid_preds[idx][1]
|
| 207 |
+
grads = tape.gradient(loss, input_tensor)
|
| 208 |
+
saliency = np.max(np.abs(grads.numpy()), axis=-1)[0]
|
| 209 |
+
|
| 210 |
+
v_min, v_max = np.percentile(saliency, (5, 95))
|
| 211 |
+
saliency = np.clip((saliency - v_min) / (v_max - v_min + 1e-8), 0, 1)
|
| 212 |
+
heatmap = cv2.applyColorMap(np.uint8(255 * saliency), cv2.COLORMAP_JET)
|
| 213 |
+
|
| 214 |
+
overlay = cv2.addWeighted(original_bgr_raw.copy(), 0.6, heatmap, 0.4, 0)
|
| 215 |
+
|
| 216 |
+
class_name = category_index.get(target_class + 1, {}).get('name', 'unknown')
|
| 217 |
+
cv2.putText(overlay, f"Top {idx+1}: {class_name}", (10, 30),
|
| 218 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 219 |
+
panels.append(overlay)
|
| 220 |
|
| 221 |
+
del tape # Clean up persistent allocations
|
| 222 |
+
|
| 223 |
+
while len(panels) < 4:
|
| 224 |
+
panels.append(np.zeros_like(base_image))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
top_row = np.hstack((panels[0], panels[1]))
|
| 227 |
bottom_row = np.hstack((panels[2], panels[3]))
|
|
|
|
| 231 |
elapsed_time = time.perf_counter() - start_time
|
| 232 |
print(f"[BENCHMARK] /explain/tiled turnaround: {elapsed_time:.4f}s")
|
| 233 |
|
| 234 |
+
return StreamingResponse(io.BytesIO(buffer.tobytes()), media_type="image/jpeg", headers={"X-Processing-Time": f"{elapsed_time:.4f}"})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
@app.post("/explain/global")
|
| 237 |
async def explain_global(file: UploadFile = File(...)):
|
| 238 |
start_time = time.perf_counter()
|
|
|
|
| 239 |
contents = await file.read()
|
| 240 |
image_pil = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 241 |
image_np = np.array(image_pil).astype(np.float32)
|
|
|
|
| 248 |
image, shapes = detection_model.preprocess(input_tensor)
|
| 249 |
prediction_dict = detection_model.predict(image, shapes)
|
| 250 |
raw_scores = prediction_dict['class_predictions_with_background'][0]
|
| 251 |
+
|
| 252 |
foreground_scores = raw_scores[:, 1:]
|
| 253 |
+
# REFINEMENT: Take top-K elements to filter out background anchor static
|
| 254 |
+
top_anchor_values, _ = tf.math.top_k(tf.reduce_max(foreground_scores, axis=-1), k=20)
|
| 255 |
+
loss = tf.reduce_sum(top_anchor_values)
|
| 256 |
|
| 257 |
grads = tape.gradient(loss, input_tensor)
|
| 258 |
saliency = np.max(np.abs(grads.numpy()), axis=-1)[0]
|
| 259 |
|
| 260 |
+
# Dilate and blur to form clean structural contours instead of fuzz
|
| 261 |
+
v_min, v_max = np.percentile(saliency, (10, 98))
|
| 262 |
saliency = np.clip((saliency - v_min) / (v_max - v_min + 1e-8), 0, 1)
|
| 263 |
+
saliency = cv2.GaussianBlur(saliency, (9, 9), 0)
|
| 264 |
|
| 265 |
heatmap = cv2.applyColorMap(np.uint8(255 * saliency), cv2.COLORMAP_JET)
|
| 266 |
overlay = cv2.addWeighted(image_bgr, 0.6, heatmap, 0.4, 0)
|
|
|
|
| 272 |
elapsed_time = time.perf_counter() - start_time
|
| 273 |
print(f"[BENCHMARK] /explain/global turnaround: {elapsed_time:.4f}s")
|
| 274 |
|
| 275 |
+
return StreamingResponse(io.BytesIO(buffer.tobytes()), media_type="image/jpeg", headers={"X-Processing-Time": f"{elapsed_time:.4f}"})
|
|
|
|
|
|
|
|
|
|
|
|