File size: 9,083 Bytes
00ee8dd
8dd63ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00ee8dd
 
 
61732db
4467a7b
00ee8dd
49fe77c
00ee8dd
 
 
 
 
 
 
6a95f1f
00ee8dd
6a95f1f
 
8dd63ac
 
 
 
00ee8dd
 
 
8dd63ac
00ee8dd
5f94b6a
00ee8dd
 
 
ccc35d4
8dd63ac
 
 
00ee8dd
49fe77c
5f94b6a
00ee8dd
8dd63ac
00ee8dd
49fe77c
00ee8dd
8dd63ac
 
 
 
 
 
 
 
00ee8dd
49fe77c
8dd63ac
00ee8dd
8dd63ac
 
 
 
00ee8dd
8dd63ac
00ee8dd
5f94b6a
00ee8dd
8dd63ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00ee8dd
8dd63ac
 
 
 
 
6a95f1f
5f94b6a
00ee8dd
 
 
 
8dd63ac
 
 
 
 
00ee8dd
 
8dd63ac
 
 
 
 
 
 
 
 
5f94b6a
00ee8dd
 
 
 
 
 
8dd63ac
00ee8dd
8dd63ac
 
 
 
00ee8dd
 
 
8dd63ac
 
 
 
 
00ee8dd
8dd63ac
00ee8dd
 
8dd63ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00ee8dd
 
8dd63ac
00ee8dd
8dd63ac
00ee8dd
 
619c27a
 
67e08d4
 
 
00ee8dd
67e08d4
8dd63ac
 
 
 
619c27a
8dd63ac
 
619c27a
8dd63ac
 
 
 
 
619c27a
8dd63ac
 
 
619c27a
9740995
49fe77c
8dd63ac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""
RT-DETR Object Detection — browser-compatible build (Plan A: single-file output)

Fixes applied vs. the upstream gradio/rt-detr-object-detection Space:
  1. Output is re-encoded to real H.264 (libx264 / yuv420p / +faststart) with ffmpeg.
     OpenCV's VideoWriter CANNOT produce H.264 in the pip opencv wheels used on
     HF Spaces — 'avc1'/'H264'/'X264' all return isOpened() == False.
  2. No streaming, no segments. One complete file is returned when done.
     This removes the entire class of MSE / chunk-boundary / short-video bugs.
  3. Every decoded+subsampled frame is written. The upstream code silently
     discarded the trailing partial batch (12 of 36 frames on a 3 s / 24 fps clip).
  4. Output dimensions forced even (H.264 yuv420p requirement).
  5. Frames are resized to the writer's exact size, so no silent write() no-ops.
  6. Optional HUD overlay showing per-frame detection count — useful when the
     purpose of the clip is to demonstrate the LIMITS of a general-purpose
     COCO-trained detector on industrial parts.
"""

import os
import time
import uuid
import shutil
import subprocess

import cv2
import numpy as np
import torch
import gradio as gr
import spaces
from PIL import Image
from transformers import RTDetrForObjectDetection, RTDetrImageProcessor

from draw_boxes import draw_bounding_boxes

# --------------------------------------------------------------------------
# Startup banner — use this to PROVE your edited code is the code running.
# --------------------------------------------------------------------------
BUILD_TAG = "PLANA-H264-SINGLEFILE-v1"
print("=" * 70, flush=True)
print(f"[BOOT] RT-DETR build tag: {BUILD_TAG}", flush=True)
print(f"[BOOT] ffmpeg on PATH: {shutil.which('ffmpeg')}", flush=True)
print(f"[BOOT] opencv: {cv2.__version__}", flush=True)
print("=" * 70, flush=True)

MODEL_ID = "PekingU/rtdetr_r50vd"
image_processor = RTDetrImageProcessor.from_pretrained(MODEL_ID)
model = RTDetrForObjectDetection.from_pretrained(MODEL_ID).to("cuda")

SUBSAMPLE = 2        # keep every Nth source frame
CHUNK = 16           # frames per forward pass (memory bound, not timing bound)
WORK_DIR = os.path.join(os.getcwd(), "rtdetr_work")
os.makedirs(WORK_DIR, exist_ok=True)


def encode_h264(src_path: str) -> str:
    """Re-encode to a stream every browser can decode. Returns the new path."""
    dst_path = src_path.replace(".mp4", "_h264.mp4")
    cmd = [
        "ffmpeg", "-y", "-i", src_path,
        "-c:v", "libx264",
        "-profile:v", "baseline",   # widest possible decoder support
        "-level", "3.1",
        "-preset", "veryfast",
        "-crf", "23",
        "-pix_fmt", "yuv420p",      # mandatory: browsers reject yuv444p/yuvj420p
        "-movflags", "+faststart",  # moov atom first -> progressive playback
        "-an",
        dst_path,
    ]
    print(f"[H264] encoding -> {dst_path}", flush=True)
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0 or not os.path.exists(dst_path) or os.path.getsize(dst_path) == 0:
        print("[H264] FAILED. ffmpeg stderr tail:", flush=True)
        print("\n".join(proc.stderr.strip().splitlines()[-15:]), flush=True)
        raise gr.Error("H.264 re-encode failed — see container logs for ffmpeg output.")
    os.remove(src_path)
    print(f"[H264] OK  {os.path.getsize(dst_path)} bytes", flush=True)
    return dst_path


def _run_batch(frames_rgb, conf_threshold, height, width, writer, show_hud, counter):
    """Detect on a list of RGB frames, draw, write as BGR. Returns detections found."""
    if not frames_rgb:
        return 0

    inputs = image_processor(images=frames_rgb, return_tensors="pt").to("cuda")
    t0 = time.time()
    with torch.no_grad():
        outputs = model(**inputs)
    t_inf = time.time() - t0

    boxes = image_processor.post_process_object_detection(
        outputs,
        target_sizes=torch.tensor([(height, width)] * len(frames_rgb)),
        threshold=conf_threshold,
    )

    found = 0
    for array, box in zip(frames_rgb, boxes):
        n_det = int(box["scores"].shape[0])
        found += n_det
        pil_image = draw_bounding_boxes(Image.fromarray(array), box, model, conf_threshold)
        frame_bgr = np.array(pil_image)[:, :, ::-1].copy()

        if show_hud:
            counter["i"] += 1
            label = f"frame {counter['i']:04d}   detections: {n_det}"
            colour = (0, 200, 0) if n_det else (0, 0, 255)
            cv2.rectangle(frame_bgr, (0, 0), (frame_bgr.shape[1], 26), (0, 0, 0), -1)
            cv2.putText(frame_bgr, label, (8, 18),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.55, colour, 1, cv2.LINE_AA)

        writer.write(frame_bgr)

    print(f"[BATCH] n={len(frames_rgb)} inference={t_inf:.3f}s detections={found}", flush=True)
    return found


@spaces.GPU(duration=180)
def detect(video, conf_threshold, show_hud):
    if not video:
        raise gr.Error("Please upload a video first.")

    print(f"\n[RUN] {BUILD_TAG} | conf={conf_threshold} | hud={show_hud}", flush=True)
    cap = cv2.VideoCapture(video)
    if not cap.isOpened():
        raise gr.Error("Could not open the uploaded video.")

    src_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
    out_fps = max(1.0, src_fps / SUBSAMPLE)
    src_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    src_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    width = max(2, ((src_w // 2) // 2) * 2)      # half size, forced even
    height = max(2, ((src_h // 2) // 2) * 2)
    print(f"[RUN] src {src_w}x{src_h} @ {src_fps:.3f} -> out {width}x{height} @ {out_fps:.3f}",
          flush=True)

    raw_path = os.path.join(WORK_DIR, f"out_{uuid.uuid4().hex}.mp4")
    writer = cv2.VideoWriter(raw_path, cv2.VideoWriter_fourcc(*"mp4v"),
                             out_fps, (width, height))
    if not writer.isOpened():
        cap.release()
        raise gr.Error("OpenCV VideoWriter could not be opened.")

    batch, n_read, n_written, total_det = [], 0, 0, 0
    counter = {"i": 0}

    try:
        while True:
            ok, frame = cap.read()
            if not ok:
                break
            if n_read % SUBSAMPLE == 0:
                frame = cv2.resize(frame, (width, height))          # exact writer size
                batch.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
                n_written += 1
                if len(batch) >= CHUNK:
                    total_det += _run_batch(batch, conf_threshold, height, width,
                                            writer, show_hud, counter)
                    batch = []
            n_read += 1

        # Trailing partial batch — the upstream Space threw these frames away.
        if batch:
            print(f"[FLUSH] trailing frames = {len(batch)}", flush=True)
            total_det += _run_batch(batch, conf_threshold, height, width,
                                    writer, show_hud, counter)
    finally:
        writer.release()
        cap.release()

    if n_written == 0:
        raise gr.Error("No frames were decoded from this video.")

    print(f"[RUN] read={n_read} written={n_written} "
          f"duration={n_written / out_fps:.2f}s total_detections={total_det}", flush=True)

    final_path = encode_h264(raw_path)

    summary = (
        f"Frames processed: {n_written} (from {n_read} source frames)\n"
        f"Output: {width}x{height} @ {out_fps:.2f} fps, {n_written / out_fps:.2f} s\n"
        f"Total detections at threshold {conf_threshold:.2f}: {total_det}\n"
        f"Codec: H.264 (libx264, yuv420p, faststart)"
    )
    if total_det == 0:
        summary += (
            "\n\nNote: zero detections. RT-DETR is trained on the 80 COCO classes; "
            "industrial parts are out-of-distribution, so an empty result is the "
            "expected outcome for a general-purpose detector."
        )
    return final_path, summary


with gr.Blocks(title="RT-DETR Object Detection") as app:
    gr.HTML(
        "<h1 style='text-align:center;margin-bottom:0'>Video Object Detection with RT-DETR</h1>"
        f"<p style='text-align:center;color:#888;font-size:0.85em'>build {BUILD_TAG}</p>"
    )
    with gr.Row():
        with gr.Column():
            video = gr.Video(label="Video Source")
            conf_threshold = gr.Slider(
                label="Confidence Threshold",
                minimum=0.0, maximum=1.0, step=0.01, value=0.30,
            )
            show_hud = gr.Checkbox(
                label="Overlay frame index + detection count", value=True
            )
            run_btn = gr.Button("Run detection", variant="primary")
        with gr.Column():
            output_video = gr.Video(label="Processed Video", autoplay=True)
            report = gr.Textbox(label="Report", lines=7, show_copy_button=True)

    run_btn.click(
        fn=detect,
        inputs=[video, conf_threshold, show_hud],
        outputs=[output_video, report],
    )
    video.upload(
        fn=detect,
        inputs=[video, conf_threshold, show_hud],
        outputs=[output_video, report],
    )

if __name__ == "__main__":
    app.launch()