Gbone3176 commited on
Commit
a1408a1
·
verified ·
1 Parent(s): 9f31c69

Fix Tr006 V2 online Segment preprocessing

Browse files

Pixel-based blue-screen detection without videoID mapping; non-blue bin-center sampling; 420x224 letterbox; absolute PTS metadata. CPU gate: 16/16 PASS, grid 40x14x26, selected blue frames 0.

pipelines/Tr006_V2/FARM04_PACKAGING_GUIDE.md CHANGED
@@ -107,15 +107,17 @@ bash do_test_run.sh
107
 
108
  ## 6. 推理一致性门禁
109
 
110
- 当前 `docker/segment-algorithm/inference.py` 是低分线上提交的事实快照,不是已经修正后的 V2 对齐实现。重新提交前,必须在公开样本上对比:
111
 
112
- 1. V2 训练侧选择的 80 个绝对 source PTS。
113
- 2. 420x224 letterbox 后的帧内容。
114
- 3. 蓝屏区间是否被排除。
115
- 4. `video_metadata.frames_indices`、`video_grid_thw` 和输入 token。
116
- 5. 同一 checkpoint 在本地训练/评测链与 Docker Transformers 链中的最终答案。
117
 
118
- 若这些项目不一致,本次构建只能用于复现旧提交,不能视为修复版。
 
 
 
119
 
120
  ## 7. 保存官方 tar.gz
121
 
 
107
 
108
  ## 6. 推理一致性门禁
109
 
110
+ 当前 `docker/segment-algorithm/inference.py` 已包含 2026-09-02 的 V2 在线修复:
111
 
112
+ 1. 从 clip 像素在线识别纯蓝信号丢失画面,不使用 `videoID` 或文件名映射。
113
+ 2. 在检测到的非蓝时间上选择 80 个 bin-center 帧。
114
+ 3. 使用与训练相同的 PIL bicubic 420x224 letterbox。
115
+ 4. 根据官方 clip PTS 与 `request.start_time` 生成绝对毫秒时间元数据。
 
116
 
117
+ 服务器侧 answer-free CPU 门禁结果:16/16 样本通过,选中蓝帧为 0,
118
+ `video_grid_thw=[[40,14,26]]`,平均预处理 4.254 秒、最大 10.905 秒。
119
+ 仿官方视频不需要下载到 Farm04。Farm04 只需完成官方 `do_test_run.sh` 的
120
+ 容器/GPU/`answer.json` smoke,再执行 `do_save.sh`。
121
 
122
  ## 7. 保存官方 tar.gz
123
 
pipelines/Tr006_V2/README.md CHANGED
@@ -124,7 +124,12 @@ bash do_test_run.sh
124
  bash do_save.sh
125
  ```
126
 
127
- 不要使用当前线上版本的 `np.linspace` 推理预处理作为“已与 V2 对齐”的证据。必须先在公开样本上比较训练 V2 的采样 PTS、420x224 帧、processor 输入和 Docker 输入;`docker/segment-algorithm/inference.py` 是已提交版本的事实快照,保留它是为了复现和定位差异。
 
 
 
 
 
128
 
129
  ## 本地评测边界
130
 
 
124
  bash do_save.sh
125
  ```
126
 
127
+ `docker/segment-algorithm/inference.py` 已更新为 V2 在线推理实现:它不依赖
128
+ `videoID` 映射,而是从官方 5fps clip 的像素识别纯蓝信号丢失画面,随后在
129
+ 非蓝时间上选择 80 个 bin-center 帧、做 420x224 letterbox,并生成原视频绝对
130
+ 毫秒时间元数据。2026-09-02 的 16 段 answer-free CPU 门禁为 16/16 PASS,
131
+ `video_grid_thw=[[40,14,26]]`,最终选中蓝帧为 0。该门禁数据不需要传到
132
+ Farm04;Farm04 直接拉取本目录文件覆盖官方目录即可。
133
 
134
  ## 本地评测边界
135
 
pipelines/Tr006_V2/docker/segment-algorithm/inference.py CHANGED
@@ -79,10 +79,11 @@ with no answer is simply scored incorrect.
79
 
80
  Frame sampling
81
  --------------
82
- The clip is decoded with `decord <https://pypi.org/project/decord/>`_ using a single
83
- batched read of exactly 80 evenly spaced frame indices. The pixels come from the
84
- trimmed clip, while their timestamps are reconstructed on the original timeline
85
- from ``Request.start_time``. Decoding and processor work count towards latency.
 
86
 
87
  Trying it out
88
  -------------
@@ -108,6 +109,7 @@ import torch
108
  # "random_device could not be read" or a segfault).
109
  import decord
110
  from focus import Request, Response, load_requests, save_items
 
111
  from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
112
 
113
  logging.basicConfig(
@@ -124,6 +126,11 @@ MODEL_PATH = RESOURCES_PATH / "model"
124
  INPUT_PATH = Path("/input")
125
  OUTPUT_PATH = Path("/output")
126
  N_FRAMES = 80
 
 
 
 
 
127
  MAX_NEW_TOKENS = 64
128
  USE_OVERLAY = False # The trained Tr006 V2 model used plain frames + metadata.
129
  VIDEO_DIR = INPUT_PATH / ("overlayed" if USE_OVERLAY else "plain")
@@ -212,13 +219,64 @@ def clip_path_for(req: Request) -> Path:
212
  return VIDEO_DIR / f"{req.qID}.mp4"
213
 
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  def your_preprocessing(video_path: Path, req: Request) -> tuple[np.ndarray, dict]:
216
  """Decode exactly ``N_FRAMES`` and attach original-timeline millisecond PTS.
217
 
218
- Official clips are already trimmed and encoded at 5fps. The local frame
219
- indices are therefore sampled across the whole clip, while ``frames_indices``
220
- are rebuilt from Request.start_time so Qwen's processor emits absolute
221
- ``<... seconds>`` anchors, matching the Tr006 fixed80 training contract.
222
  """
223
  log.debug("Decoding %s (size on disk: %d bytes)", video_path, video_path.stat().st_size)
224
 
@@ -227,24 +285,36 @@ def your_preprocessing(video_path: Path, req: Request) -> tuple[np.ndarray, dict
227
  n_frames = len(vr)
228
  if n_frames <= 0 or base_fps <= 0:
229
  raise RuntimeError(f"invalid video stream: n_frames={n_frames}, fps={base_fps}")
230
- # np.linspace + round mirrors the fixed-frame preprocessing and supports
231
- # short clips by repeating the nearest decoded frame.
232
- indices = np.linspace(0, n_frames - 1, N_FRAMES).round().astype(np.int64)
233
- log.debug("Video: %d frames @ %.3f fps; sampling %d across %.2f s", n_frames, base_fps, N_FRAMES, n_frames / base_fps)
234
- frames = vr.get_batch(indices).asnumpy() # (T, H, W, 3) uint8 RGB — one batched decode
 
 
 
 
 
 
 
 
 
 
 
 
235
  del vr
236
 
237
  # Qwen3's processor averages timestamps per temporal patch (2 frames), so
238
  # metadata must have one entry per decoded frame and use the absolute source
239
  # timeline. The request endpoint is in seconds; the training manifest used
240
  # the same millisecond grid and ``precomputed_absolute_pts`` backend.
241
- absolute_ms = np.rint((float(req.start_time) + indices / base_fps) * 1000.0).astype(np.int64)
242
  metadata = {
243
  "fps": 1000,
244
  "frames_indices": absolute_ms.tolist(),
245
  "total_num_frames": int(absolute_ms.max()) + 1,
246
- "width": int(frames.shape[2]),
247
- "height": int(frames.shape[1]),
248
  "duration": (int(absolute_ms.max()) + 1) / 1000.0,
249
  "video_backend": "precomputed_absolute_pts",
250
  }
 
79
 
80
  Frame sampling
81
  --------------
82
+ The clip is decoded with `decord <https://pypi.org/project/decord/>`_. Full-frame
83
+ solid-blue signal-loss screens are detected from pixels, without relying on a
84
+ video identifier. Exactly 80 bin-centre frames are then sampled over the remaining
85
+ time and letterboxed to the Tr006 training resolution. Their timestamps are mapped
86
+ back to the original timeline from ``Request.start_time``.
87
 
88
  Trying it out
89
  -------------
 
109
  # "random_device could not be read" or a segfault).
110
  import decord
111
  from focus import Request, Response, load_requests, save_items
112
+ from PIL import Image, ImageOps
113
  from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
114
 
115
  logging.basicConfig(
 
126
  INPUT_PATH = Path("/input")
127
  OUTPUT_PATH = Path("/output")
128
  N_FRAMES = 80
129
+ FRAME_WIDTH = 420
130
+ FRAME_HEIGHT = 224
131
+ BLUE_DETECT_CHUNK = 64
132
+ BLUE_DETECT_STRIDE = 16
133
+ BLUE_REFINE_ATTEMPTS = 2
134
  MAX_NEW_TOKENS = 64
135
  USE_OVERLAY = False # The trained Tr006 V2 model used plain frames + metadata.
136
  VIDEO_DIR = INPUT_PATH / ("overlayed" if USE_OVERLAY else "plain")
 
219
  return VIDEO_DIR / f"{req.qID}.mp4"
220
 
221
 
222
+ def blue_screen_flags(frames: np.ndarray) -> np.ndarray:
223
+ """Classify full-frame solid-blue signal loss on a sparse pixel grid."""
224
+ sampled = frames[:, ::BLUE_DETECT_STRIDE, ::BLUE_DETECT_STRIDE]
225
+ pixels = sampled.reshape(len(sampled), -1, 3).astype(np.float32)
226
+ red, green, blue = pixels[:, :, 0], pixels[:, :, 1], pixels[:, :, 2]
227
+ blue_fraction = ((blue > 180) & (blue > 4 * (red + 1)) & (blue > 4 * (green + 1))).mean(1)
228
+ return (blue_fraction >= 0.98) & (pixels.std(1).mean(1) <= 15)
229
+
230
+
231
+ def sample_non_blue_indices(blue: np.ndarray) -> np.ndarray:
232
+ valid = np.flatnonzero(~blue)
233
+ if not len(valid):
234
+ log.warning("Every decoded frame looks like a blue screen; using the full clip")
235
+ valid = np.arange(len(blue))
236
+ positions = (np.arange(N_FRAMES) + 0.5) * len(valid) / N_FRAMES
237
+ slots = np.floor(positions + 0.5 - 1e-9).astype(np.int64).clip(0, len(valid) - 1)
238
+ return valid[slots]
239
+
240
+
241
+ def detect_blue_intervals(vr: decord.VideoReader) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
242
+ """Probe 80 time bins and expand blue probes to their represented intervals."""
243
+ probes = sample_non_blue_indices(np.zeros(len(vr), dtype=bool))
244
+ probe_frames = vr.get_batch(probes).asnumpy()
245
+ flags = blue_screen_flags(probe_frames)
246
+ if flags.all():
247
+ blue = np.zeros(len(vr), dtype=bool)
248
+ for begin in range(0, len(vr), BLUE_DETECT_CHUNK):
249
+ indices = np.arange(begin, min(begin + BLUE_DETECT_CHUNK, len(vr)))
250
+ blue[indices] = blue_screen_flags(vr.get_batch(indices).asnumpy())
251
+ return blue, probes, probe_frames
252
+ boundaries = np.concatenate((
253
+ [0], ((probes[:-1] + probes[1:]) // 2 + 1), [len(vr)]
254
+ ))
255
+ blue = np.zeros(len(vr), dtype=bool)
256
+ for index in np.flatnonzero(flags):
257
+ blue[boundaries[index]:boundaries[index + 1]] = True
258
+ return blue, probes, probe_frames
259
+
260
+
261
+ def letterbox_frames(frames: np.ndarray) -> np.ndarray:
262
+ return np.stack([
263
+ np.asarray(ImageOps.pad(
264
+ Image.fromarray(frame),
265
+ (FRAME_WIDTH, FRAME_HEIGHT),
266
+ method=Image.Resampling.BICUBIC,
267
+ color=(0, 0, 0),
268
+ centering=(0.5, 0.5),
269
+ ))
270
+ for frame in frames
271
+ ])
272
+
273
+
274
  def your_preprocessing(video_path: Path, req: Request) -> tuple[np.ndarray, dict]:
275
  """Decode exactly ``N_FRAMES`` and attach original-timeline millisecond PTS.
276
 
277
+ Official clips are already trimmed and encoded at 5fps. Blue signal-loss time
278
+ is detected online, then the same non-blue bin-centre and 420x224 letterbox
279
+ contracts used for Tr006 V2 are applied. ``frames_indices`` remain absolute.
 
280
  """
281
  log.debug("Decoding %s (size on disk: %d bytes)", video_path, video_path.stat().st_size)
282
 
 
285
  n_frames = len(vr)
286
  if n_frames <= 0 or base_fps <= 0:
287
  raise RuntimeError(f"invalid video stream: n_frames={n_frames}, fps={base_fps}")
288
+ blue, indices, frames = detect_blue_intervals(vr)
289
+ if blue.any():
290
+ for _ in range(BLUE_REFINE_ATTEMPTS + 1):
291
+ indices = sample_non_blue_indices(blue)
292
+ frames = vr.get_batch(indices).asnumpy()
293
+ selected_blue = blue_screen_flags(frames)
294
+ if not selected_blue.any() or blue.all():
295
+ break
296
+ radius = max(1, int(np.ceil(n_frames / (2 * N_FRAMES))))
297
+ for index in indices[selected_blue]:
298
+ blue[max(0, index - radius):min(n_frames, index + radius + 1)] = True
299
+ local_seconds = vr.get_frame_timestamp(indices)[:, 0]
300
+ frames = letterbox_frames(frames)
301
+ log.debug(
302
+ "Video: %d frames @ %.3f fps; excluded %d blue frames; selected %d non-blue bin centres",
303
+ n_frames, base_fps, int(blue.sum()), len(indices),
304
+ )
305
  del vr
306
 
307
  # Qwen3's processor averages timestamps per temporal patch (2 frames), so
308
  # metadata must have one entry per decoded frame and use the absolute source
309
  # timeline. The request endpoint is in seconds; the training manifest used
310
  # the same millisecond grid and ``precomputed_absolute_pts`` backend.
311
+ absolute_ms = np.rint((float(req.start_time) + local_seconds) * 1000.0).astype(np.int64)
312
  metadata = {
313
  "fps": 1000,
314
  "frames_indices": absolute_ms.tolist(),
315
  "total_num_frames": int(absolute_ms.max()) + 1,
316
+ "width": FRAME_WIDTH,
317
+ "height": FRAME_HEIGHT,
318
  "duration": (int(absolute_ms.max()) + 1) / 1000.0,
319
  "video_backend": "precomputed_absolute_pts",
320
  }
pipelines/Tr006_V2/docker/segment-algorithm/requirements.txt CHANGED
@@ -13,6 +13,7 @@ orena-focus>=0.3.4
13
  # of orena-focus, but anything imported directly is declared explicitly.
14
  decord>=0.6.0
15
  numpy>=1.26.0
 
16
 
17
  # Qwen3.5 support (including Qwen3_5ForConditionalGeneration and the
18
  # timestamp-aware Qwen3VLProcessor) landed after the 4.x line. The merged
 
13
  # of orena-focus, but anything imported directly is declared explicitly.
14
  decord>=0.6.0
15
  numpy>=1.26.0
16
+ Pillow>=11.0.0
17
 
18
  # Qwen3.5 support (including Qwen3_5ForConditionalGeneration and the
19
  # timestamp-aware Qwen3VLProcessor) landed after the 4.x line. The merged
pipelines/Tr006_V2/manifest/pipeline_manifest.json CHANGED
@@ -52,6 +52,26 @@
52
  "lora_rank": 64,
53
  "lora_alpha": 128
54
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  "submission_image_snapshot": {
56
  "image_id": "sha256:3ea66ddf64a40c9d2389f7c78326ab3728d54d6bb287b0cfa27cdee391827c5f",
57
  "runtime_backend": "transformers Qwen3_5ForConditionalGeneration",
 
52
  "lora_rank": 64,
53
  "lora_alpha": 128
54
  },
55
+ "online_inference_preprocessing": {
56
+ "version": "v2_online_blue_pixel_bin_center_letterbox_20260902",
57
+ "video_id_mapping_required": false,
58
+ "blue_detection": "sparse pixel probes with boundary refinement; full scan fallback when all probes are blue",
59
+ "sampling": "80 bin-center frames over detected non-blue time",
60
+ "letterbox": "PIL bicubic 420x224",
61
+ "metadata": "absolute millisecond PTS from request.start_time plus official clip PTS",
62
+ "inference_sha256": "a1eae658018582a5feb9fc0b844c5ae38c7c1dc73c541efb928e991924c62005",
63
+ "cpu_gate": {
64
+ "date": "2026-09-02",
65
+ "samples": 16,
66
+ "passed": 16,
67
+ "selected_blue_frames": 0,
68
+ "video_grid_thw": [40, 14, 26],
69
+ "mean_preprocess_seconds": 4.254098,
70
+ "max_preprocess_seconds": 10.904805,
71
+ "answers_read": false,
72
+ "test_references_read": false
73
+ }
74
+ },
75
  "submission_image_snapshot": {
76
  "image_id": "sha256:3ea66ddf64a40c9d2389f7c78326ab3728d54d6bb287b0cfa27cdee391827c5f",
77
  "runtime_backend": "transformers Qwen3_5ForConditionalGeneration",