Spaces:
Running
Running
Anish commited on
Commit ·
edd550f
1
Parent(s): ee84eed
[Feature Added] > anomaly_gif_maker. This feature detects the frame where the pipeline thinks it is AI, converts it to gif and shows a side by side comparision to the Original Video, while also annotating the 'Peak Glitch' on the AI RADIANCE MAP gif
Browse files
backend/app/ai/video/anomaly_gif_maker.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import imageio
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import uuid
|
| 6 |
+
from typing import List
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
def generate_anomaly_gif(frames: List[np.ndarray], center_idx: int) -> str:
|
| 12 |
+
try:
|
| 13 |
+
save_dir = "uploads/video_heatmaps"
|
| 14 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 15 |
+
|
| 16 |
+
safe_filename = f"{uuid.uuid4().hex}_anomaly_clip.gif"
|
| 17 |
+
save_path = os.path.join(save_dir, safe_filename)
|
| 18 |
+
|
| 19 |
+
start_idx = max(0, center_idx - 10)
|
| 20 |
+
end_idx = min(len(frames), center_idx + 10)
|
| 21 |
+
|
| 22 |
+
gif_frames = []
|
| 23 |
+
|
| 24 |
+
for i in range(start_idx, end_idx):
|
| 25 |
+
frame = frames[i]
|
| 26 |
+
|
| 27 |
+
# 1. Take the grayscale X-Ray of the current frame
|
| 28 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 29 |
+
# 2. Extract sharp structural edges
|
| 30 |
+
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
|
| 31 |
+
abs_laplacian = np.absolute(laplacian)
|
| 32 |
+
|
| 33 |
+
# 3. Normalize to force the highest anomaly to glow at maximum brightness (255)
|
| 34 |
+
laplacian_8u = cv2.normalize(abs_laplacian, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
|
| 35 |
+
# 4. Blur it to turn sharp noise into soft glowing clouds
|
| 36 |
+
heatmap_blurred = cv2.GaussianBlur(laplacian_8u, (15, 15), 0)
|
| 37 |
+
|
| 38 |
+
# 5. Filter out natural background camera noise (anything under 70 brightness)
|
| 39 |
+
_, heatmap_mask = cv2.threshold(heatmap_blurred, 70, 255, cv2.THRESH_TOZERO)
|
| 40 |
+
# 6. Apply Jet Thermal Colors (Blue = natural, Red = AI Glitch)
|
| 41 |
+
colored_heatmap = cv2.applyColorMap(heatmap_mask, cv2.COLORMAP_JET)
|
| 42 |
+
|
| 43 |
+
# 7. Apply the thermal glow onto a copy of the video frame
|
| 44 |
+
heatmap_overlay = cv2.addWeighted(frame.copy(), 0.5, colored_heatmap, 0.7, 0)
|
| 45 |
+
|
| 46 |
+
# 8. Find the contours (puddles) of the highest glowing areas and draw Red Boxes
|
| 47 |
+
_, thresh = cv2.threshold(laplacian_8u, 100, 255, cv2.THRESH_BINARY)
|
| 48 |
+
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 49 |
+
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:3]
|
| 50 |
+
|
| 51 |
+
for contour in contours:
|
| 52 |
+
if cv2.contourArea(contour) > 50:
|
| 53 |
+
x, y, w, h = cv2.boundingRect(contour)
|
| 54 |
+
cv2.rectangle(heatmap_overlay, (x, y), (x+w, y+h), (0, 0, 255), 2)
|
| 55 |
+
|
| 56 |
+
# --- CREATE THE SIDE-BY-SIDE SPLIT SCREEN ---
|
| 57 |
+
|
| 58 |
+
# 9. Resize both halves by 50% so the final GIF file size stays small
|
| 59 |
+
h, w = frame.shape[:2]
|
| 60 |
+
small_original = cv2.resize(frame, (w//2, h//2))
|
| 61 |
+
small_heatmap = cv2.resize(heatmap_overlay, (w//2, h//2))
|
| 62 |
+
|
| 63 |
+
# 10. Add clear text labels to the top of both screens
|
| 64 |
+
cv2.putText(small_original, "ORIGINAL", (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 65 |
+
cv2.putText(small_heatmap, "AI RADIANCE MAP", (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
|
| 66 |
+
|
| 67 |
+
# 11. Add an extra flashing text warning when it reaches the absolute worst frame
|
| 68 |
+
if i == center_idx:
|
| 69 |
+
cv2.putText(small_heatmap, "PEAK GLITCH", (10, 55), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
| 70 |
+
|
| 71 |
+
# 12. Mathematically glue the two images together side-by-side using axis=1 (horizontal)
|
| 72 |
+
combined_frame = np.concatenate((small_original, small_heatmap), axis=1)
|
| 73 |
+
|
| 74 |
+
# 13. Convert from BGR back to RGB before saving so the GIF colors aren't inverted
|
| 75 |
+
rgb_frame = cv2.cvtColor(combined_frame, cv2.COLOR_BGR2RGB)
|
| 76 |
+
gif_frames.append(rgb_frame)
|
| 77 |
+
|
| 78 |
+
# 13b. If this is the absolute peak glitch, append it 8 extra times to create a 1.2-second "Freeze-Frame" pause!
|
| 79 |
+
if i == center_idx:
|
| 80 |
+
for _ in range(8):
|
| 81 |
+
gif_frames.append(rgb_frame)
|
| 82 |
+
|
| 83 |
+
# 14. Synthesize the final GIF animation (Slowed down to ~6.5fps for better readability, and loop=0 for infinite looping)
|
| 84 |
+
imageio.mimsave(save_path, gif_frames, format='GIF', duration=5, loop=0)
|
| 85 |
+
|
| 86 |
+
return save_path
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Failed to generate Anomaly GIF Clip: {str(e)}")
|
| 90 |
+
return None
|