mchi8sp2 commited on
Commit
00ee8dd
·
verified ·
1 Parent(s): 5f94b6a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -145
app.py CHANGED
@@ -1,179 +1,161 @@
1
- import spaces
2
- import gradio as gr
3
- import cv2
4
- from PIL import Image
5
- import torch
 
 
 
 
 
 
 
 
 
 
 
6
  import time
7
- import numpy as np
8
  import uuid
9
- import os
10
  import subprocess
 
 
 
 
 
 
 
11
  from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
 
12
  from draw_boxes import draw_bounding_boxes
13
 
14
- print("=" * 60)
15
- print("RT-DETR Space Browser-compatible version starting")
16
- print("=" * 60)
 
 
17
 
18
- image_processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_r50vd")
19
- model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd").to("cuda")
 
20
 
21
  SUBSAMPLE = 2
22
- SEGMENT_SECONDS = 1.0 # smaller segments faster first output
23
-
 
24
 
25
- def reencode_to_browser_h264(input_path: str) -> str:
26
- """Force H.264 that Chrome can actually play."""
27
- if not os.path.exists(input_path):
28
- print(f"[ERROR] Input file does not exist: {input_path}")
29
- return input_path
30
-
31
- output_path = input_path.replace(".mp4", "_h264.mp4")
32
- print(f"[RE-ENCODE] {input_path} → {output_path}")
33
 
 
 
34
  cmd = [
35
- "ffmpeg", "-y",
36
- "-i", input_path,
37
- "-c:v", "libx264",
38
- "-preset", "veryfast",
39
- "-crf", "23",
40
- "-pix_fmt", "yuv420p",
41
- "-movflags", "+faststart",
42
- "-an",
43
- output_path
44
  ]
 
 
 
 
 
 
 
45
 
46
- try:
47
- result = subprocess.run(
48
- cmd,
49
- check=True,
50
- stdout=subprocess.PIPE,
51
- stderr=subprocess.PIPE,
52
- text=True
53
- )
54
- print(f"[RE-ENCODE] SUCCESS → {output_path}")
55
- # remove intermediate
56
- os.remove(input_path)
57
- return output_path
58
- except subprocess.CalledProcessError as e:
59
- print(f"[RE-ENCODE] FAILED!")
60
- print("ffmpeg stderr:", e.stderr)
61
- # return original as last resort
62
- return input_path
63
- except FileNotFoundError:
64
- print("[RE-ENCODE] ffmpeg not found in PATH!")
65
- return input_path
66
-
67
-
68
- @spaces.GPU
69
  def stream_object_detection(video, conf_threshold):
70
- print(f"\n[START] New video received. conf_threshold={conf_threshold}")
71
  cap = cv2.VideoCapture(video)
72
  if not cap.isOpened():
73
- raise gr.Error("Cannot open the uploaded video")
74
-
75
- fps = int(cap.get(cv2.CAP_PROP_FPS)) or 30
76
- desired_fps = max(1, fps // SUBSAMPLE)
77
- print(f"[INFO] original fps={fps}, desired_fps={desired_fps}")
78
-
79
- orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
80
- orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
81
- width = max(2, (orig_w // 2) // 2 * 2)
82
- height = max(2, (orig_h // 2) // 2 * 2)
83
- print(f"[INFO] output size = {width}x{height}")
84
-
85
- frames_per_segment = max(1, int(desired_fps * SEGMENT_SECONDS))
86
- print(f"[INFO] frames_per_segment = {frames_per_segment}")
87
-
88
- iterating, frame = cap.read()
89
- n_frames = 0
90
- batch = []
91
- name = f"/tmp/output_{uuid.uuid4()}.mp4" # use /tmp for safety
92
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
93
- segment_file = cv2.VideoWriter(name, fourcc, desired_fps, (width, height))
94
-
95
- def flush_segment(current_batch, writer, current_name):
96
- if len(current_batch) == 0:
97
- writer.release()
98
  return None
99
-
100
- print(f"[PROCESS] batch size = {len(current_batch)}")
101
- inputs = image_processor(images=current_batch, return_tensors="pt").to("cuda")
102
-
103
- start = time.time()
104
  with torch.no_grad():
105
  outputs = model(**inputs)
106
- print(f"time taken for inference {time.time() - start:.3f}")
107
-
108
- start = time.time()
109
  boxes = image_processor.post_process_object_detection(
110
  outputs,
111
- target_sizes=torch.tensor([(height, width)] * len(current_batch)),
112
- threshold=conf_threshold
113
  )
 
 
 
 
 
 
 
 
 
114
 
115
- for array, box in zip(current_batch, boxes):
116
- pil_image = draw_bounding_boxes(
117
- Image.fromarray(array), box, model, conf_threshold
118
- )
119
- frame_bgr = np.array(pil_image)[:, :, ::-1].copy()
120
- writer.write(frame_bgr)
121
-
122
- writer.release()
123
- print(f"time taken for processing boxes {time.time() - start:.3f}")
124
-
125
- playable = reencode_to_browser_h264(current_name)
126
- print(f"[YIELD] {playable}")
127
- return playable
128
-
129
- while iterating:
130
- frame = cv2.resize(frame, (width, height))
131
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
132
-
133
- if n_frames % SUBSAMPLE == 0:
134
- batch.append(frame)
135
-
136
- if len(batch) >= frames_per_segment:
137
- playable = flush_segment(batch, segment_file, name)
138
- if playable:
139
- yield playable
140
- batch = []
141
- name = f"/tmp/output_{uuid.uuid4()}.mp4"
142
- segment_file = cv2.VideoWriter(name, fourcc, desired_fps, (width, height))
143
-
144
- iterating, frame = cap.read()
145
- n_frames += 1
146
-
147
- # Final remaining frames
148
- print(f"[FINAL] remaining frames in batch = {len(batch)}")
149
- playable = flush_segment(batch, segment_file, name)
150
- if playable:
151
- yield playable
152
-
153
- cap.release()
154
- print("[END] Video processing finished\n")
155
-
156
-
157
- with gr.Blocks() as app:
158
- gr.HTML("""
159
- <h1 style='text-align: center'>
160
- RT-DETR Object Detection<br>
161
- <span style='font-size:0.55em;color:#555'>(Short video + Chrome H.264 fixed)</span>
162
- </h1>
163
- """)
164
  with gr.Row():
165
  with gr.Column():
166
  video = gr.Video(label="Video Source")
167
  conf_threshold = gr.Slider(
168
  label="Confidence Threshold",
169
- minimum=0.0, maximum=1.0, step=0.05, value=0.30
170
  )
171
  with gr.Column():
172
- output_video = gr.Video(
173
- label="Processed Video",
174
- streaming=True,
175
- autoplay=True
176
- )
177
 
178
  video.upload(
179
  fn=stream_object_detection,
 
1
+ """
2
+ RT-DETR Object Detection — streaming variant (Plan B).
3
+
4
+ Use this ONLY if you specifically need progressive playback while processing.
5
+ It is inherently more fragile than app.py (single-file). Differences from upstream:
6
+
7
+ * Every yielded segment is re-encoded to real H.264 with ffmpeg. Gradio's
8
+ streaming-outputs guide requires .mp4 or h.264-in-.ts for video chunks.
9
+ * Segment length is time-based and >= 1 s (Gradio requires this for smooth
10
+ playback). Upstream hardcoded exactly 2*desired_fps frames, so any video
11
+ whose frame count was not an exact multiple of that lost its tail.
12
+ * The trailing partial segment is flushed instead of discarded.
13
+ * Even dimensions enforced; frames resized to the writer's exact size.
14
+ """
15
+
16
+ import os
17
  import time
 
18
  import uuid
19
+ import shutil
20
  import subprocess
21
+
22
+ import cv2
23
+ import numpy as np
24
+ import torch
25
+ import gradio as gr
26
+ import spaces
27
+ from PIL import Image
28
  from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
29
+
30
  from draw_boxes import draw_bounding_boxes
31
 
32
+ BUILD_TAG = "PLANB-H264-STREAMING-v1"
33
+ print("=" * 70, flush=True)
34
+ print(f"[BOOT] RT-DETR build tag: {BUILD_TAG}", flush=True)
35
+ print(f"[BOOT] ffmpeg on PATH: {shutil.which('ffmpeg')}", flush=True)
36
+ print("=" * 70, flush=True)
37
 
38
+ MODEL_ID = "PekingU/rtdetr_r50vd"
39
+ image_processor = RTDetrImageProcessor.from_pretrained(MODEL_ID)
40
+ model = RTDetrForObjectDetection.from_pretrained(MODEL_ID).to("cuda")
41
 
42
  SUBSAMPLE = 2
43
+ SEGMENT_SECONDS = 1.5 # must stay >= 1.0
44
+ WORK_DIR = os.path.join(os.getcwd(), "rtdetr_stream")
45
+ os.makedirs(WORK_DIR, exist_ok=True)
46
 
 
 
 
 
 
 
 
 
47
 
48
+ def encode_h264(src_path: str) -> str:
49
+ dst_path = src_path.replace(".mp4", "_h264.mp4")
50
  cmd = [
51
+ "ffmpeg", "-y", "-i", src_path,
52
+ "-c:v", "libx264", "-profile:v", "baseline", "-level", "3.1",
53
+ "-preset", "veryfast", "-crf", "23",
54
+ "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-an",
55
+ dst_path,
 
 
 
 
56
  ]
57
+ proc = subprocess.run(cmd, capture_output=True, text=True)
58
+ if proc.returncode != 0 or not os.path.exists(dst_path):
59
+ print("[H264] FAILED:", proc.stderr[-800:], flush=True)
60
+ raise gr.Error("H.264 re-encode failed — see container logs.")
61
+ os.remove(src_path)
62
+ print(f"[H264] OK -> {dst_path} ({os.path.getsize(dst_path)} bytes)", flush=True)
63
+ return dst_path
64
 
65
+
66
+ @spaces.GPU(duration=180)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def stream_object_detection(video, conf_threshold):
68
+ print(f"\n[RUN] {BUILD_TAG} | conf={conf_threshold}", flush=True)
69
  cap = cv2.VideoCapture(video)
70
  if not cap.isOpened():
71
+ raise gr.Error("Could not open the uploaded video.")
72
+
73
+ src_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
74
+ out_fps = max(1.0, src_fps / SUBSAMPLE)
75
+ width = max(2, ((int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) // 2) // 2) * 2)
76
+ height = max(2, ((int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) // 2) // 2) * 2)
77
+ per_segment = max(1, int(round(out_fps * SEGMENT_SECONDS)))
78
+ print(f"[RUN] out {width}x{height} @ {out_fps:.2f}fps | {per_segment} frames/segment",
79
+ flush=True)
80
+
81
+ def new_writer():
82
+ p = os.path.join(WORK_DIR, f"seg_{uuid.uuid4().hex}.mp4")
83
+ return cv2.VideoWriter(p, cv2.VideoWriter_fourcc(*"mp4v"), out_fps, (width, height)), p
84
+
85
+ writer, path = new_writer()
86
+ batch, n_read, seg_no = [], 0, 0
87
+
88
+ def flush(frames, wr, p):
89
+ if not frames:
90
+ wr.release()
91
+ if os.path.exists(p):
92
+ os.remove(p)
 
 
 
93
  return None
94
+ inputs = image_processor(images=frames, return_tensors="pt").to("cuda")
95
+ t0 = time.time()
 
 
 
96
  with torch.no_grad():
97
  outputs = model(**inputs)
 
 
 
98
  boxes = image_processor.post_process_object_detection(
99
  outputs,
100
+ target_sizes=torch.tensor([(height, width)] * len(frames)),
101
+ threshold=conf_threshold,
102
  )
103
+ n_det = 0
104
+ for array, box in zip(frames, boxes):
105
+ n_det += int(box["scores"].shape[0])
106
+ pil_image = draw_bounding_boxes(Image.fromarray(array), box, model, conf_threshold)
107
+ wr.write(np.array(pil_image)[:, :, ::-1].copy())
108
+ wr.release()
109
+ print(f"[SEG] frames={len(frames)} inference={time.time() - t0:.3f}s dets={n_det}",
110
+ flush=True)
111
+ return encode_h264(p)
112
 
113
+ try:
114
+ while True:
115
+ ok, frame = cap.read()
116
+ if not ok:
117
+ break
118
+ if n_read % SUBSAMPLE == 0:
119
+ frame = cv2.resize(frame, (width, height))
120
+ batch.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
121
+ if len(batch) >= per_segment:
122
+ out = flush(batch, writer, path)
123
+ if out:
124
+ seg_no += 1
125
+ print(f"[YIELD] #{seg_no} {out}", flush=True)
126
+ yield out
127
+ batch = []
128
+ writer, path = new_writer()
129
+ n_read += 1
130
+
131
+ # Trailing frames upstream dropped these.
132
+ out = flush(batch, writer, path)
133
+ if out:
134
+ seg_no += 1
135
+ print(f"[YIELD] #{seg_no} (tail) {out}", flush=True)
136
+ yield out
137
+ finally:
138
+ cap.release()
139
+
140
+ print(f"[RUN] done. read={n_read} segments={seg_no}", flush=True)
141
+ if seg_no == 0:
142
+ raise gr.Error("No segments produced — the video decoded to zero frames.")
143
+
144
+
145
+ with gr.Blocks(title="RT-DETR Streaming") as app:
146
+ gr.HTML(
147
+ "<h1 style='text-align:center;margin-bottom:0'>RT-DETR Object Detection (streaming)</h1>"
148
+ f"<p style='text-align:center;color:#888;font-size:0.85em'>build {BUILD_TAG}</p>"
149
+ )
 
 
 
 
 
 
 
 
 
 
 
 
150
  with gr.Row():
151
  with gr.Column():
152
  video = gr.Video(label="Video Source")
153
  conf_threshold = gr.Slider(
154
  label="Confidence Threshold",
155
+ minimum=0.0, maximum=1.0, step=0.01, value=0.30,
156
  )
157
  with gr.Column():
158
+ output_video = gr.Video(label="Processed Video", streaming=True, autoplay=True)
 
 
 
 
159
 
160
  video.upload(
161
  fn=stream_object_detection,