Skip to content

Commit 588f786

Browse files
committed
2 parents 367a23b + 3de996e commit 588f786

53 files changed

Lines changed: 3486 additions & 176 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ __pycache__/
1919
AI/ai_pipeline/videos/
2020
AI/ai_pipeline/data/
2121
AI/ai_pipeline/outputs/
22+
AI/ai_pipeline/bigru_frame_selector/outputs/
23+
*.npy
2224
PGL-SUM/data/
2325
PGL-SUM/Summaries/
2426
storage/
27+
*.csv
2528

2629
# 영상 파일
2730
*.mp4
@@ -40,8 +43,12 @@ storage/
4043
!**/LICENSE.md
4144
CLAUDE.md
4245

43-
# 발표 초안
44-
MIDTERM_PRESENTATION*.txt
46+
# 작업 메모/발표자료 텍스트 (requirements.txt, robots.txt 제외)
47+
*.txt
48+
!requirements.txt
49+
!**/requirements.txt
50+
!robots.txt
51+
!**/robots.txt
4552

4653
# OS
4754
# frontend
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
def select_candidates(results, thresholds, verbose=True):
2+
candidates = []
3+
fight_threshold = thresholds["fight_candidate"]
4+
fall_threshold = thresholds.get("fall_candidate")
5+
6+
for result in results:
7+
fight_score = result["scores"]["fight_candidate_score"]
8+
fall_score = result["scores"].get("fall_candidate_score")
9+
10+
candidate_types = []
11+
if fight_score >= fight_threshold:
12+
candidate_types.append("fight")
13+
if fall_threshold is not None and fall_score is not None and fall_score >= fall_threshold:
14+
candidate_types.append("fall")
15+
16+
if not candidate_types:
17+
continue
18+
19+
result["candidate_types"] = candidate_types
20+
if fall_score is None:
21+
result["candidate_score"] = fight_score
22+
else:
23+
result["candidate_score"] = max(fight_score, fall_score)
24+
candidates.append(result)
25+
26+
if verbose:
27+
print(f" candidate clip 개수: {len(candidates)} / {len(results)}")
28+
return candidates
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from utils.video import clip_frame_span, frames_to_seconds
2+
3+
4+
def merge_candidate_events(candidates, clip_length, stride, fps, max_gap=1):
5+
if not candidates:
6+
return []
7+
8+
candidates = sorted(candidates, key=lambda item: item["clip_id"])
9+
events = []
10+
current_event = None
11+
12+
for candidate in candidates:
13+
clip_id = candidate["clip_id"]
14+
start_frame, end_frame = clip_frame_span(clip_id, clip_length, stride)
15+
label = candidate["vlm_output"]["label"]
16+
17+
if label in {"normal", "uncertain"}:
18+
continue
19+
20+
if (
21+
current_event is None or
22+
current_event["label"] != label or
23+
clip_id - current_event["last_clip_id"] > max_gap
24+
):
25+
if current_event is not None:
26+
events.append(current_event)
27+
28+
current_event = {
29+
"event_id": len(events),
30+
"label": label,
31+
"start_frame": start_frame,
32+
"end_frame": end_frame,
33+
"confidence": candidate["vlm_output"]["confidence"],
34+
"evidence": candidate["vlm_output"]["evidence"],
35+
"clip_ids": [clip_id],
36+
"last_clip_id": clip_id
37+
}
38+
continue
39+
40+
current_event["end_frame"] = end_frame
41+
current_event["clip_ids"].append(clip_id)
42+
current_event["last_clip_id"] = clip_id
43+
current_event["confidence"] = max(
44+
current_event["confidence"],
45+
candidate["vlm_output"]["confidence"]
46+
)
47+
48+
if current_event is not None:
49+
events.append(current_event)
50+
51+
for event in events:
52+
event["start_time"] = frames_to_seconds(event["start_frame"], fps)
53+
event["end_time"] = frames_to_seconds(event["end_frame"] + 1, fps)
54+
event.pop("last_clip_id", None)
55+
56+
return events

AI/ai_pipeline/pipeline/filter.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
def filter_clips(results, threshold=0.4):
2+
"""
3+
results: main_pipeline 결과 리스트
4+
"""
5+
6+
candidates = []
7+
8+
for r in results:
9+
score = r["scores"]["final_score"]
10+
11+
if score > threshold:
12+
candidates.append(r)
13+
14+
print(f" candidate clip 개수: {len(candidates)} / {len(results)}")
15+
16+
return candidates
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import numpy as np
2+
3+
4+
def sample_uniform_plus_center(clip_frames, num_samples=6):
5+
total_frames = len(clip_frames)
6+
7+
if total_frames <= num_samples:
8+
return clip_frames
9+
10+
base_count = max(2, num_samples - 2)
11+
uniform_indices = np.linspace(0, total_frames - 1, base_count).astype(int).tolist()
12+
13+
center = total_frames // 2
14+
extra_indices = [max(0, center - 1), min(total_frames - 1, center + 1)]
15+
indices = sorted(set(uniform_indices + extra_indices))
16+
17+
if len(indices) > num_samples:
18+
indices = indices[:num_samples]
19+
20+
return [clip_frames[i] for i in indices]
21+
22+
23+
def sample_from_candidates(candidates, num_samples=6, strategy="uniform_plus_center"):
24+
for candidate in candidates:
25+
clip = candidate["clip"]
26+
27+
if strategy == "uniform_plus_center":
28+
sampled = sample_uniform_plus_center(clip, num_samples=num_samples)
29+
else:
30+
sampled = sample_uniform_plus_center(clip, num_samples=num_samples)
31+
32+
candidate["sampled_frames"] = sampled
33+
34+
return candidates
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import json
2+
from pathlib import Path
3+
4+
from utils.paths import ensure_dir
5+
6+
7+
def write_results(results_path, payload):
8+
results_path = Path(results_path)
9+
ensure_dir(results_path.parent)
10+
with open(results_path, "w", encoding="utf-8") as f:
11+
json.dump(payload, f, indent=2, ensure_ascii=False)
12+
13+
14+
def write_debug(debug_dir, clip_results):
15+
debug_dir = ensure_dir(debug_dir)
16+
debug_path = debug_dir / "clip_results.json"
17+
with open(debug_path, "w", encoding="utf-8") as f:
18+
json.dump(clip_results, f, indent=2, ensure_ascii=False)

AI/ai_pipeline/pipeline/scorer.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import torch
2+
3+
FIGHT_IDX = [259, 314, 345, 395]
4+
ATTACK_IDX = [150, 152, 302]
5+
FALL_IDX = [122]
6+
ABNORMAL_IDX = [79, 149]
7+
8+
9+
def compute_scores(probs):
10+
11+
if isinstance(probs, torch.Tensor):
12+
probs = probs.tolist()
13+
14+
# score 계산
15+
fight_score = sum(probs[i] for i in FIGHT_IDX if i < len(probs))
16+
attack_score = sum(probs[i] for i in ATTACK_IDX if i < len(probs))
17+
fall_score = sum(probs[i] for i in FALL_IDX if i < len(probs))
18+
abnormal_score = sum(probs[i] for i in ABNORMAL_IDX if i < len(probs))
19+
20+
# uncertainty
21+
max_prob = max(probs)
22+
uncertainty_score = 1 - max_prob
23+
24+
# final score
25+
final_score = (
26+
0.5 * uncertainty_score +
27+
0.3 * fight_score +
28+
0.1 * attack_score +
29+
0.1 * fall_score
30+
)
31+
32+
return {
33+
"final_score": final_score,
34+
"uncertainty": uncertainty_score,
35+
"fight": fight_score,
36+
"attack": attack_score,
37+
"fall": fall_score,
38+
"abnormal": abnormal_score
39+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import argparse
2+
import json
3+
4+
5+
def parse_args():
6+
parser = argparse.ArgumentParser(description="Inspect pipeline output JSON.")
7+
parser.add_argument(
8+
"--results-path",
9+
default="/home/deepgu/test/outputs/results.json",
10+
help="Path to results.json."
11+
)
12+
parser.add_argument(
13+
"--top-k",
14+
type=int,
15+
default=10,
16+
help="Number of candidate clips to print."
17+
)
18+
return parser.parse_args()
19+
20+
21+
def main():
22+
args = parse_args()
23+
24+
with open(args.results_path, "r", encoding="utf-8") as f:
25+
payload = json.load(f)
26+
27+
print(f"video_path: {payload.get('video_path')}")
28+
print(f"num_total_clips: {payload.get('num_total_clips')}")
29+
print(f"num_candidate_clips: {payload.get('num_candidate_clips')}")
30+
print(f"num_events: {len(payload.get('events', []))}")
31+
print("=" * 60)
32+
33+
for event in payload.get("events", []):
34+
print(
35+
f"event_id={event['event_id']} "
36+
f"label={event['label']} "
37+
f"time=({event['start_time']:.2f}s ~ {event['end_time']:.2f}s) "
38+
f"confidence={event['confidence']:.2f}"
39+
)
40+
print(f"evidence={event['evidence']}")
41+
print("-" * 60)
42+
43+
print("Top candidate clips")
44+
print("=" * 60)
45+
46+
candidate_clips = sorted(
47+
payload.get("candidate_clips", []),
48+
key=lambda item: item.get("candidate_score", 0.0),
49+
reverse=True
50+
)
51+
52+
for candidate in candidate_clips[:args.top_k]:
53+
print(
54+
f"clip_id={candidate['clip_id']} "
55+
f"types={candidate['candidate_types']} "
56+
f"candidate_score={candidate['candidate_score']:.4f} "
57+
f"vlm_label={candidate['vlm_output']['label']} "
58+
f"vlm_confidence={candidate['vlm_output']['confidence']:.2f}"
59+
)
60+
61+
62+
if __name__ == "__main__":
63+
main()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import argparse
2+
import json
3+
import sys
4+
from pathlib import Path
5+
6+
7+
ROOT = Path("/home/deepgu/test")
8+
if str(ROOT) not in sys.path:
9+
sys.path.insert(0, str(ROOT))
10+
11+
from pipeline.main_pipeline import run_pipeline
12+
13+
14+
def parse_args():
15+
parser = argparse.ArgumentParser(description="Run the fight anomaly pipeline.")
16+
parser.add_argument(
17+
"--video-path",
18+
default="/home/deepgu/test/data/raw_videos/Abuse007_x264.mp4",
19+
help="Input video path."
20+
)
21+
parser.add_argument(
22+
"--clip-dir",
23+
default="/home/deepgu/test/data/clips",
24+
help="Legacy all-clips directory. Ignored when outputs.root_dir is set in config."
25+
)
26+
parser.add_argument(
27+
"--config-path",
28+
default="/home/deepgu/test/configs/fight_pipeline_config.json",
29+
help="Pipeline config path."
30+
)
31+
return parser.parse_args()
32+
33+
34+
def main():
35+
args = parse_args()
36+
results = run_pipeline(
37+
video_path=args.video_path,
38+
clip_dir=args.clip_dir,
39+
config_path=args.config_path
40+
)
41+
42+
print(json.dumps({
43+
"video_path": results["video_path"],
44+
"num_total_clips": results["num_total_clips"],
45+
"num_candidate_clips": results["num_candidate_clips"],
46+
"num_events": len(results["events"]),
47+
"events": results["events"]
48+
}, indent=2, ensure_ascii=False))
49+
50+
51+
if __name__ == "__main__":
52+
main()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import sys
2+
import os
3+
4+
_AI_PIPELINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5+
_KEYFRAME = os.path.normpath(os.path.join(_AI_PIPELINE, "../keyframe"))
6+
sys.path.insert(0, _AI_PIPELINE)
7+
sys.path.insert(0, _KEYFRAME)
8+
9+
from pipeline.clip_generator import generate_clips
10+
11+
video_path = "/home/deepgu/test/data/raw_videos/Abuse007_x264.mp4"
12+
output_dir = "/home/deepgu/test/data/clips"
13+
14+
clips = generate_clips(
15+
video_path=video_path,
16+
output_dir=output_dir,
17+
clip_len=16,
18+
stride=8,
19+
save=True
20+
)
21+
22+
print(f"생성된 clip 개수: {len(clips)}")

0 commit comments

Comments
 (0)