Spaces:
Paused
Paused
Initial Space upload
Browse files- .gitattributes +10 -0
- README.md +72 -5
- app.py +363 -0
- config_local.py +32 -0
- confusion_matrix.png +3 -0
- descriptive_prompts.py +106 -0
- example_videos.py +119 -0
- examples/Abuse005_x264.mp4 +3 -0
- examples/Arson003_x264.mp4 +3 -0
- examples/Explosion001_x264.mp4 +3 -0
- examples/Fighting004_x264.mp4 +3 -0
- examples/Normal_Videos_150_x264.mp4 +3 -0
- examples/Robbery005_x264.mp4 +3 -0
- examples/Shoplifting001_x264.mp4 +3 -0
- examples/Stealing002_x264.mp4 +3 -0
- inference.py +283 -0
- per_class_f1.png +3 -0
- requirements.txt +7 -0
- summary_table.png +0 -0
- training_curve.png +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,13 @@ saved_model/**/* 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
confusion_matrix.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/Abuse005_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/Arson003_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/Explosion001_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
examples/Fighting004_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/Normal_Videos_150_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
examples/Robbery005_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
examples/Shoplifting001_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
examples/Stealing002_x264.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
per_class_f1.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,12 +1,79 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.12.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: X-CLIP Crime Classifier
|
| 3 |
+
emoji: π¬
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "6.12.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: X-CLIP fine-tuned on UCF-Crime with LLM descriptions
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Crime Detection with X-CLIP + LLM Descriptions
|
| 15 |
+
|
| 16 |
+
Fine-tunes [X-CLIP](https://huggingface.co/microsoft/xclip-base-patch16) on the
|
| 17 |
+
UCF-Crime dataset using rich scene descriptions generated by Google Gemini Flash,
|
| 18 |
+
instead of bare category labels. Multi-class classification over 14 categories
|
| 19 |
+
(13 crime types + Normal).
|
| 20 |
+
|
| 21 |
+
## Quick try
|
| 22 |
+
|
| 23 |
+
Click any of the 8 example videos on the **Live Prediction** tab. Each example is
|
| 24 |
+
labelled with one of three scenarios:
|
| 25 |
+
|
| 26 |
+
- π’ **All pass** β simple case where CLIP, X-CLIP, and our model all get it right
|
| 27 |
+
- π‘ **Ours only** β baselines fail, the fine-tuned model recovers it
|
| 28 |
+
- π΄ **All fail** β open limitation (mostly visually ambiguous classes like Abuse / Robbery)
|
| 29 |
+
|
| 30 |
+
The **Method Comparison** tab runs all three side-by-side on the same video
|
| 31 |
+
(slower β ~45s on the free CPU tier because models are swapped sequentially).
|
| 32 |
+
|
| 33 |
+
## Method in one paragraph
|
| 34 |
+
|
| 35 |
+
Each of the 532 training videos gets a unique paragraph-long description from
|
| 36 |
+
Gemini Flash. These pairs are used to contrastively fine-tune X-CLIP's visual
|
| 37 |
+
encoder (last 3 transformer blocks + CCT + MIT). At inference, we average 5
|
| 38 |
+
hand-crafted descriptive prompts per class and classify test videos by cosine
|
| 39 |
+
similarity in the shared embedding space.
|
| 40 |
+
|
| 41 |
+
## Numbers (UCF-Crime Action Recognition test split, 168 videos)
|
| 42 |
+
|
| 43 |
+
| Method | Accuracy | F1 (macro) | AUC-ROC |
|
| 44 |
+
|---|---|---|---|
|
| 45 |
+
| CLIP zero-shot | 23.2% | 21.0% | 0.77 |
|
| 46 |
+
| X-CLIP zero-shot | 17.3% | 13.9% | 0.70 |
|
| 47 |
+
| **Ours (X-CLIP + LLM)** | **26.2%** | **22.1%** | **0.80** |
|
| 48 |
+
|
| 49 |
+
## Free CPU tier note
|
| 50 |
+
|
| 51 |
+
This Space runs on CPU. Inference takes roughly 30-60 seconds per video on the
|
| 52 |
+
Live Prediction tab, and ~2-3 minutes on the Method Comparison tab (3 models
|
| 53 |
+
loaded sequentially). GPU-backed inference (~2-5s per video) would require a
|
| 54 |
+
paid Space upgrade.
|
| 55 |
+
|
| 56 |
+
## Dataset & license
|
| 57 |
+
|
| 58 |
+
- **Dataset:** [UCF-Crime](https://webpages.uncc.edu/cchen62/dataset.html)
|
| 59 |
+
(Sultani et al., CVPR 2018). Used here under its research-only license.
|
| 60 |
+
- **Example videos:** 8 clips from the public UCF-Crime test split.
|
| 61 |
+
- **Code:** MIT.
|
| 62 |
+
|
| 63 |
+
## Citation
|
| 64 |
+
|
| 65 |
+
If this project is useful to you, please cite the original UCF-Crime and X-CLIP papers:
|
| 66 |
+
|
| 67 |
+
```
|
| 68 |
+
@inproceedings{sultani2018real,
|
| 69 |
+
title={Real-world anomaly detection in surveillance videos},
|
| 70 |
+
author={Sultani, Waqas and Chen, Chen and Shah, Mubarak},
|
| 71 |
+
booktitle={CVPR}, year={2018}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@inproceedings{ni2022expanding,
|
| 75 |
+
title={Expanding language-image pretrained models for general video recognition},
|
| 76 |
+
author={Ni, Bolin and others},
|
| 77 |
+
booktitle={ECCV}, year={2022}
|
| 78 |
+
}
|
| 79 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio app for the Hugging Face Space.
|
| 2 |
+
|
| 3 |
+
Adapted from the local demo. Entry point: `app.py` at the Space root.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
import config_local as config
|
| 14 |
+
from descriptive_prompts import PROMPTS
|
| 15 |
+
from inference import (
|
| 16 |
+
CLASS_DISPLAY,
|
| 17 |
+
extract_frames_from_video,
|
| 18 |
+
predict_clip_zs,
|
| 19 |
+
predict_ours,
|
| 20 |
+
predict_xclip_zs,
|
| 21 |
+
)
|
| 22 |
+
from example_videos import EXAMPLES, get_example_video_paths
|
| 23 |
+
|
| 24 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 25 |
+
PLOTS_DIR = APP_DIR
|
| 26 |
+
|
| 27 |
+
# Map filename -> example dict for ground-truth display
|
| 28 |
+
_EXAMPLE_BY_FILENAME: dict[str, dict] = {
|
| 29 |
+
f"{ex['name']}.mp4": ex for ex in EXAMPLES
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _prepare_examples() -> list[list]:
|
| 34 |
+
paths = get_example_video_paths(APP_DIR / "examples")
|
| 35 |
+
return [[str(p)] for p in paths if p.exists()]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ββ Tab 1: Live Prediction ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def classify_video(video_file: str | None, progress=gr.Progress()) -> tuple:
|
| 42 |
+
if not video_file:
|
| 43 |
+
return None, None, "_Pick an example or upload a video to begin._"
|
| 44 |
+
|
| 45 |
+
progress(0.1, desc="Extracting 8 frames from video...")
|
| 46 |
+
try:
|
| 47 |
+
frames = extract_frames_from_video(video_file, num_frames=config.NUM_FRAMES)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
return None, None, f"**Error extracting frames:** {e}"
|
| 50 |
+
|
| 51 |
+
progress(0.5, desc="Running fine-tuned X-CLIP inference (may take 30-60s on CPU)...")
|
| 52 |
+
try:
|
| 53 |
+
result = predict_ours(frames)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
return None, None, f"**Error during inference:** {e}"
|
| 56 |
+
|
| 57 |
+
gallery = [(frame, f"Frame {i+1}/8") for i, frame in enumerate(frames)]
|
| 58 |
+
label_dict = result["all_scores"]
|
| 59 |
+
|
| 60 |
+
filename = Path(video_file).name
|
| 61 |
+
gt_msg = ""
|
| 62 |
+
if filename in _EXAMPLE_BY_FILENAME:
|
| 63 |
+
ex = _EXAMPLE_BY_FILENAME[filename]
|
| 64 |
+
gt_display = CLASS_DISPLAY[ex["category"]]
|
| 65 |
+
pred = result["top3_labels"][0]
|
| 66 |
+
scenario = ex.get("scenario", "")
|
| 67 |
+
preds = ex.get("preds", {})
|
| 68 |
+
scenario_tag = {
|
| 69 |
+
"ALL_PASS": "\U0001f7e2 All 3 methods agree",
|
| 70 |
+
"OURS_ONLY": "\U0001f4a1 Only Ours gets this right",
|
| 71 |
+
"ALL_FAIL": "\U0001f534 Known failure: all 3 methods are wrong",
|
| 72 |
+
}.get(scenario, "")
|
| 73 |
+
|
| 74 |
+
ok_icon = "\u2705" if pred == gt_display else "\u274c"
|
| 75 |
+
header = f"### {ok_icon} Ground Truth: **{gt_display}** Predicted: **{pred}**"
|
| 76 |
+
if scenario_tag:
|
| 77 |
+
header += f"\n\n{scenario_tag}"
|
| 78 |
+
if preds:
|
| 79 |
+
header += (
|
| 80 |
+
f"\n\n| Method | Prediction |\n|---|---|\n"
|
| 81 |
+
f"| CLIP zero-shot | {preds.get('clip', '?')} |\n"
|
| 82 |
+
f"| X-CLIP zero-shot | {preds.get('xclip', '?')} |\n"
|
| 83 |
+
f"| **Ours** | **{pred}** |\n"
|
| 84 |
+
)
|
| 85 |
+
header += f"\n\n_{ex['note']}_"
|
| 86 |
+
gt_msg = header
|
| 87 |
+
else:
|
| 88 |
+
gt_msg = f"### Predicted: **{result['top3_labels'][0]}**"
|
| 89 |
+
|
| 90 |
+
progress(1.0, desc="Done!")
|
| 91 |
+
return gallery, label_dict, gt_msg
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# ββ Tab 2: Method Comparison ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def compare_methods(video_file: str | None, progress=gr.Progress()) -> tuple:
|
| 98 |
+
if not video_file:
|
| 99 |
+
empty = {"(no video)": 1.0}
|
| 100 |
+
return None, empty, empty, empty, "Please upload or select a video first."
|
| 101 |
+
|
| 102 |
+
progress(0.05, desc="Extracting frames...")
|
| 103 |
+
try:
|
| 104 |
+
frames = extract_frames_from_video(video_file, num_frames=config.NUM_FRAMES)
|
| 105 |
+
except Exception as e:
|
| 106 |
+
empty = {"(error)": 1.0}
|
| 107 |
+
return None, empty, empty, empty, f"Error: {e}"
|
| 108 |
+
|
| 109 |
+
gallery = [(frame, f"Frame {i+1}/8") for i, frame in enumerate(frames)]
|
| 110 |
+
|
| 111 |
+
progress(0.20, desc="Running CLIP zero-shot...")
|
| 112 |
+
clip_result = predict_clip_zs(frames)
|
| 113 |
+
|
| 114 |
+
progress(0.50, desc="Running X-CLIP zero-shot...")
|
| 115 |
+
xclip_result = predict_xclip_zs(frames)
|
| 116 |
+
|
| 117 |
+
progress(0.80, desc="Running our fine-tuned model...")
|
| 118 |
+
ours_result = predict_ours(frames)
|
| 119 |
+
|
| 120 |
+
filename = Path(video_file).name
|
| 121 |
+
gt_msg = ""
|
| 122 |
+
if filename in _EXAMPLE_BY_FILENAME:
|
| 123 |
+
ex = _EXAMPLE_BY_FILENAME[filename]
|
| 124 |
+
gt = CLASS_DISPLAY[ex["category"]]
|
| 125 |
+
clip_ok = "\u2705" if clip_result["top3_labels"][0] == gt else "\u274c"
|
| 126 |
+
xclip_ok = "\u2705" if xclip_result["top3_labels"][0] == gt else "\u274c"
|
| 127 |
+
ours_ok = "\u2705" if ours_result["top3_labels"][0] == gt else "\u274c"
|
| 128 |
+
gt_msg = (
|
| 129 |
+
f"### Ground Truth: **{gt}**\n"
|
| 130 |
+
f"- CLIP zero-shot: {clip_ok} (predicted: {clip_result['top3_labels'][0]})\n"
|
| 131 |
+
f"- X-CLIP zero-shot: {xclip_ok} (predicted: {xclip_result['top3_labels'][0]})\n"
|
| 132 |
+
f"- **Ours:** {ours_ok} (predicted: {ours_result['top3_labels'][0]})\n\n"
|
| 133 |
+
f"_{ex['note']}_"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
progress(1.0, desc="Done!")
|
| 137 |
+
return gallery, clip_result["all_scores"], xclip_result["all_scores"], ours_result["all_scores"], gt_msg
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# ββ Tab 3: How It Works ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
HOW_IT_WORKS_INTRO = """
|
| 144 |
+
## The Idea
|
| 145 |
+
|
| 146 |
+
Standard zero-shot classifiers ask a video-language model: *"Is this a video of robbery?"*
|
| 147 |
+
The model has only the **word** to work with.
|
| 148 |
+
|
| 149 |
+
We instead train it with rich, scene-level descriptions:
|
| 150 |
+
- **Training (per video):** Each of 532 training videos gets a unique description from Gemini Flash.
|
| 151 |
+
- **Inference (per class):** 5 hand-crafted descriptive prompts per class are averaged to form
|
| 152 |
+
the class embedding.
|
| 153 |
+
|
| 154 |
+
This gives the text encoder much more information about *what each crime actually looks like*.
|
| 155 |
+
|
| 156 |
+
## Pipeline
|
| 157 |
+
|
| 158 |
+
```
|
| 159 |
+
[Video] -> [Extract 8 frames] -> [X-CLIP visual encoder] -> [Video embedding]
|
| 160 |
+
|
|
| 161 |
+
v
|
| 162 |
+
[LLM description] -> [X-CLIP text encoder] -> [Text embedding] -> [Cosine similarity] -> [Class]
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
## What's frozen vs. trained
|
| 166 |
+
|
| 167 |
+
We freeze the first 9 of 12 transformer blocks in both encoders, training only:
|
| 168 |
+
- The last 3 transformer blocks (visual + text)
|
| 169 |
+
- X-CLIP's cross-frame communication module (CCT)
|
| 170 |
+
- The multi-frame integration transformer (MIT)
|
| 171 |
+
- Projection heads
|
| 172 |
+
|
| 173 |
+
That's about **50M trainable parameters out of 195M** (~25%).
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
SAMPLE_TRAINING_DESC = """**An example Gemini description** from the training set (Robbery):
|
| 178 |
+
|
| 179 |
+
> _A dimly lit convenience store is shown. A person wearing dark clothing and a hood enters the frame holding what appears to be a firearm, pointing it at the cashier behind the counter. The cashier raises their hands in apparent compliance. The aggressor moves quickly around the counter area, and after several seconds of tense interaction leaves through the main door. The confrontation is brief but clearly threatening, and the victim's body language (raised hands, stiff posture) indicates they perceive a real threat._
|
| 180 |
+
|
| 181 |
+
Every training video has its own unique paragraph like this one, generated by Gemini Flash over 8 sampled frames.
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _format_sample_prompts() -> str:
|
| 186 |
+
prompts = PROMPTS["Robbery"]
|
| 187 |
+
out = "**Inference prompts for `Robbery`** (averaged into one class embedding):\n\n"
|
| 188 |
+
for i, p in enumerate(prompts, 1):
|
| 189 |
+
out += f"{i}. *{p}*\n"
|
| 190 |
+
return out
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ββ Tab 4: Results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
RESULTS_INTRO = """
|
| 197 |
+
## Results on UCF-Crime Action Recognition Test Set
|
| 198 |
+
|
| 199 |
+
168 test videos, 14 classes, 12 samples per class.
|
| 200 |
+
|
| 201 |
+
| Method | Accuracy | F1 (macro) | AUC-ROC |
|
| 202 |
+
|---|---|---|---|
|
| 203 |
+
| CLIP zero-shot | 23.2% | 21.0% | 0.77 |
|
| 204 |
+
| X-CLIP zero-shot | 17.3% | 13.9% | 0.70 |
|
| 205 |
+
| **Ours (X-CLIP + LLM descriptions)** | **26.2%** | **22.1%** | **0.80** |
|
| 206 |
+
|
| 207 |
+
The 10-point AUC-ROC improvement (0.70 -> 0.80) over the base X-CLIP shows the model
|
| 208 |
+
has learned meaningful structure even when its top-1 pick is wrong.
|
| 209 |
+
"""
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def build_app() -> gr.Blocks:
|
| 213 |
+
examples_list = _prepare_examples()
|
| 214 |
+
|
| 215 |
+
with gr.Blocks(title="X-CLIP Crime Classifier") as app:
|
| 216 |
+
gr.Markdown(
|
| 217 |
+
"# Crime Detection with X-CLIP + LLM Descriptions\n\n"
|
| 218 |
+
"Multi-class crime classification on the **UCF-Crime** dataset, using "
|
| 219 |
+
"X-CLIP fine-tuned with per-video descriptions from Gemini Flash. "
|
| 220 |
+
"**Click any example below** to see it in action. "
|
| 221 |
+
"_(Running on free CPU tier β first prediction takes 30-60s while "
|
| 222 |
+
"the model downloads and loads.)_"
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
with gr.Tabs():
|
| 226 |
+
# Tab 1
|
| 227 |
+
with gr.Tab("\U0001f3ac Live Prediction"):
|
| 228 |
+
gr.Markdown(
|
| 229 |
+
"Pick an example below (auto-classifies) or upload your own "
|
| 230 |
+
"video and click **Classify**."
|
| 231 |
+
)
|
| 232 |
+
with gr.Row():
|
| 233 |
+
with gr.Column(scale=1):
|
| 234 |
+
video_input = gr.Video(label="Input Video", height=320)
|
| 235 |
+
gr.Examples(
|
| 236 |
+
examples=examples_list,
|
| 237 |
+
inputs=[video_input],
|
| 238 |
+
outputs=[],
|
| 239 |
+
fn=lambda x: x,
|
| 240 |
+
label="Example Videos (click to load and classify)",
|
| 241 |
+
examples_per_page=8,
|
| 242 |
+
run_on_click=False,
|
| 243 |
+
)
|
| 244 |
+
classify_btn = gr.Button("Classify", variant="primary", size="lg")
|
| 245 |
+
with gr.Column(scale=1):
|
| 246 |
+
gt_status = gr.Markdown("")
|
| 247 |
+
confidence = gr.Label(
|
| 248 |
+
num_top_classes=5,
|
| 249 |
+
label="Top-5 Predictions (confidence)",
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
gr.Markdown("### What the model sees: 8 uniformly-sampled frames")
|
| 253 |
+
frame_gallery = gr.Gallery(
|
| 254 |
+
label="Extracted Frames", columns=4, rows=2,
|
| 255 |
+
height=280, object_fit="contain",
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
classify_btn.click(
|
| 259 |
+
fn=classify_video, inputs=[video_input],
|
| 260 |
+
outputs=[frame_gallery, confidence, gt_status],
|
| 261 |
+
)
|
| 262 |
+
video_input.change(
|
| 263 |
+
fn=classify_video, inputs=[video_input],
|
| 264 |
+
outputs=[frame_gallery, confidence, gt_status],
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# Tab 2
|
| 268 |
+
with gr.Tab("\u2696\ufe0f Method Comparison"):
|
| 269 |
+
gr.Markdown(
|
| 270 |
+
"Run the same video through all three methods. "
|
| 271 |
+
"Takes ~2-3 minutes on the free CPU tier because the three "
|
| 272 |
+
"models must be loaded sequentially."
|
| 273 |
+
)
|
| 274 |
+
with gr.Row():
|
| 275 |
+
with gr.Column(scale=1):
|
| 276 |
+
comp_video_input = gr.Video(label="Input Video", height=280)
|
| 277 |
+
gr.Examples(
|
| 278 |
+
examples=examples_list,
|
| 279 |
+
inputs=[comp_video_input],
|
| 280 |
+
label="Example Videos",
|
| 281 |
+
examples_per_page=8,
|
| 282 |
+
)
|
| 283 |
+
compare_btn = gr.Button("Run Comparison", variant="primary", size="lg")
|
| 284 |
+
with gr.Column(scale=1):
|
| 285 |
+
comp_gt = gr.Markdown("")
|
| 286 |
+
|
| 287 |
+
with gr.Row():
|
| 288 |
+
with gr.Column():
|
| 289 |
+
gr.Markdown("### CLIP zero-shot")
|
| 290 |
+
clip_label = gr.Label(num_top_classes=5)
|
| 291 |
+
with gr.Column():
|
| 292 |
+
gr.Markdown("### X-CLIP zero-shot")
|
| 293 |
+
xclip_label = gr.Label(num_top_classes=5)
|
| 294 |
+
with gr.Column():
|
| 295 |
+
gr.Markdown("### Ours (X-CLIP + LLM)")
|
| 296 |
+
ours_label = gr.Label(num_top_classes=5)
|
| 297 |
+
|
| 298 |
+
gr.Markdown("### Frames")
|
| 299 |
+
comp_gallery = gr.Gallery(
|
| 300 |
+
label="Extracted Frames", columns=4, rows=2,
|
| 301 |
+
height=240, object_fit="contain",
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
compare_btn.click(
|
| 305 |
+
fn=compare_methods, inputs=[comp_video_input],
|
| 306 |
+
outputs=[comp_gallery, clip_label, xclip_label, ours_label, comp_gt],
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
# Tab 3
|
| 310 |
+
with gr.Tab("\U0001f4d6 How It Works"):
|
| 311 |
+
gr.Markdown(HOW_IT_WORKS_INTRO)
|
| 312 |
+
with gr.Row():
|
| 313 |
+
with gr.Column():
|
| 314 |
+
gr.Markdown("### Sample Training Description")
|
| 315 |
+
gr.Markdown(SAMPLE_TRAINING_DESC)
|
| 316 |
+
with gr.Column():
|
| 317 |
+
gr.Markdown("### Sample Inference Prompts")
|
| 318 |
+
gr.Markdown(_format_sample_prompts())
|
| 319 |
+
|
| 320 |
+
# Tab 4
|
| 321 |
+
with gr.Tab("\U0001f4ca Results"):
|
| 322 |
+
gr.Markdown(RESULTS_INTRO)
|
| 323 |
+
with gr.Row():
|
| 324 |
+
with gr.Column():
|
| 325 |
+
gr.Markdown("### Training Loss")
|
| 326 |
+
if (PLOTS_DIR / "training_curve.png").exists():
|
| 327 |
+
gr.Image(value=str(PLOTS_DIR / "training_curve.png"),
|
| 328 |
+
label="Loss converges from 0.72 to 0.0005",
|
| 329 |
+
show_label=True, height=280)
|
| 330 |
+
with gr.Column():
|
| 331 |
+
gr.Markdown("### Per-Class F1")
|
| 332 |
+
if (PLOTS_DIR / "per_class_f1.png").exists():
|
| 333 |
+
gr.Image(value=str(PLOTS_DIR / "per_class_f1.png"),
|
| 334 |
+
label="Three-way comparison",
|
| 335 |
+
show_label=True, height=280)
|
| 336 |
+
with gr.Row():
|
| 337 |
+
with gr.Column():
|
| 338 |
+
gr.Markdown("### Confusion Matrix (Ours)")
|
| 339 |
+
if (PLOTS_DIR / "confusion_matrix.png").exists():
|
| 340 |
+
gr.Image(value=str(PLOTS_DIR / "confusion_matrix.png"),
|
| 341 |
+
label="Robbery is consistently confused with Shoplifting",
|
| 342 |
+
show_label=True, height=420)
|
| 343 |
+
with gr.Column():
|
| 344 |
+
gr.Markdown("### Summary Table")
|
| 345 |
+
if (PLOTS_DIR / "summary_table.png").exists():
|
| 346 |
+
gr.Image(value=str(PLOTS_DIR / "summary_table.png"),
|
| 347 |
+
label="Method comparison",
|
| 348 |
+
show_label=True, height=420)
|
| 349 |
+
|
| 350 |
+
gr.Markdown(
|
| 351 |
+
"---\n"
|
| 352 |
+
"_Powered by [X-CLIP](https://huggingface.co/microsoft/xclip-base-patch16) "
|
| 353 |
+
"fine-tuned on UCF-Crime with descriptions from "
|
| 354 |
+
"[Gemini Flash](https://ai.google.dev/gemini-api). "
|
| 355 |
+
"Built with [Gradio](https://gradio.app)._"
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
return app
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
if __name__ == "__main__":
|
| 362 |
+
app = build_app()
|
| 363 |
+
app.launch()
|
config_local.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal config for the Hugging Face Space.
|
| 2 |
+
|
| 3 |
+
A stripped-down version of the project's config.py β only the constants
|
| 4 |
+
needed at inference time. Dataset paths are removed since the Space runs
|
| 5 |
+
with the bundled example videos only.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# ββ Classes (14 total: 13 anomaly + Normal) βββββββββββββββββββββββββββββββ
|
| 11 |
+
CLASSES = [
|
| 12 |
+
"Abuse", "Arrest", "Arson", "Assault", "Burglary",
|
| 13 |
+
"Explosion", "Fighting", "Normal_Videos_event",
|
| 14 |
+
"RoadAccidents", "Robbery", "Shooting",
|
| 15 |
+
"Shoplifting", "Stealing", "Vandalism",
|
| 16 |
+
]
|
| 17 |
+
NUM_CLASSES = len(CLASSES)
|
| 18 |
+
|
| 19 |
+
# ββ Inference βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
NUM_FRAMES = 8
|
| 21 |
+
IMAGE_SIZE = 224
|
| 22 |
+
NUM_PROMPTS_PER_CLASS = 5
|
| 23 |
+
|
| 24 |
+
# ββ Paths (relative to this file) βββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 26 |
+
EXAMPLES_DIR = APP_DIR / "examples"
|
| 27 |
+
PLOTS_DIR = APP_DIR
|
| 28 |
+
|
| 29 |
+
# Fine-tuned checkpoint is downloaded from the HF Model repo at startup.
|
| 30 |
+
# Set HF_MODEL_REPO to the repo ID holding best_model.pt.
|
| 31 |
+
HF_MODEL_REPO = "yashppawar/xclip-ucf-crime"
|
| 32 |
+
HF_MODEL_FILENAME = "best_model.pt"
|
confusion_matrix.png
ADDED
|
Git LFS Details
|
descriptive_prompts.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Descriptive prompts for each crime category (M=5 per class).
|
| 2 |
+
|
| 3 |
+
Used at inference time: each prompt is encoded by the text encoder,
|
| 4 |
+
averaged per category, then compared to video embeddings via cosine similarity.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
PROMPTS = {
|
| 8 |
+
"Abuse": [
|
| 9 |
+
"A person physically strikes, pushes, or forcefully handles another individual who appears defenseless or restrained in a confined space.",
|
| 10 |
+
"An aggressor repeatedly hits or kicks a victim who is on the ground or unable to fight back, showing a clear power imbalance.",
|
| 11 |
+
"One individual dominates another through physical force, with the victim showing signs of distress, pain, or submission.",
|
| 12 |
+
"A person violently grabs, shoves, or beats another person in what appears to be a domestic or interpersonal conflict.",
|
| 13 |
+
"An individual is being physically mistreated by another, with visible signs of coercion, restraint, or repeated violent contact.",
|
| 14 |
+
],
|
| 15 |
+
"Arrest": [
|
| 16 |
+
"Law enforcement officers physically restrain a person, placing them in handcuffs or pinning them against a surface.",
|
| 17 |
+
"Police officers approach and detain a suspect, using physical force to bring them under control.",
|
| 18 |
+
"Multiple uniformed officers surround and apprehend an individual, often forcing them to the ground.",
|
| 19 |
+
"A person is being taken into custody by authorities, with visible restraining actions like handcuffing or arm-locking.",
|
| 20 |
+
"Officers chase and tackle a suspect, then proceed to restrain and escort them to a vehicle.",
|
| 21 |
+
],
|
| 22 |
+
"Arson": [
|
| 23 |
+
"A person deliberately sets fire to a building, vehicle, or property, with flames visibly spreading from the ignition point.",
|
| 24 |
+
"An individual uses an accelerant or lighter to ignite materials, causing a fire that grows rapidly in the scene.",
|
| 25 |
+
"Flames erupt from a structure or object after a person is seen placing or throwing an incendiary device.",
|
| 26 |
+
"A deliberate fire is started by an individual, with smoke and flames engulfing the targeted property.",
|
| 27 |
+
"Someone intentionally lights a fire that spreads to nearby structures, with the perpetrator fleeing the scene.",
|
| 28 |
+
],
|
| 29 |
+
"Assault": [
|
| 30 |
+
"Two or more individuals engage in a one-sided physical attack where one person overpowers another with punches, kicks, or weapons.",
|
| 31 |
+
"An aggressor attacks a victim unprovoked, striking them repeatedly while the victim attempts to defend or escape.",
|
| 32 |
+
"A person is violently attacked by one or more assailants in a public space, with clear aggressive intent.",
|
| 33 |
+
"An individual uses a weapon or physical force to harm another person who is caught off guard or unable to resist.",
|
| 34 |
+
"A sudden violent confrontation where one party aggressively attacks another, causing visible injury or distress.",
|
| 35 |
+
],
|
| 36 |
+
"Burglary": [
|
| 37 |
+
"A person breaks into a building or property by forcing open doors, windows, or locks, often under cover of darkness.",
|
| 38 |
+
"An individual enters a closed establishment through unauthorized means, such as climbing through a window or picking a lock.",
|
| 39 |
+
"A person is seen sneaking into a building, searching through rooms, and taking items while trying to avoid detection.",
|
| 40 |
+
"Someone forces entry into a premises when it appears unoccupied, systematically searching for valuables.",
|
| 41 |
+
"An intruder breaks into a home or business, moving cautiously through the space while gathering stolen goods.",
|
| 42 |
+
],
|
| 43 |
+
"Explosion": [
|
| 44 |
+
"A sudden violent blast occurs, sending debris, smoke, and shockwaves through the environment, damaging surrounding structures.",
|
| 45 |
+
"A detonation creates a large fireball and plume of smoke, with visible destruction to vehicles or buildings nearby.",
|
| 46 |
+
"An explosive device goes off in a public or commercial area, causing immediate destruction and chaos.",
|
| 47 |
+
"A powerful blast rips through a location, with the explosion captured as a bright flash followed by expanding smoke and debris.",
|
| 48 |
+
"An explosion destroys part of a structure, with fire, flying debris, and a visible shockwave emanating from the blast point.",
|
| 49 |
+
],
|
| 50 |
+
"Fighting": [
|
| 51 |
+
"Two or more individuals engage in mutual physical combat, exchanging punches, kicks, and grappling with each other.",
|
| 52 |
+
"A group of people are involved in a brawl, with multiple participants throwing punches and wrestling on the ground.",
|
| 53 |
+
"Two people fight aggressively in a public space, with both parties actively attacking and defending.",
|
| 54 |
+
"A violent altercation between multiple individuals, with mutual aggression and no clear victim or aggressor.",
|
| 55 |
+
"People engage in a physical confrontation, shoving, hitting, and tackling each other in a chaotic scene.",
|
| 56 |
+
],
|
| 57 |
+
"Normal_Videos_event": [
|
| 58 |
+
"People walk, stand, or sit in a public space, going about routine daily activities with no unusual behavior.",
|
| 59 |
+
"Normal pedestrian and vehicle traffic flows through a street or parking lot with no signs of disturbance.",
|
| 60 |
+
"Individuals engage in everyday activities such as shopping, walking, or socializing in a calm environment.",
|
| 61 |
+
"A typical surveillance scene showing regular human movement and interactions without any anomalous events.",
|
| 62 |
+
"People move naturally through a space, with standard patterns of walking, entering, and exiting buildings.",
|
| 63 |
+
],
|
| 64 |
+
"RoadAccidents": [
|
| 65 |
+
"Vehicles collide at an intersection or on a road, with visible impact damage and debris scattered across the roadway.",
|
| 66 |
+
"A car crashes into another vehicle or object, causing visible damage, spinning, or flipping of the vehicles involved.",
|
| 67 |
+
"A traffic accident occurs where vehicles suddenly collide, often with one running a signal or losing control.",
|
| 68 |
+
"Multiple vehicles are involved in a collision, with the impact causing cars to spin, crumple, or come to abrupt stops.",
|
| 69 |
+
"A vehicle loses control and crashes, with visible signs of impact such as shattered glass, deformed metal, and skid marks.",
|
| 70 |
+
],
|
| 71 |
+
"Robbery": [
|
| 72 |
+
"A person threatens another with a weapon and forcibly takes their belongings in a store or street.",
|
| 73 |
+
"An armed individual demands valuables from a victim, using intimidation and the threat of violence.",
|
| 74 |
+
"A person confronts a store clerk or pedestrian with a weapon, stealing cash or property through force.",
|
| 75 |
+
"An individual uses clear threats and force to obtain personal belongings from a frightened victim.",
|
| 76 |
+
"A robbery occurs where the perpetrator uses a weapon to threaten the victim while stealing their possessions.",
|
| 77 |
+
],
|
| 78 |
+
"Shooting": [
|
| 79 |
+
"A person fires a gun at another individual or into a crowd, with visible muzzle flash or victims falling.",
|
| 80 |
+
"An armed individual discharges a firearm in a public space, causing people to flee in panic.",
|
| 81 |
+
"A shooting incident where a person aims and fires a weapon, with visible recoil and people reacting in fear.",
|
| 82 |
+
"Gunshots are fired in a confrontation, with individuals seen drawing weapons and shooting at targets.",
|
| 83 |
+
"A violent shooting occurs where one or more individuals fire weapons, causing casualties or widespread panic.",
|
| 84 |
+
],
|
| 85 |
+
"Shoplifting": [
|
| 86 |
+
"A person discreetly conceals merchandise on their person while inside a retail store, looking around nervously.",
|
| 87 |
+
"An individual picks up store items and hides them in their bag or clothing, attempting to leave without paying.",
|
| 88 |
+
"A person in a retail environment secretly takes products off shelves and conceals them before heading toward the exit.",
|
| 89 |
+
"Someone stealthily pockets small items from store displays while pretending to browse or shop normally.",
|
| 90 |
+
"A shoplifter carefully selects items and conceals them in their clothing or bag while avoiding store staff.",
|
| 91 |
+
],
|
| 92 |
+
"Stealing": [
|
| 93 |
+
"A person takes property that does not belong to them from an outdoor or public setting, such as a parked vehicle or yard.",
|
| 94 |
+
"An individual approaches an unattended item or vehicle and takes it, quickly leaving the area.",
|
| 95 |
+
"Someone steals objects from outside a building, car, or public space, acting quickly to avoid being caught.",
|
| 96 |
+
"A thief takes items from an unattended area, such as bicycles, packages, or belongings left outside.",
|
| 97 |
+
"A person sneaks up to and takes property from a public or semi-public area, departing swiftly after the theft.",
|
| 98 |
+
],
|
| 99 |
+
"Vandalism": [
|
| 100 |
+
"A person deliberately damages property by smashing windows, spray-painting walls, or breaking objects in public.",
|
| 101 |
+
"An individual uses tools or physical force to destroy or deface property, such as cars, buildings, or street furniture.",
|
| 102 |
+
"Someone intentionally causes damage to public or private property through acts like breaking, smashing, or graffiti.",
|
| 103 |
+
"A person vandalizes a vehicle or building, kicking, striking, or throwing objects to cause visible damage.",
|
| 104 |
+
"An individual engages in deliberate destruction of property, leaving visible signs of damage like broken glass or paint.",
|
| 105 |
+
],
|
| 106 |
+
}
|
example_videos.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Curated example videos for the Hugging Face Space.
|
| 2 |
+
|
| 3 |
+
Same list as the local demo. Video files live under `./examples/` (bundled
|
| 4 |
+
with the Space repo).
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
EXAMPLES = [
|
| 10 |
+
{
|
| 11 |
+
"name": "Shoplifting001_x264",
|
| 12 |
+
"category": "Shoplifting",
|
| 13 |
+
"scenario": "ALL_PASS",
|
| 14 |
+
"display_label": "[All pass] Shoplifting β easy case",
|
| 15 |
+
"preds": {"clip": "Shoplifting", "xclip": "Shoplifting", "ours": "Shoplifting"},
|
| 16 |
+
"note": (
|
| 17 |
+
"All three methods identify the shoplifting correctly. Concealment "
|
| 18 |
+
"in a retail setting is a visually distinctive pattern that even "
|
| 19 |
+
"zero-shot models catch."
|
| 20 |
+
),
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"name": "Explosion001_x264",
|
| 24 |
+
"category": "Explosion",
|
| 25 |
+
"scenario": "ALL_PASS",
|
| 26 |
+
"display_label": "[All pass] Explosion β bright visual signature",
|
| 27 |
+
"preds": {"clip": "Explosion", "xclip": "Explosion", "ours": "Explosion"},
|
| 28 |
+
"note": (
|
| 29 |
+
"Fire, smoke, and shockwaves are highly distinctive. All three "
|
| 30 |
+
"methods classify this correctly with high confidence."
|
| 31 |
+
),
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"name": "Fighting004_x264",
|
| 35 |
+
"category": "Fighting",
|
| 36 |
+
"scenario": "OURS_ONLY",
|
| 37 |
+
"display_label": "[Ours only] Fighting β baselines guess Shooting / Arrest",
|
| 38 |
+
"preds": {"clip": "Shooting", "xclip": "Arrest", "ours": "Fighting"},
|
| 39 |
+
"note": (
|
| 40 |
+
"Baselines are wildly off (CLIP: Shooting, X-CLIP: Arrest), while "
|
| 41 |
+
"our fine-tuned model correctly identifies the physical altercation. "
|
| 42 |
+
"This is exactly the kind of visual ambiguity that fine-tuning helps "
|
| 43 |
+
"resolve."
|
| 44 |
+
),
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"name": "Stealing002_x264",
|
| 48 |
+
"category": "Stealing",
|
| 49 |
+
"scenario": "OURS_ONLY",
|
| 50 |
+
"display_label": "[Ours only] Stealing β baselines score 0% F1 on this class",
|
| 51 |
+
"preds": {"clip": "Normal", "xclip": "Road Accident", "ours": "Stealing"},
|
| 52 |
+
"note": (
|
| 53 |
+
"The Stealing class has 0.0% F1 for both CLIP and X-CLIP zero-shot. "
|
| 54 |
+
"Our fine-tuned model learned to recognise outdoor theft from 38 "
|
| 55 |
+
"training examples β a clear demonstration of the paper's central "
|
| 56 |
+
"claim."
|
| 57 |
+
),
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "Arson003_x264",
|
| 61 |
+
"category": "Arson",
|
| 62 |
+
"scenario": "OURS_ONLY",
|
| 63 |
+
"display_label": "[Ours only] Arson β baselines confuse fire cues",
|
| 64 |
+
"preds": {"clip": "Explosion", "xclip": "Road Accident", "ours": "Arson"},
|
| 65 |
+
"note": (
|
| 66 |
+
"CLIP mistakes deliberate fire-setting for an Explosion; X-CLIP "
|
| 67 |
+
"guesses Road Accident. The LLM descriptions taught our model to "
|
| 68 |
+
"distinguish the temporal build-up of arson from an instantaneous "
|
| 69 |
+
"blast."
|
| 70 |
+
),
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"name": "Normal_Videos_150_x264",
|
| 74 |
+
"category": "Normal_Videos_event",
|
| 75 |
+
"scenario": "OURS_ONLY",
|
| 76 |
+
"display_label": "[Ours only] Normal β baselines over-fire on benign scenes",
|
| 77 |
+
"preds": {"clip": "Shoplifting", "xclip": "Arrest", "ours": "Normal"},
|
| 78 |
+
"note": (
|
| 79 |
+
"A calm surveillance scene. Both baselines hallucinate a crime "
|
| 80 |
+
"(Shoplifting / Arrest). Our model correctly says Normal β a "
|
| 81 |
+
"critical property for any real deployment, since false alarms on "
|
| 82 |
+
"benign footage would make the system unusable."
|
| 83 |
+
),
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"name": "Robbery005_x264",
|
| 87 |
+
"category": "Robbery",
|
| 88 |
+
"scenario": "ALL_FAIL",
|
| 89 |
+
"display_label": "[All fail] Robbery β famous confusion with Shoplifting",
|
| 90 |
+
"preds": {"clip": "Shooting", "xclip": "Shoplifting", "ours": "Shoplifting"},
|
| 91 |
+
"note": (
|
| 92 |
+
"The paper's most visible failure: Robbery is systematically "
|
| 93 |
+
"misclassified as Shoplifting (11/12 in our confusion matrix). "
|
| 94 |
+
"The visual setting β retail environment, someone taking goods β "
|
| 95 |
+
"overlaps too much with shoplifting for any of the three methods "
|
| 96 |
+
"to distinguish the two without weapon/threat cues."
|
| 97 |
+
),
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"name": "Abuse005_x264",
|
| 101 |
+
"category": "Abuse",
|
| 102 |
+
"scenario": "ALL_FAIL",
|
| 103 |
+
"display_label": "[All fail] Abuse β interpersonal violence is hard",
|
| 104 |
+
"preds": {"clip": "Arrest", "xclip": "Road Accident", "ours": "Shooting"},
|
| 105 |
+
"note": (
|
| 106 |
+
"Abuse scored 0.0% F1 across the test set. The distinguishing cue "
|
| 107 |
+
"(one-sided power dynamic between people) is extremely subtle in "
|
| 108 |
+
"low-resolution surveillance footage, so all three methods guess "
|
| 109 |
+
"something else entirely."
|
| 110 |
+
),
|
| 111 |
+
},
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_example_video_paths(examples_dir: Path | None = None) -> list[Path]:
|
| 116 |
+
"""Return the list of mp4 paths for all curated examples."""
|
| 117 |
+
if examples_dir is None:
|
| 118 |
+
examples_dir = Path(__file__).resolve().parent / "examples"
|
| 119 |
+
return [examples_dir / f"{ex['name']}.mp4" for ex in EXAMPLES]
|
examples/Abuse005_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:425744aa3472e424d52d7ce97bf6d0bdd445ad62ad1be110095d2027a31550cc
|
| 3 |
+
size 6250495
|
examples/Arson003_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6a17a5ae6d7a01eb84eb0afa976d8cb493ac52051ca8301d4428e53af91b7e7e
|
| 3 |
+
size 24229193
|
examples/Explosion001_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b4d9dddadf4f102e5f017e098f345b30cacc499058f0f69ef1f2787618721bf4
|
| 3 |
+
size 1187504
|
examples/Fighting004_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e93bb27e96279ff1a740d96f4e4bc5de5a5fe324deffa435e2dd58fd285f9916
|
| 3 |
+
size 121757725
|
examples/Normal_Videos_150_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9241161d31599a70264edc2dff747030a3a25d4b7a296a7809e1cad28d026aa4
|
| 3 |
+
size 7315607
|
examples/Robbery005_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dfea68e67bf1a15d4dcba919e8dfa462189c6dacb047b7445bfeaebbaea22c03
|
| 3 |
+
size 15221600
|
examples/Shoplifting001_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7af64fd7e96f81da3f91c9cd4bf531dc2d5968718cdb646822ea9895d71ff060
|
| 3 |
+
size 35944805
|
examples/Stealing002_x264.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:88acfce0d4442084f313e9a6537d93b49567a83f5c1756ccaf5ca21d9d6d455b
|
| 3 |
+
size 15233758
|
inference.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference utilities for the Hugging Face Space.
|
| 2 |
+
|
| 3 |
+
Adapted from the local demo β downloads the fine-tuned checkpoint from a
|
| 4 |
+
HF Model repo at first use, and manages VRAM/RAM by lazy-loading one of
|
| 5 |
+
three models at a time.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
from PIL import Image
|
| 17 |
+
|
| 18 |
+
import config_local as config
|
| 19 |
+
from descriptive_prompts import PROMPTS
|
| 20 |
+
|
| 21 |
+
# ββ Display names βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
CLASS_DISPLAY = {
|
| 23 |
+
"Abuse": "Abuse",
|
| 24 |
+
"Arrest": "Arrest",
|
| 25 |
+
"Arson": "Arson",
|
| 26 |
+
"Assault": "Assault",
|
| 27 |
+
"Burglary": "Burglary",
|
| 28 |
+
"Explosion": "Explosion",
|
| 29 |
+
"Fighting": "Fighting",
|
| 30 |
+
"Normal_Videos_event": "Normal",
|
| 31 |
+
"RoadAccidents": "Road Accident",
|
| 32 |
+
"Robbery": "Robbery",
|
| 33 |
+
"Shooting": "Shooting",
|
| 34 |
+
"Shoplifting": "Shoplifting",
|
| 35 |
+
"Stealing": "Stealing",
|
| 36 |
+
"Vandalism": "Vandalism",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
_CLASS_PROMPT_SHORT = {
|
| 40 |
+
"Abuse": "abuse", "Arrest": "arrest", "Arson": "arson", "Assault": "assault",
|
| 41 |
+
"Burglary": "burglary", "Explosion": "explosion", "Fighting": "fighting",
|
| 42 |
+
"Normal_Videos_event": "normal activity", "RoadAccidents": "road accident",
|
| 43 |
+
"Robbery": "robbery", "Shooting": "shooting", "Shoplifting": "shoplifting",
|
| 44 |
+
"Stealing": "stealing", "Vandalism": "vandalism",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
# ββ Module state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
_device: Optional[torch.device] = None
|
| 49 |
+
_xclip_ft = None
|
| 50 |
+
_xclip_zs = None
|
| 51 |
+
_clip = None
|
| 52 |
+
_current_model: Optional[str] = None
|
| 53 |
+
_checkpoint_path: Optional[Path] = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _get_device() -> torch.device:
|
| 57 |
+
global _device
|
| 58 |
+
if _device is None:
|
| 59 |
+
_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 60 |
+
return _device
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _get_checkpoint_path() -> Path:
|
| 64 |
+
"""Download the fine-tuned checkpoint from the HF Model repo on first call."""
|
| 65 |
+
global _checkpoint_path
|
| 66 |
+
if _checkpoint_path is not None and _checkpoint_path.exists():
|
| 67 |
+
return _checkpoint_path
|
| 68 |
+
|
| 69 |
+
from huggingface_hub import hf_hub_download
|
| 70 |
+
print(f"[inference] Downloading checkpoint from {config.HF_MODEL_REPO}...")
|
| 71 |
+
p = hf_hub_download(
|
| 72 |
+
repo_id=config.HF_MODEL_REPO,
|
| 73 |
+
filename=config.HF_MODEL_FILENAME,
|
| 74 |
+
repo_type="model",
|
| 75 |
+
)
|
| 76 |
+
_checkpoint_path = Path(p)
|
| 77 |
+
print(f"[inference] Checkpoint cached at {_checkpoint_path}")
|
| 78 |
+
return _checkpoint_path
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _pad_frames(frames: list[Image.Image], num_frames: int) -> list[Image.Image]:
|
| 82 |
+
if len(frames) == 0:
|
| 83 |
+
raise ValueError("No frames provided")
|
| 84 |
+
frames = list(frames[:num_frames])
|
| 85 |
+
while len(frames) < num_frames:
|
| 86 |
+
frames.append(frames[-1])
|
| 87 |
+
return frames
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _unload(name: str) -> None:
|
| 91 |
+
global _xclip_ft, _xclip_zs, _clip
|
| 92 |
+
if name == "ft":
|
| 93 |
+
_xclip_ft = None
|
| 94 |
+
elif name == "xclip_zs":
|
| 95 |
+
_xclip_zs = None
|
| 96 |
+
elif name == "clip":
|
| 97 |
+
_clip = None
|
| 98 |
+
if torch.cuda.is_available():
|
| 99 |
+
torch.cuda.empty_cache()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _switch_to(target: str) -> None:
|
| 103 |
+
global _current_model
|
| 104 |
+
if _current_model is not None and _current_model != target:
|
| 105 |
+
_unload(_current_model)
|
| 106 |
+
_current_model = None
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def unload_all_models() -> None:
|
| 110 |
+
global _xclip_ft, _xclip_zs, _clip, _current_model
|
| 111 |
+
_xclip_ft = None
|
| 112 |
+
_xclip_zs = None
|
| 113 |
+
_clip = None
|
| 114 |
+
_current_model = None
|
| 115 |
+
if torch.cuda.is_available():
|
| 116 |
+
torch.cuda.empty_cache()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ββ Frame extraction ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def extract_frames_from_video(video_path: str, num_frames: int = 8) -> list[Image.Image]:
|
| 123 |
+
import decord
|
| 124 |
+
decord.bridge.set_bridge("native")
|
| 125 |
+
vr = decord.VideoReader(str(video_path), num_threads=1)
|
| 126 |
+
total = len(vr)
|
| 127 |
+
if total == 0:
|
| 128 |
+
return []
|
| 129 |
+
indices = np.linspace(0, total - 1, num_frames, dtype=int)
|
| 130 |
+
frames_np = vr.get_batch(indices).asnumpy()
|
| 131 |
+
return [Image.fromarray(frames_np[i]) for i in range(len(frames_np))]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ββ Model loaders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _load_xclip_finetuned():
|
| 138 |
+
from transformers import XCLIPModel, XCLIPProcessor
|
| 139 |
+
|
| 140 |
+
device = _get_device()
|
| 141 |
+
processor = XCLIPProcessor.from_pretrained("microsoft/xclip-base-patch16")
|
| 142 |
+
model = XCLIPModel.from_pretrained("microsoft/xclip-base-patch16")
|
| 143 |
+
|
| 144 |
+
ckpt_path = _get_checkpoint_path()
|
| 145 |
+
cp = torch.load(ckpt_path, map_location=device, weights_only=True)
|
| 146 |
+
model.load_state_dict(cp["model_state_dict"])
|
| 147 |
+
model = model.to(device).eval()
|
| 148 |
+
|
| 149 |
+
text_embeds = _compute_descriptive_text_embeds_xclip(model, processor, device)
|
| 150 |
+
return model, processor, text_embeds
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _load_xclip_zero_shot():
|
| 154 |
+
from transformers import XCLIPModel, XCLIPProcessor
|
| 155 |
+
|
| 156 |
+
device = _get_device()
|
| 157 |
+
processor = XCLIPProcessor.from_pretrained("microsoft/xclip-base-patch16")
|
| 158 |
+
model = XCLIPModel.from_pretrained("microsoft/xclip-base-patch16").to(device).eval()
|
| 159 |
+
|
| 160 |
+
texts = [f"a surveillance video of {_CLASS_PROMPT_SHORT[c]}" for c in config.CLASSES]
|
| 161 |
+
all_embeds = []
|
| 162 |
+
with torch.inference_mode():
|
| 163 |
+
for t in texts:
|
| 164 |
+
ti = processor.tokenizer(
|
| 165 |
+
t, return_tensors="pt", padding="max_length",
|
| 166 |
+
truncation=True, max_length=77,
|
| 167 |
+
).to(device)
|
| 168 |
+
te = model.get_text_features(**ti).pooler_output
|
| 169 |
+
te = F.normalize(te, dim=-1)
|
| 170 |
+
all_embeds.append(te)
|
| 171 |
+
text_embeds = torch.cat(all_embeds, dim=0)
|
| 172 |
+
return model, processor, text_embeds
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _load_clip_zero_shot():
|
| 176 |
+
from transformers import CLIPModel, CLIPProcessor
|
| 177 |
+
|
| 178 |
+
device = _get_device()
|
| 179 |
+
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch16")
|
| 180 |
+
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch16").to(device).eval()
|
| 181 |
+
|
| 182 |
+
texts = [f"a surveillance video of {_CLASS_PROMPT_SHORT[c]}" for c in config.CLASSES]
|
| 183 |
+
with torch.inference_mode():
|
| 184 |
+
text_inputs = processor(text=texts, return_tensors="pt", padding=True).to(device)
|
| 185 |
+
text_embeds = model.get_text_features(**text_inputs).pooler_output
|
| 186 |
+
text_embeds = F.normalize(text_embeds, dim=-1)
|
| 187 |
+
return model, processor, text_embeds
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _compute_descriptive_text_embeds_xclip(model, processor, device):
|
| 191 |
+
all_embeds = []
|
| 192 |
+
with torch.inference_mode():
|
| 193 |
+
for cls in config.CLASSES:
|
| 194 |
+
prompts = PROMPTS[cls]
|
| 195 |
+
cls_embeds = []
|
| 196 |
+
for p in prompts:
|
| 197 |
+
ti = processor.tokenizer(
|
| 198 |
+
p, return_tensors="pt", padding="max_length",
|
| 199 |
+
truncation=True, max_length=77,
|
| 200 |
+
).to(device)
|
| 201 |
+
te = model.get_text_features(**ti).pooler_output
|
| 202 |
+
te = F.normalize(te, dim=-1)
|
| 203 |
+
cls_embeds.append(te)
|
| 204 |
+
avg = F.normalize(
|
| 205 |
+
torch.mean(torch.cat(cls_embeds), dim=0, keepdim=True), dim=-1
|
| 206 |
+
)
|
| 207 |
+
all_embeds.append(avg)
|
| 208 |
+
return torch.cat(all_embeds, dim=0)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ββ Shared result formatter βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _format_result(sims_row: torch.Tensor) -> dict:
|
| 215 |
+
probs = F.softmax(sims_row, dim=-1).detach().cpu().numpy()
|
| 216 |
+
order = np.argsort(-probs)
|
| 217 |
+
top3_idx = order[:3]
|
| 218 |
+
top3_labels = [CLASS_DISPLAY[config.CLASSES[i]] for i in top3_idx]
|
| 219 |
+
top3_scores = [float(probs[i]) for i in top3_idx]
|
| 220 |
+
all_scores = {
|
| 221 |
+
CLASS_DISPLAY[config.CLASSES[i]]: float(probs[i])
|
| 222 |
+
for i in range(len(config.CLASSES))
|
| 223 |
+
}
|
| 224 |
+
return {"top3_labels": top3_labels, "top3_scores": top3_scores, "all_scores": all_scores}
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
# ββ Prediction entry points βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def predict_ours(frames: list[Image.Image]) -> dict:
|
| 231 |
+
global _xclip_ft, _current_model
|
| 232 |
+
_switch_to("ft")
|
| 233 |
+
if _xclip_ft is None:
|
| 234 |
+
_xclip_ft = _load_xclip_finetuned()
|
| 235 |
+
_current_model = "ft"
|
| 236 |
+
model, processor, text_embeds = _xclip_ft
|
| 237 |
+
device = _get_device()
|
| 238 |
+
frames = _pad_frames(frames, config.NUM_FRAMES)
|
| 239 |
+
inputs = processor(images=frames, return_tensors="pt")
|
| 240 |
+
pixel_values = inputs["pixel_values"].to(device)
|
| 241 |
+
with torch.inference_mode():
|
| 242 |
+
vid_embeds = model.get_video_features(pixel_values=pixel_values).pooler_output
|
| 243 |
+
vid_embeds = F.normalize(vid_embeds, dim=-1)
|
| 244 |
+
sims = vid_embeds @ text_embeds.T
|
| 245 |
+
return _format_result(sims[0])
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def predict_xclip_zs(frames: list[Image.Image]) -> dict:
|
| 249 |
+
global _xclip_zs, _current_model
|
| 250 |
+
_switch_to("xclip_zs")
|
| 251 |
+
if _xclip_zs is None:
|
| 252 |
+
_xclip_zs = _load_xclip_zero_shot()
|
| 253 |
+
_current_model = "xclip_zs"
|
| 254 |
+
model, processor, text_embeds = _xclip_zs
|
| 255 |
+
device = _get_device()
|
| 256 |
+
frames = _pad_frames(frames, config.NUM_FRAMES)
|
| 257 |
+
inputs = processor(images=frames, return_tensors="pt")
|
| 258 |
+
pixel_values = inputs["pixel_values"].to(device)
|
| 259 |
+
with torch.inference_mode():
|
| 260 |
+
vid_embeds = model.get_video_features(pixel_values=pixel_values).pooler_output
|
| 261 |
+
vid_embeds = F.normalize(vid_embeds, dim=-1)
|
| 262 |
+
sims = vid_embeds @ text_embeds.T
|
| 263 |
+
return _format_result(sims[0])
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def predict_clip_zs(frames: list[Image.Image]) -> dict:
|
| 267 |
+
global _clip, _current_model
|
| 268 |
+
_switch_to("clip")
|
| 269 |
+
if _clip is None:
|
| 270 |
+
_clip = _load_clip_zero_shot()
|
| 271 |
+
_current_model = "clip"
|
| 272 |
+
model, processor, text_embeds = _clip
|
| 273 |
+
device = _get_device()
|
| 274 |
+
if len(frames) == 0:
|
| 275 |
+
raise ValueError("No frames provided")
|
| 276 |
+
mid = frames[len(frames) // 2]
|
| 277 |
+
inputs = processor(images=mid, return_tensors="pt")
|
| 278 |
+
pixel_values = inputs["pixel_values"].to(device)
|
| 279 |
+
with torch.inference_mode():
|
| 280 |
+
img_embeds = model.get_image_features(pixel_values=pixel_values).pooler_output
|
| 281 |
+
img_embeds = F.normalize(img_embeds, dim=-1)
|
| 282 |
+
sims = img_embeds @ text_embeds.T
|
| 283 |
+
return _format_result(sims[0])
|
per_class_f1.png
ADDED
|
Git LFS Details
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.12.0
|
| 2 |
+
torch>=2.1
|
| 3 |
+
transformers>=4.40
|
| 4 |
+
decord>=0.6.0
|
| 5 |
+
pillow>=10.0
|
| 6 |
+
numpy>=1.24
|
| 7 |
+
huggingface_hub>=0.24
|
summary_table.png
ADDED
|
training_curve.png
ADDED
|