yashppawar commited on
Commit
a45c0ca
Β·
verified Β·
1 Parent(s): 4df8ffc

Initial Space upload

Browse files
.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: Xclip Crime Demo
3
- emoji: πŸ‘€
4
  colorFrom: blue
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.12.0
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: a0d91c1e510a8c98974636df1881b09c891959fa571fcf7ae33e059859759645
  • Pointer size: 131 Bytes
  • Size of remote file: 176 kB
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

  • SHA256: c7b214a84c38b9ae8f4fb55153ca0da590f2759a78423e228e3dd6b4356a417b
  • Pointer size: 131 Bytes
  • Size of remote file: 124 kB
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