Fix Tr006 V2 online Segment preprocessing
Browse filesPixel-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`
|
| 111 |
|
| 112 |
-
1.
|
| 113 |
-
2.
|
| 114 |
-
3.
|
| 115 |
-
4. `
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/>`_
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
| 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.
|
| 219 |
-
|
| 220 |
-
|
| 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 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) +
|
| 242 |
metadata = {
|
| 243 |
"fps": 1000,
|
| 244 |
"frames_indices": absolute_ms.tolist(),
|
| 245 |
"total_num_frames": int(absolute_ms.max()) + 1,
|
| 246 |
-
"width":
|
| 247 |
-
"height":
|
| 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",
|