---
title: "Violence anticipation capstone | Nathan Gamache"
description: "My Lawrence Tech senior capstone: warning before violence starts in video, rebuilt in 2026 and confirmed on held-out footage against a no-video baseline."
url: https://www.nathangamache.com/projects/capstone/
---

# Streaming Violence Anticipation System

My senior capstone at Lawrence Technological University, from fall 2025 to May 2026. I built it on my own, with Dr. Eric Martinson as my advisor. The goal is a model that warns before violence starts in video, rather than one that recognizes it afterwards. In October 2026 I went back to it and rebuilt the evaluation. The result held up on footage I had held back, and an outside test showed where it stops.

[Code on GitHub](https://github.com/nathangamache/violence_anticipation) · [All projects](https://www.nathangamache.com/projects/)

- Type: **Senior capstone, solo**
- School: **Lawrence Technological University**
- Advisor: **Dr. Eric Martinson**
- When: **Fall 2025 to May 2026; rebuilt October 2026**
- Data: **UCF-Crime · XD-Violence · UBI-Fights**
- Tested on: **NTU CCTV-Fights · MEVA**
- Model: **SigLIP 2 prompt scores, linear probe**
- Status: **Confirmed on a held-out fold**
- Stack: **PyTorch · SigLIP 2 · Computer Vision · Evaluation**

![A grainy black-and-white security camera frame labeled CAMERA05, looking down on people behind a bar counter](https://www.nathangamache.com/assets/img/projects/capstone.webp)

## The idea

The idea is to predict risk **before** something happens, rather than classify it after the fact. The model has to make that call from what the video shows in the moments leading up to it, second by second, without seeing what comes next. That is a harder framing than detection.

The targets I set were 30 seconds of warning, with fewer than 6 false alarms an hour. In the rebuild a nearer target, a warning at least 10 seconds ahead, became the main one, with 30 seconds as the stretch.

It was a solo project. In the October rebuild, AI coding agents wrote nearly all of the code, ran the experiments, did the visual review of start times and drafted the documentation, under my direction. I set the goals, including making the 10 second warning the main one, made the call at each stage, and approved each step that could not be undone, such as the one-time final check.

The capstone year

## What I tried first

From November 2025 to April 2026 I worked mostly from XD-Violence and UCF-Crime clips and tried several approaches in PyTorch:

- A **transformer** over person boxes, VideoMAE crops and a CLIP summary of each clip.
- **Embeddings from Qwen2.5-VL**, a vision-language model, with a small model trained on top of them.
- A **fusion of motion, audio and pose**.
- A **live feed**, which I tried, though I mainly ran it on recorded clips.

When I audited that work, most of the impressive numbers did not hold up. They came from labels that did not line up with the frames, test clips that shared footage with training clips, and alarm metrics that were too forgiving. The best honest result ranked the moments before a fight only a little better than chance, with a test AUC of 0.56 to 0.62.

October 2026

## Rebuilding the evaluation

1. **One benchmark.** I combined UCF-Crime, XD-Violence and UBI-Fights into 7,604 videos and about 419 hours, split into five folds that keep near-duplicates and clips from the same film together. One fold is held back for a single final check.
2. **Scored as it would run.** The model scores each second using only what it has seen so far. An alarm counts as a warning only if it starts at least 30 (or 10) seconds before the first physical aggression, and no more than two minutes before.
3. **A baseline with no video.** Fights tend to start early in these clips, so an alarm at the start of every clip would look like anticipation. Every result is compared with the best a rule can do using only what kind of source a clip came from and how far into it the model is, with no video content at all.
4. **Start times re-checked.** The start time of every fight with enough footage before it was re-checked against the frames, which cut those events from 551 to 419: 137 were corrected or newly counted, 120 were confirmed where they were, and 162 could not be settled and keep their original time. AI models did that review, not people.

Signals

## Which signals carry a warning

I fixed pass and fail rules before running the screen on the corrected start times, then tested each kind of feature on its own against them. I had already seen an earlier run on the uncorrected times, and the 10 second target came after it. A stream has to tell the lead-up to violence from ordinary footage, tell it from the lead-up to a non-violent incident such as a theft, and add something beyond the scene itself. Only the SigLIP 2 prompt scores, which compare each frame with short text descriptions such as an argument or a shove, passed the main test. The first set, the one the model uses, was not clearly specific to violence; the second set passed every rule. Audio never entered the rebuilt model: too few real-world clips have their own sound, and the movie clips carry soundtracks, so I only audited it.

**Single streams, screened one at a time**

| Stream | AUROC 30–120 s | AUROC 0–10 s | Result |
| --- | --: | --: | --- |
| SigLIP 2 prompts, first set | 0.844 | 0.881 | Anticipation |
| SigLIP 2 prompts, second set | 0.813 | 0.853 | Anticipation, and passes every gate |
| SigLIP 2 image embeddings | 0.847 | 0.886 | Context shortcut |
| V-JEPA 2 | 0.780 | 0.812 | Context shortcut |
| Pose: how people interact | 0.614 | 0.640 | Context shortcut |
| Pose state and both motion streams | 0.55–0.56 | 0.56–0.63 | Inconclusive |
| Pose: pairs of people | 0.475 | 0.551 | Dropped |

AUROC for telling seconds that come 30 to 120 seconds, or 0 to 10 seconds, before violence from seconds of clean footage, after matching on dataset and position in the clip, so 0.50 is chance. Each horizon has its own trained probe. A context shortcut has a real signal but fails a control: it cannot tell the lead-up to violence from the lead-up to a non-violent incident, or it adds nothing beyond the scene. Inconclusive means too weak to decide; dropped means no signal. The pairs stream covers UCF-Crime and UBI-Fights only.

Development

## On the folds used to choose it

There are two heads, each a linear probe on one set of prompt scores. The early-warning head is the first set, read by a linear probe over the last 10 seconds, minute and three minutes. Its alarm threshold is tuned on validation data for a target number of false alarms an hour, then tested on 344 violent events with at least 30 seconds of footage before them, and about 126 hours of clean video. At about 2 false alarms an hour on the clean footage it warned at least 30 seconds ahead for 31% of the events, against 10% for the baseline with no video content. The imminent head, on the second set, warned at least 10 seconds ahead for 33% at about 2 an hour, against 11%. These folds are the ones I chose the streams on, so the numbers below are flattering; the next section is the fair test.

**Warnings on the screening folds**

| Threshold tuned for | False alarms / h (test) | Warned 30 s+ ahead | Baseline, 30 s+ (no video) | Warned 10 s+ ahead | Median lead |
| --- | --: | --: | --: | --: | --: |
| 1 an hour | 1.16 | 21% | 7% | 31% | 35 s |
| 2 an hour | 2.12 | 31% 95% CI 24–38% | 10% | 42% | 43 s |
| 5 an hour | 4.99 | 39% | 16% | 51% | 43 s |

“Warned 30 s+ ahead” is the share of events whose first alarm started at least 30 seconds, and at most two minutes, before the violence. The baseline is the best any rule using only the kind of source and the time since the clip started can reach at the same false-alarm rate, for the 30 second column. Both warning columns come from the same model. The interval also re-picks the threshold in each bootstrap sample. Median lead is over the events with an alarm in the two minutes before the violence.

Confirmation

## On footage it had never seen

One fold of 1,214 videos, with 75 fights, was set aside from the start, and nothing touched it until both heads were frozen. I then scored it once, against rules written down beforehand. Both heads passed. At under one false alarm an hour, the early head warned at least 30 seconds ahead for 23% of the fights, against 4% for the no-video baseline, and the imminent head warned at least 10 seconds ahead for 20%, against 5%. The thresholds, picked on other data, fired less often than they were tuned for.

**Both heads on the held-out fold**

| Threshold tuned for | Early: false alarms / h | Early: warned 30 s+ | Early: baseline | Imminent: false alarms / h | Imminent: warned 10 s+ | Imminent: baseline |
| --- | --: | --: | --: | --: | --: | --: |
| 1 an hour | 0.33 | 16% | 3% | 0.42 | 11% | 4% |
| 2 an hour | 0.72 | 23% 95% CI 14–39% | 4% | 0.85 | 20% 95% CI 10–39% | 5% |
| 5 an hour | 2.64 | 36% | 9% | 3.23 | 45% | 12% |

75 fights and about 31 hours of clean video, scored once. The baselines are the same no-video rule as above, at each head’s own false-alarm rate. The intervals also re-pick the threshold in each bootstrap sample.

Outside test

## On another dataset

Last, I ran both frozen heads, unchanged, on data from outside the benchmark: the fights in NTU CCTV-Fights, against hours of MEVA footage in which people go about scripted activities and nothing violent ever happens. The plan was committed before any score existed. On MEVA neither head raised a single false alarm, and they warned in time for a quarter or more of the fights.

**NTU CCTV-Fights fights against MEVA footage**

| Head | Threshold tuned for | False alarms / h on MEVA | Fights warned in time | CCTV | Phone and other |
| --- | --- | --: | --: | --: | --: |
| Early | 1 an hour | 0 | 17% | 16% | 18% |
| Early | 2 an hour | 0 | 24% | 17% | 36% |
| Early | 5 an hour | 0 | 36% | 26% | 53% |
| Imminent | 1 an hour | 0 | 14% | 11% | 18% |
| Imminent | 2 an hour | 0 | 28% | 27% | 31% |
| Imminent | 5 an hour | 0 | 50% | 44% | 60% |

145 fights in 985 CCTV-Fights videos (90 CCTV, 55 phone and other) and 355 MEVA clips, about 30 hours, with the thresholds from the held-out fold. “In time” means at least 30 seconds ahead for the early head and 10 seconds for the imminent one.

That passed its rule, but only trivially, and the control inside the same videos is the more telling result. Against calm stretches of the very videos that later contain a fight, the heads scored the lead-up no higher than chance (AUROC 0.40 and 0.48, where 0.50 is chance). They recognize footage that has a fight in it; they do not see one building.

Limits

## What it does and does not show

- **Confirmed once.** Both heads passed on the held-out fold, but that is one fold, with 75 fights, from the same datasets.
- **No build-up inside a video.** On outside data the heads could not tell the moments before a fight from calm moments of the same video.
- **Mostly the kind of clip.** The score is about as high two minutes before a fight as ten seconds before it, and a probe trained only to spot violence already happening scores as well, so much of it seems to come from recognizing the type of footage rather than watching tension build. Some of it goes beyond the scene, but not most.
- **Movies are easier.** At one shared threshold it raised about 4.3 false alarms an hour on movie footage but 0.1 on UCF-Crime’s CCTV, and warned 30 seconds ahead for 41% of the movie events against 17% of the UCF-Crime ones.
- **One video stream per head.** Pose and motion were tested and did not make it in; audio was only audited.
- **A risk estimate, not proof.** The output is an uncertain estimate for a person to review, not evidence of anyone’s intent and not something to act on automatically.

So it is a confirmed result with a clear edge: a model that flags footage likely to contain a fight, well ahead of it and with few false alarms, but not yet one that sees a fight coming. The next step is a method that separates the moments before a fight from calm moments in the same video. The code, the benchmark and the results are [on GitHub](https://github.com/nathangamache/violence_anticipation).

## Related projects

### [3D Hand Gesture Recognition (HS Capstone)](https://www.nathangamache.com/projects/hand-gesture/)

My high school senior capstone, a door lock you open with a sequence of hand gestures, which I rebuilt in 2026 with a Kinect depth camera and a small PyTorch model.

Technologies: PyTorch, Kinect, Computer Vision, Python

[View project: 3D Hand Gesture Recognition (HS Capstone)](https://www.nathangamache.com/projects/hand-gesture/)

### [Detroit Pulse: Live Incident Map from Scanner Radio](https://www.nathangamache.com/projects/detroit-pulse/)

A solo project for my Deep Learning class in the same school year: local Whisper and a local language model, with a LoRA adapter I trained, turning scanner radio into a live incident map.

Technologies: Python, Whisper, Local LLMs, QLoRA, React

[View project: Detroit Pulse: Live Incident Map from Scanner Radio](https://www.nathangamache.com/projects/detroit-pulse/)

[View all 16 projects](https://www.nathangamache.com/projects/)

## Questions about this research?

I am happy to talk through the approach. [nathanpaulgamache@gmail.com](mailto:nathanpaulgamache@gmail.com)

[Ask me about it](https://www.nathangamache.com/contact/)

---

Nathan Gamache · [nathanpaulgamache@gmail.com](mailto:nathanpaulgamache@gmail.com) · [GitHub](https://github.com/nathangamache) · [LinkedIn](https://www.linkedin.com/in/nathan-p-gamache/) · [nathangamache.com](https://www.nathangamache.com/)
