fullseye

op の高速化・省メモリ化・動画処理 — 実測にもとづく調査報告(2026-09-03)

目的: 「op の高速化と省メモリ化について調査した上で着手したい。動画(動画像処理)も扱えるようにしたいが、そこが課題になるはず」(ユーザー、2026-09-03)への 実装前の調査。 性格: 実測値 + コードの根本原因(file:line)+ 選択肢の順位づけ。実装はしていない(この文書 1 本だけを追加)。 規律: 数字はすべてこの PC でこの日に測ったもの。熱定常でない(docs/FSCRIPT_MEASUREMENTS.md §0-a の 1.7 倍ぶれ問題は本測定にも当てはまる)ので、絶対値は ±30〜70 % の幅を持って読む。倍率(内訳の構造)のほうが信頼できる。


0. 結論(先に要点)

  1. 遅さの正体は「Python」ではなく「scipy.ndimage を float64 で単スレッド実行している」こと。同じ処理を cv2(IPP+SIMD+24 スレッド)で走らせると 2048² で gaussian 58 → 8.6 ms(6.8×)、median 1827 → 44.6 ms(41×、生の cv2 uint8 なら 3.4 ms = 540×)、gray opening 154 → 17 ms(9×)、canny 205 → 38 ms(5×)、CLAHE 386 → 41 ms(9×)。cv2 経路は registry に既にある(cv_* 76 op、backends.py)が、コア名(gaussian / median …)の既定実装は scipy のままなので、進化 champion もユーザーの fullseye.apply(x, "gauss_filter") も遅い方を踏む。
  2. 1080p @ 30 fps(62 Mpx/s)に今のコア実装で届く op は 56 中 25(§1.4)。median / percentile(2.3 Mpx/s、27 倍不足)、FFT 系(12〜19 Mpx/s)、幾何変換(16〜18 Mpx/s)、corner_response / clahe(11 Mpx/s)が 10 倍級で外れる。cv2 ラッパ側は同じ処理が 100〜580 Mpx/s。
  3. メモリは「入力の何倍か」で見ると 1〜8 倍、最悪 22 倍corner_response 8×(2048² で 256 MB)、FFT lowpass/highpass 7.1×(complex128 の中間)、cfa_to_rgb 6×、ncc_locate/shape_locate は RSS ピーク 21〜22×(ndimage.correlate の内部バッファ)。core の separable filter 系は 1.0×(出力のみ)で健全。float64 という選択自体が uint8 比 8 倍の常駐コスト。
  4. dtype の実態: 公開契約は float64 0,1だが、facade は uint8 を 拒否しない(api.py:1089-1103 は「数値 dtype か」しか見ない)。uint8 を通すと core op の出力が uint8 のまま(gaussian/mean_box/gerode/rotate_img)/float16(sobel_mag)/全画素 1(threshold) になり、例外を出さずに間違う(§1.3)。uint8 の高速経路は存在せず、uint8 の拒否も存在しない — 両方欠けている。
  5. 動画: 読む側(video.iter_frames)は 18 fps(1080p gray)、同じファイルを cv2 で素読みすると 204 fps。差の 11 倍は 1 フレームごとの uint8→float64 変換 + 輝度 matmul(video.py:62-97)で、デコードではない。時間軸 op(videops.py)は 全フレームを (T,H,W) float64 で一括保持する API(videops.py:42-71)で、30 フレームの 1080p = 475 MB(RSS ピーク 947 MB)。ストリーミング・リングバッファ・状態つき op の契約は 無い(registry の video sort は 2 op のみ、backends_typed.py:100)。
  6. GPU 経路は本物だが「1 op ずつ D2H/H2D」だと転送床(1080p 1 枚で 13.7 ms、8 枚で 110 ms)に食われる。常駐で 5-op 連鎖を回すと 1080p 2.1 ms/フレーム(計算のみ)、転送込み 16 ms、CPU 連鎖 111 ms(§1.6)。動画向けには フレームを GPU に常駐させたままリングで回す設計が要る。
  7. 明日着手する 3 件(§4.3): (h) tools/bench_ops.py + JSON ベースライン、(a′) コア op の CPU 高速 twin テーブル(cv2)+ parity ゲート + uint8 fail-closed、(d) VideoPipeline(ストリーミング reader / uint8 ゼロコピー / リングバッファ / 状態つき op 契約)

0.1 測定環境

項目 値(実測)
CPU Intel Core Ultra 7 270K Plus、24 論理コア(Get-CimInstance Win32_Processor: cores=24, logical=24)
RAM 128 GB(psutil 127.5 GiB)
GPU NVIDIA GeForce RTX 5090 32 GB、driver 610.74(nvidia-smi)
Python 3.11.9 (MSC) py -3.11
numpy / scipy 2.4.6(OpenBLAS 0.3.31, 24 threads)/ 1.15.2
OpenCV 5.0.0(FFMPEG YES avcodec 61.19、Intel IPP 2026.0、Parallel=Concurrency、getNumThreads()=24)
scikit-image / kornia / PIL 0.26.0 / 0.8.3 / 12.3.0
torch グローバル: 2.11.0+cpu(CUDA 不可)。GPU 実測は <ローカルの作業パス>(2.11.0+cu128、numpy 1.24.4、scipy 1.15.2)で accel.py / accel_vol.py のみ実行
動画 I/O imageio 2.37.4 + imageio-ffmpeg 0.6.0(同梱 ffmpeg 7.1 バイナリ)。システム ffmpeg 無しav / numba / cupy / pyfftw 無し、psutil 7.2.2 あり
スレッド threadpoolctl: openblas 24 / openmp 24、torch 24、cv2 24。scipy.ndimage は常に単スレッド(スレッドプールを持たない)

レジストリ実測: 860 op(image 518 / region 129 / contour 65 / points 44 / signal 26 / volume 14 / color 11 / video 2 …)、うち guard 付き 475。定義元: backends_auto 227 / backends_typed 122 / backends_halcon_ext 81 / ops.py 76 / backends.py(sk_/cv_) 76 / backends_r3 56 …。accel(GPU)対応 90 mapping + volume 10。

測定方法(使い捨てスクリプト、リポジトリ外 scratchpad/prof_ops.pyprof_accel.py):


1. ベースライン実測

1.1 2048² float64(4.19 Mpx)— 主表

tm× = tracemalloc ピーク ÷ 入力バイト(32 MB)、rss× = RSS ピーク増分 ÷ 入力バイト。「ms (u8 in)」は同じ op に 0..255 の uint8 を渡した時間、「out dtype」はそのときの出力 dtype(正しさの検査ではなく挙動の記録)。

op family module ms (f64) Mpx/s tm× rss× ms (u8 in) out dtype (u8 in)
cfa_to_rgb color backends_color 81.8 51 6.1 6.1 88.6 float64
principal_comp color backends_color 352.5 12 5.0 5.0 374.1 float64
rgb1_to_gray color backends_color 72.9 58 2.0 2.0 96.3 float64
trans_from_rgb color backends_color 124.3 34 2.1 2.1 145.0 float64
canny edge ops 204.9 20 4.0 4.0 174.9 float64
corner_response edge ops 386.7 11 8.0 8.0 233.6 float64
cv_canny edge backends 38.3 110 2.0 2.0 21.8 float64
fft_image fft backends_auto 225.5 19 4.0 4.0 220.4 float64
highpass fft ops 339.5 12 7.1 7.1 322.2 float64
lowpass fft ops 313.3 13 7.1 7.1 296.4 float64
sk_butterworth fft backends 107.6 39 2.5 3.5 115.7 float64
cv_bilateral filter backends 20.8 201 1.5 1.4 18.9 float64
cv_box filter backends 10.5 398 1.1 1.1 2.5 uint8
cv_gaussian filter backends 8.6 486 1.1 1.1 1.3 uint8
cv_median filter backends 44.6 94 2.0 2.0 27.2 float64
cv_scharr filter backends 67.8 62 3.0 3.0 60.9 float64
gauss_filter filter backends_auto 60.9 69 1.1 1.1 76.0 float64
gaussian filter ops 58.2 72 1.0 1.0 38.3 uint8
log filter ops 161.7 26 2.0 2.0 92.1 float64
mean_box filter ops 53.6 78 1.0 1.0 31.3 uint8
median filter ops 1827.3 2.3 1.0 1.0 1767.3 uint8
percentile filter ops 1844.6 2.3 1.0 1.0 1741.5 uint8
sobel_mag filter ops 150.2 28 3.0 3.0 110.7 float16
std_filter filter ops 167.3 25 4.0 4.0 104.7 float64
unsharp filter ops 93.1 45 2.0 2.0 63.8 float64
xkor_gaussian filter backends_kornia 39.2 107 1.5 4.5 47.6 float64
clahe gray ops 386.2 11 3.3 3.3 188.6 float64
cv_clahe gray backends 41.3 102 2.0 2.0 25.4 float64
equalize gray ops 109.4 38 2.0 2.0 42.4 float64
gamma gray ops 39.8 105 2.0 2.0 11.9 float64
invert gray ops 17.2 244 2.0 2.0 8.5 float64
identity gray ops 0.0 0.0 0.0 0.0 uint8
cv_open morph backends 17.4 241 1.1 2.0 3.5 uint8
fill_holes morph ops 80.7 52 1.2 1.2 84.7 float64
gerode morph ops 88.9 47 1.0 1.0 67.2 uint8
gopen morph ops 153.9 27 2.0 2.0 111.0 uint8
opening_circle morph backends_auto 67.4 62 1.4 1.3 73.2 float64
reg_dilate morph ops 42.0 100 1.2 1.2 47.6 float64
reg_erode morph ops 36.6 115 1.2 1.2 43.7 float64
tophat morph ops 177.3 24 2.0 2.0 123.1 float64
area_frac props ops 22.9 184 1.2 1.2 29.9 float
blob_count props ops 28.1 149 1.2 1.2 34.7 float
circularity props backends_auto 90.6 46 3.1 3.1 96.7 float
count_obj props backends_auto 28.8 146 1.2 1.2 34.8 float
dist_transform props ops 293.7 14 4.1 4.1 300.8 float64
remove_small props ops 79.9 53 3.0 3.0 85.3 float64
select_largest props ops 84.0 50 3.0 3.0 88.3 float64
cv_otsu seg backends 38.8 108 2.0 2.0 24.2 float64
dyn_threshold seg backends_auto 90.7 46 3.0 3.0 91.8 float64
otsu seg ops 34.7 121 2.1 2.1 33.9 float64
threshold seg ops 9.4 447 1.1 1.1 8.8 float64
affine_warp xform ops 266.2 16 2.0 2.0 249.3 uint8
rescale_img xform ops 229.0 18 2.0 2.0 204.9 uint8
rotate_image xform backends_auto 270.3 16 2.0 2.0 280.4 float64
rotate_img xform ops 270.4 16 2.0 2.0 246.1 uint8
zoom_image_size xform backends_auto 87.5 48 3.0 3.0 96.7 float64

重い op(2048² は時間予算外なので 512² のみ):

op module 512² ms Mpx/s tm× rss×
shape_locate(48² テンプレ、30° 刻み 12 回転) ops 3076.6 0.09 7.2 22.3
sk_tv(Chambolle TV) backends 502.1 0.52 10.0 10.0
lines_gauss backends_auto 366.3 0.72 15.0 14.6
ncc_locate(48² テンプレ) ops 260.9 1.0 6.1 21.3
bilateral(純 numpy 25 シフト) ops 127.9 2.05 6.0 6.0
edges_sub_pix backends_auto 27.7 9.5 6.0 5.1
sk_canny backends 12.0 21.9 6.0 5.5
sk_find_contours backends 3.7 71.6 1.7 1.4

注意(honest): median / percentile の scipy 実装は 入力の内容で 10 倍変わる。3 % ノイズ入りのシーンで 2048² 1827 ms、ノイズ無しの平滑画像(clip で飽和多数)では 197 ms、別測で 92 ms(選択アルゴリズムが同値の多さに依存)。cv2 の medianBlur(uint8, 5) は内容によらず 3.4 ms。「実画像(ノイズあり)」でこそ最悪側に落ちるので、ベンチにはノイズ入りを使うべき。

1.2 Top-10(画素あたり最遅 / メモリ最大)

画素あたり最遅(Mpx/s、低い順): 1 shape_locate 0.09 / 2 sk_tv 0.52 / 3 lines_gauss 0.72 / 4 ncc_locate 1.0 / 5 bilateral 2.05 / 6 percentile 2.3 / 7 median 2.3 / 8 edges_sub_pix 9.5 / 9 corner_response 10.9 / 10 clahe 10.9(次点 principal_comp 11.9、highpass 12.4、lowpass 13.4、dist_transform 14.3)。

メモリ最大(入力の倍率、2048² f64): 1 ncc_locate/shape_locate rss 21〜22×(512² 実測)/ 2 lines_gauss 15× / 3 sk_tv 10× / 4 corner_response 8.0×(256 MB)/ 5 lowpass・highpass 7.1×(228 MB)/ 6 cfa_to_rgb 6.1× / 7 bilateral 6.0× / 8 principal_comp 5.0×(480 MB)/ 9 xkor_gaussian rss 4.5×(torch 常駐分)/ 10 dist_transform・std_filter・fft_image・canny 4.0×。

1.3 dtype 昇格・コピー・バックエンド

1.4 1080p(1920×1080 = 2.07 Mpx)の fps — 30 fps(62 Mpx/s)予算

判定 op(fps)
30 fps 以上(25 op) cv_gaussian 279 / threshold 215 / cv_box 184 / invert 118 / cv_bilateral 107 / area_frac 101 / cv_open 91 / blob_count 80 / count_obj 80 / mean_box 69 / otsu 59 / xkor_gaussian 58 / reg_erode 58 / cv_canny 54 / cv_otsu 53 / gaussian 53 / gamma 53 / reg_dilate 51 / gauss_filter 48 / cv_clahe 48 / cv_median 46 / cv_scharr 32 / opening_circle 32 / dyn_threshold 30
10〜30 fps(29 op) gerode 30 / unsharp 29 / rgb1_to_gray 29 / fill_holes 26 / remove_small 26 / cfa_to_rgb 25 / select_largest 25 / zoom_image_size 23 / circularity 23 / sobel_mag 21 / sk_butterworth 20 / equalize 18 / log 18 / gopen 17 / trans_from_rgb 17 / std_filter 16 / tophat 14 / canny 14 / fft_image 11 / dist_transform 10
3〜10 fps rescale_img 9.9 / lowpass 8.5 / rotate_img 8.1 / affine_warp 8.1 / rotate_image 8.0 / highpass 7.8 / corner_response 6.9 / principal_comp 5.7 / clahe 5.4
10 倍以上不足 median 1.1 / percentile 1.1(+ 512² 実測から換算: bilateral ≈ 1 fps、ncc_locate ≈ 0.5 fps、sk_tv ≈ 0.25 fps、shape_locate ≈ 0.04 fps)

読み: 1 op 単体なら半数弱が届くが、動画パイプラインは 3〜6 op を連ねるので、「コア実装の 5-op 連鎖」は 1080p で 111 ms = 9 fps(§1.6 の CPU chain)。cv2 twin に載せ替えられれば同じ連鎖が ~20 ms 台。

1.5 3-D volume(128³ = 2.1 M voxel、16 MB f64)

op ms (f64) tm× ms (u8 in) out dtype (u8) GPU(RTX 5090)ms GPU 倍率
vol_gaussian 33.5 1.0 18.4 uint8 14.8(転送込み) 2.3×
vol_median 539.8 1.0 525.2 uint8 25.1 21×
vol_erode 53.0 1.0 36.7 uint8 14.3 3.7×
vol_threshold 4.6 1.1 4.6 float64 14.0 0.3×(転送負け)
vol_count 6.1 0.6 8.2 float
vol_dilation_ball 35.6 1.1 8.6 float64(u8 は 9×)
vol_opening_ball 39.9 1.1 32.7 float64
macro_vol_denoise 44.8 2.0 48.5 float64(u8 は 17×)

3-D も uint8 問題は同じ(ops.py:725-738)。GPU は単発でも median が 21 倍(docs/GPU_ACCEL_PLAN.md の「~64×」は 128³×4 バッチ・常駐での値で、単発転送込みはこの表の通り)。

1.6 既存 accel / GPU パイプライン(accel.py, accel_vol.py)

accel.run_batch = H2D → op → D2H(accel.py:769-774)、run_pipeline = 転送 1 回で op 連鎖(accel.py:777-796)。loco venv(cu128)で RTX 5090、同期つき最小時間。「CPU RT」は ops.RT 直接(numpy 1.24 環境)。

形状 転送のみ gaussian GPU / CPU median GPU / CPU bilateral GPU / CPU lowpass GPU / CPU threshold GPU / CPU 5-op 連鎖 GPU 転送込み / 常駐計算のみ / CPU
512² ×1 1.25 ms 2.1 / 2.1 2.2 / 111 3.7 / 121 1.6 / 11.1 1.4 / 0.54 2.6 / 1.17 / 13.7
512² ×8 9.4 10.0 / 16.6 15.0 / 892 12.3 / 968 14.3 / 90.8 9.6 / 4.4 15.9 / 1.59 / 111
1080p ×1 13.7 14.1 / 18.9 18.2 / 880 14.0 / 986 16.0 / 96 11.6 / 4.6 16.1 / 2.08 / 111
1080p ×8 110 125 / 155 132 / 7046 116 / 7859 81 / 746 78 / 37.7 84 / 3.71 / 926
2048² ×1 19.7 20.5 / 58.2 37.9 / 1784 23.2 / 1964 26.1 / 272 27.7 / 9.4 33.0 / 6.73 / 291

読み:

1.7 facade のオーバーヘッド(2048²)

項目 実測 出典
find_op 線形走査(先頭 / 末尾 / HALCON 別名) 0.1 / 9.0 / 3.0 µs api.py:921-944(860 要素の list 走査、1 apply で 2 回: api.py:1239, 1262)
api.apply(identity, 512²) vs RT 直接 2 µs vs 0.1 µs api.py:1238-1276
_coerce_input(region f64) 17.6 ms(np.unique 16.9 ms) api.py:1035-1038: 毎回 np.unique(a)(ソート O(N log N))
api.apply(reg_erode) / 同 coerce=False / RT 直接 35.0 / 18.2 / 17.8 ms region op は coerce で 2 倍になる
sanitize(image / region) 1.9 / 6.1 ms backend_safe.py:387-389 isfinite 全走査、region は region01 でさらに 2 走査(:361)
段間 np.clip(新規配列) 8.3 ms ops.py:1077-1084(_apply は毎段 clip)、api.run_pipeline の CPU 経路は clip しない(api.py:1338-1348)
u8→f64 /255 / f64→f32 17.5 / 5.1 ms video.py:62-72, accel.py:38-42
_try_accel の辞書再構築 90 要素の内包表記を 毎 apply api.py:1164

facade の固定費は 1 op あたり数 ms(2048²)で、op 本体(数十〜数百 ms)に比べれば小さい。ただし region op の np.unique と、動画で 30 fps × 5 op = 150 回/s 掛かる sanitize+clip1080p で 1 フレーム 10〜15 ms 分になり、無視できなくなる。

1.8 タイル分割・フレーム並列・float32 の追加実測(2048²、scale.process_tiled_mt = 既存)

op 全体 tiled w1 w8 w16 全体との最大差
gaussian 59.3 60.2 14.5(4.1×) 14.9 0.0
median(平滑画像) 196.6 228.0 55.2(3.6×) 51.9 0.0
gerode 51.0 51.3 13.4(3.8×) 13.3 0.0
sobel_mag 130.2 104.0 26.8(4.9×) 22.1 0.98(不一致)
canny 192.2 152.6 32.9(5.8×) 29.1 1.0(不一致)
otsu 41.5 59.0 29.6(1.4×) 32.1 1.0(不一致)

2. コードに見る根本原因

2.1 dtype と契約

2.2 コピー・中間配列

2.3 Python レベルの画素ループ

2.4 全体処理 vs タイル

2.5 スレッド

2.6 accel パイプラインが既に持つもの


3. 動画・画像列処理

3.1 いま在るもの(実コードで確認)

モジュール 役割 動画観点の性質
video.py iter_frames(generator、video.py:116-195)/ read_frames(全読み np.stack:198-214)/ frame_pairs / write_video / probe ストリーミングはある。ただし 1 フレームごとに _to01(uint8→f64 /255 + clip = 2 コピー、:62-72)と _coerce(RGB→gray を a @ _LUMA の float64 matmul、:75-97)を通す。imageio 優先、cv2 は fallback(:99-113)
acquire.py Camera.grab/stream(acquire.py:245-273)= OpenCV / dir / callable / GenICam / Basler 同じ _coerce で毎フレーム float64 化(:88-108)
videops.py temporal_mean/median/std/max/min、frame_difference、background_subtraction、temporal_gradient、motion_energy、moving_average、spatiotemporal_gaussian/sobel、per_frame、flicker_reduce、optical_flow_sequence 全部 (T,H,W) float64 を一括で受ける(_as_video, videops.py:42-71: asarray + isfinite 全走査)。状態を跨いで持つ設計は無い
flow.py / motion.py / sceneflow.py LK/HS 密フロー、支配運動、FoE/TTC 2 フレーム関数(pure)。LK 1080p 1.26 s / 437 MB、HS iters=50 で 14.7 s
events.py DVS 事象、time_surface(T スタック)、contrast_maximization time_surface は T ループ内で 2 フレーム関数を呼ぶ(events.py:158-162)= ストリーム化しやすい形だが入口はスタック
engine.py / graphengine.py pipeline / DAG の実行、run_stepwise は全中間結果を保持(engine.py:221-237) フレーム概念無し(1 画像 → 1 結果)
fsruntime.py FullseyeRuntime.inspect = 1 フレーム 1 サイクル、deadline は事後判定(fsruntime.py:454-470) サイクル型。ストリーム/リングは無い
backends_typed.py video sort の registry op は 2 つ(tb_temporal_bandpass, tb_temporal_band_power、motionmag 由来) どちらも (T,H,W) 一括
vloop.py / sim_source.py 遅延つき閉ループ台(MuJoCo)/ F4 契約 高速ビジョン計画(docs/HIGHSPEED_VISION.md)の実験台。op の速度が遅延に直結

3.2 実測(動画経路)

項目 実測 意味
video.iter_frames(gray=True) 1080p H.264 18.3 fps、RSS ピーク +123 MB 変換込み 55 ms/フレーム
gray=False(RGB f64) 21.3 fps gray の方が遅い = 輝度 matmul が支配
read_frames 30 フレーム 17.0 fps、475 MB(RSS ピーク 947 MB) 1 秒の 1080p で 0.5〜1 GB
cv2 VideoCapture.read() 素読み(BGR uint8) 203.8 fps デコード自体は 4.9 ms/フレーム
cv2 素読み + cvtColor gray uint8 187.2 fps gray 変換は 0.4 ms
write_video 30 フレーム 1080p 582 ms(19 ms/フレーム) imageio-ffmpeg 経由
temporal_median(16×540×960 = 63 MB) 113 ms(7.1 ms/フレーム)、1.1× np.median(axis=0) のソート
background_subtraction 166 ms、2.1×(vid − bg の (T,H,W) 差分を丸ごと作る、videops.py:185-186)  
moving_average(w=3) 24 ms、1.0× uniform_filter1d
spatiotemporal_gaussian 73 ms、1.0×  
per_frame(gaussian) 96 ms(6 ms/フレーム)、1.2× 直列
optical_flow_sequence(LK、4 フレーム) 839 ms(280 ms/対 @540×960) 1080p 換算 1.1 s/対
simulate_events 1080p 対 70 ms、79 MB 14 fps
frame_difference 2 フレーム 1080p 20 ms、63 MB  

codec/I/O の結論: この PC では imageio-ffmpeg(同梱 ffmpeg 7.1)と cv2(FFMPEG/avcodec 61)の両方が使える。デコード性能は十分(204 fps)で、ボトルネックは 100 % Python 側の毎フレーム float64 化av(PyAV)は無い。

3.3 動画経路に必要なもの(現状との差)

  1. ストリーミング: iter_frames は既に generator。欠けているのは dtype="uint8" で素通しする口と、gray 変換を cv2 cvtColor(0.4 ms)に任せる分岐。これだけで 18 → ~180 fps。
  2. リングバッファ: temporal median / 背景差分 / moving average / temporal gradient / time_surface / optical flow(前フレーム保持)は 窓 N フレームの状態があれば 1 フレームずつ出せる。今は (T,H,W) 一括なので メモリ = T × フレーム(1 秒 = 0.5 GB f64、uint8 なら 62 MB)。リングなら N × フレーム(N=5 の uint8 1080p = 10 MB)。
  3. 状態つき op の契約: registry の Opfn(v, a, b) の純関数(ops.py:795-802)で状態を表せない。表現案(§4 (d)): Opstateful: boolstate_factory() を足し、fn(v, a, b, state) 規約の 2 番目のシグネチャを持つ、または video sort の op を “reducer” として push(frame) -> out を持つクラスで登録する。進化(decode/_candidates)は in_sort が video の op を既に別枠で扱える(backends_typed.py:133-137_NEW_SORTS)ので、genome 互換を壊さずに追加できる
  4. uint8 ゼロコピー経路: 契約が float64 なので、今は uint8 を入れると壊れる(§1.3)。最低限 入口で fail-closed(拒否 or 明示変換)にし、その上で「uint8 で計算できる op」(cv2 twin)だけを uint8 のまま通す表を持つ。
  5. タイル: 1080p は 16 MB f64 なので L2 溢れによる 1.5〜1.8 倍(512²→2048² で 28×、理想 16×)を除けば大きな問題ではない。4K 以上・複数フレーム同時処理で効く。既存 process_tiled_mt の 4 倍は魅力だが §1.8 の _norm 問題を先に解く。
  6. fps 予算: §1.4。コア実装で 30 fps に届くのは 25/56、連鎖なら 9 fps。cv2 twin なら連鎖でも 30〜50 fps、GPU 常駐なら 480 fps 相当(計算のみ)。
  7. GPU: 常駐リング(torch テンソルのまま N フレーム保持)+ accel.run_pipeline の「転送を外した版」(accel.ACCEL[name][0](t, a, b, dev) を直接連結)で 2.1 ms/フレーム。D2H は 結果(region/feature)だけにする。
  8. メモリ上限: 1080p f64 1 枚 16.6 MB、op の中間 1〜8×、region op の np.unique が別に 1 枚、5-op 連鎖で ~100〜150 MB/フレームの瞬間ピーク。リング N=5 + 中間 8× でも 300 MB 以内 → 128 GB の PC では無問題だが、組込み(evis/ロボ)側では uint8 リング + cv2 in-place が要る

4. 選択肢と推奨(実測の効き ÷ 手間 で順位づけ)

「効き」は §1 の実測から、「手間」は触るファイル数と契約変更の有無から。fail-soft/ledger(backend_safe.guardrecordsanitize)は全案で温存し、新経路の失敗も同じ ledger に source="fast" 等で記録する。

順位 仕組み 期待効果(実測根拠) 正しさのリスク テスト戦略 手間
1 (h) ベンチ harness tools/bench_ops.py + JSON ベースライン 本調査の prof_ops.py を repo に移し、op 集合 × サイズ × dtype の (ms, Mpx/s, tm×, rss×, out dtype, fallback 数) を out/bench_ops_baseline.json に保存。CI/手動で ±X %(既定 30 %、熱ぶれ考慮) 超過を検出。ノイズ入り画像を必ず含める(median の 10 倍差) 効果は「退行を捕まえる」こと。§0-a の 1.7 倍ぶれがあるので 相対比較(cv2 twin vs core、GPU vs CPU)を同一 run 内で出す設計にする 無し(read-only) harness 自身のスモーク(3 op、64²)。tests/test_bench_ops.py (1 ファイル + テスト)
2 (a′) コア op の CPU 高速 twin テーブル(cv2/IPP)+ parity ゲート + uint8 fail-closed accel.ACCEL と同じ構造で fast.FAST = {core_name: (fn_cv2, dtype_policy)} を作り、api._apply_impl_calldevice="cpu" かつ fast 有効かつ parity 済みなら twin を呼ぶ(既定 ON にするかは parity 結果次第; まず opt-in FULLSEYE_FAST=1 / apply(fast=True))。同時に _check_input_sortdtype 方針(uint8 → on_error="raise" なら拒否、既定は /255 明示変換 + ledger source="input")を追加 gaussian 6.8×、median 41×(uint8 twin なら 500×)、box 5×、gray open 9×、canny 5×、CLAHE 9×、otsu 1.1×(効かない op は載せない)。1080p 5-op 連鎖 111 → 推定 20〜25 ms(30 fps 圏内) 境界規約と丸め: cv2 の border(reflect101)と scipy(reflect=symmetric)は違う(accel で同じ罠を踏み修正済 docs/GPU_ACCEL_PLAN.md 2026-08-31 Batch 0)。uint8 twin は 1/255 量子化を伴う。→ accel.parity と同じ 5 点 a/b × 6 画像(定数・量子化含む)の interior <5e-3 ゲートを通った op だけ載せる。「速いが違う」は作らない tests/test_fast_parity.py(ゲート自動化)、tests/test_api.py に uint8 拒否/変換のケース、bench で twin/コアの同 run 比較 (新規 1 ファイル + api 数行 + テスト)
3 (d) VideoPipeline / 状態つき op 契約 + リングバッファ + uint8 ストリーム video.iter_frames(dtype="uint8", gray_backend="cv2")(18 → ~180 fps); ② FrameRing(n, dtype) (固定長 deque + 事前確保 (n,H,W) バッファ); ③ 状態つき op: class TemporalOp: def push(self, frame) -> out、registry には Op(stateful=True, state_factory=…) を追加し in_sort="video" で登録(genome 経路は _NEW_SORTS 扱いで既存 decode 不変); ④ VideoPipeline(stages, ring=n, device=…) が reader → ring → 段 → 結果だけ返す。temporal_median/背景差分/moving_average/time_surface/optical_flow を ring 版で提供 読み込み 11×、メモリ T× → N×(1 秒 1080p: 475 MB → N=5 uint8 で 10 MB)、temporal median は ring N=5 で 1080p 1 フレーム ~5 ms 見込み(63 MB 16 フレームで 113 ms から換算) (T,H,W) 一括版との 数値一致(窓の端処理: 一括版は全 T の中央値、ring 版は窓中央値 = 別物なので 同名にしないtemporal_median_window 等) ring 版 vs 一括版を 同じ窓幅で比較する差分テスト、状態リセットのテスト、フレーム落ち(shape 不一致)は fail-closed 中〜大(video.py + 新規 videostream.py + ops.Op 拡張 + api)
4 (g) GPU フレームバッチ/常駐リング accelResident(device)(テンソルのまま N フレーム保持)と run_resident(steps, tensor) を足し、VideoPipeline の device="cuda" で使う。D2H は終端(region/feature)だけ 1080p 5-op 2.1 ms/フレーム(計算のみ)、転送込みでも 16 ms。CPU 連鎖 111 ms から 50× accel の既存 parity(90/90 faithful)に依存。ring 版 temporal op の GPU 版は新規 parity 要 既存 tests/test_accel_pipeline.py 型 + loco venv でのみ走る GPU テスト(skip 条件) 中(accel 数十行 + VideoPipeline 分岐)。(d) の後
5 (f) フレーム並列 executor VideoPipeline(workers=k)フレーム単位に ThreadPoolExecutor(GIL は numpy/scipy が離す)。順序保持は ex.map 1080p gaussian 4.6×(233 fps)、sobel 4.7×、cv_median 4.6×。cv2 twin(既に 24 スレッド)には効かない(1.5×)。状態つき op とは相性が悪い(順序依存) 状態つき段では並列不可 → パイプラインを「純関数区間(並列)/状態区間(直列)」に plan で分割(accel_bridge.plan と同型) 並列/直列の結果一致テスト 小〜中。(a′) を入れると効きが減るので順位は下
6 (c) 大フレームのハローつきタイル 既存 scale.process_tiled_mtVideoPipeline の「純関数・局所 op 区間」に配線。Opglobal_reduction: bool(_norm/_signed01/otsu 等)を持たせ、それを含む op は自動で除外 2048² で 4×(gaussian 59 → 14.5 ms、median 3.6×)。1080p では L2 溢れ分(~1.5×)。メモリは tile² に上限 §1.8 の通り edges/segmentation を誤分類しており、sobel_mag/canny/otsu はタイルで別解になる。フラグ無しで配線してはいけない 全 tile_safe op の tiled vs whole の bit 一致テスト(既存 tests/test_scale.py を op 全数に拡張) 中(フラグ付与が 860 op に及ぶ → まず core 76 + cv_ 76)
7 (b) in-place / out= 配線 guard に out= を通す、_normnp.divide(x, mx, out=x)、段間 clip を np.clip(v, 0, 1, out=v)(自分の配列のときだけ) 速度は ほぼ変わらない(gaussian output= 57.3 vs 57.4 ms、clip out= 8.7 vs 8.3)。得られるのはピークメモリ −1×(2×→1× の op)と GC 圧。1080p 30 fps で毎秒 150 枚の 16 MB 配列確保を消せる 入力を書き換える op が混ざると input_mutated になる(今は 0 件)。identity は入力を共有して返すので in-place 後段が 入力を破壊しうる harness の input_mutated / shares_mem 列で常時監視 小だが効果も小。(d) の ring バッファ内で「事前確保 + out=」として実装するのが自然
8 (e) メモリプール / スクラッチ op ごとの中間配列(_corner_response 8×、FFT 7×)を scratch(shape, dtype) から借りる 8× → 2〜3× に下げられるが速度は変わらない。GPU 側は torch のキャッシュアロケータが既にやっている スレッド安全性(フレーム並列と競合)、ライフタイム 中。(b)(d) が済んでから、必要なら

補足(実測から落ちた案):

4.1 fail-soft / ledger との整合(全案共通)

4.2 各案の「効き」を一枚に

指標 現状 (a′) cv2 twin (d)+(g) GPU 常駐 (c) tiling 4×(局所 op) (f) フレーム並列
1080p gaussian 19 ms 3.6 ms 0.4 ms(常駐)/14 ms(転送込み) ~5 ms 4.3 ms(8 枚並列の 1 枚あたり)
1080p median 5×5 894 ms 21.6 ms(uint8 生なら ~2 ms) 3.6 ms(常駐) ~250 ms ~190 ms
1080p 5-op 連鎖 111 ms ~20〜25 ms(推定、gaussian+sobel+threshold+dilate+erode の twin 合算) 2.1 ms ~30 ms ~25 ms
動画読み 1080p gray 18 fps
動画読み uint8 素通し(d) ~180 fps 同左
1 秒 1080p の常駐メモリ 475 MB(f64 一括) 同左 GPU 側 ~60 MB(f32 ring 5)
同 ring N=5 uint8(d) 10 MB

4.3 明日着手する 3 件(順序つき)

  1. (h) tools/bench_ops.py + out/bench_ops_baseline.json — 半日。理由: 以降の全案が「速くなったか」「壊れていないか(out dtype / fallback / mutated)」を 同じ物差しで言う前提。今回の使い捨てスクリプトの再利用で済み、リスク 0。熱ぶれ対策として 同 run 内の相対値--warm N を必ず持つ。
  2. (a′) cv2 高速 twin テーブル + parity ゲート + uint8 fail-closed — 1〜2 日。理由: 測定した中で最大の効き(6.8〜41×)を、契約を変えず・進化 champion を壊さず・既存の parity 手法(accel と同型)で取れる。1080p で「コアの 5-op 連鎖 9 fps」を 30 fps 圏に入れる唯一の CPU 側の一手。uint8 の「黙って壊れる」を同時に閉じる(正しさの穴は速度の穴より先に塞ぐ)。
  3. (d) VideoPipeline(uint8 ストリーム reader + ring + 状態つき op 契約) — 2〜3 日。理由: 動画対応の 構造的な欠落(状態・リング・ストリーミング)はここでしか埋まらず、(g)(f)(c) はこの器の中の分岐として後から足せる。最初の実装範囲は reader + ring + temporal_median_window / background_subtraction_window / frame_difference / optical_flow(前フレーム保持)の 4 op + engine からの呼び出し。GPU 常駐(g)は (d) の device 引数として翌週。

やらないこと(今回の判断): float32 契約化、CPU torch の高速経路化、output= 配線を先にやること(速度が出ない)、scale_class のまま tiling を facade に載せること(誤分類)。


5. 付録

5.1 honest な限界

5.2 再現

5.3 本調査で見つけた「速度以外」の不具合候補(要別途対応)

  1. api._check_input_sort(api.py:1089-1103)が uint8 を通し、コア op が uint8 のまま計算する(ops.py:163-178, 240, 725-738)— 例外なしで別物の結果。
  2. scale.scale_class(scale.py:27,29,35-58)が _norm を含む edges/segmentation を tile_safe に分類し、タイル結果が全体結果と一致しない(sobel_mag 差 0.98、canny 1.0)。
  3. api._coerce_inputnp.unique(api.py:1036)は region op を毎回 2 倍遅くする。{0,1} 判定は np.isin/min-max で O(N) にできる。
  4. api._try_accel(api.py:1164)が呼ぶたびに ACCEL の逆引き辞書を作る(モジュールレベルにキャッシュ可)。

6. bench harness (実装済)

§4 の推奨 (h) を実装した(2026-09-03)。この調査の使い捨て profiler(scratchpad/prof_ops.py)を repo の恒久ツールに据えたもので、以降の (a′) cv2 twin / (d) VideoPipeline / (g) GPU 常駐 / (c) タイル が「本当に速くなったか」「壊れていないか」を 同じ 1 本の物差しで言うための土台。

追加物 役割
tools/bench_ops.py 測定 harness + JSON ベースライン比較(CLI)
tests/test_bench_ops.py harness 自身の契約テスト(3 op × 64² のスモーク、合成 2 倍退行の検出、未知 op の fail-closed、ノイズ画像の存在)
bench/bench_ops_baseline.json 実測ベースライン(--sizes 512,2048,1080p --dtypes float64)。out/.gitignore 対象なので、追跡したいベースラインは bench/ に置く(bench.py は module、bench/ は namespace dir なので import bench は従来どおり bench.py に解決する — 実測確認済み)

6.1 使い方

# 手元で 1 セット測る(既定 --set all / --sizes 512,2048,1080p / --dtypes float64)
PYTHONUTF8=1 py -3.11 tools/bench_ops.py --set core --sizes 512 --dtypes float64 --repeat 3

# op を名指し・複数サイズ・uint8 も込みで
PYTHONUTF8=1 py -3.11 tools/bench_ops.py --ops gaussian,median,cv_median --sizes 2048,1080p --dtypes float64,uint8

# ベースラインを書く(数分)
PYTHONUTF8=1 py -3.11 tools/bench_ops.py --sizes 512,2048,1080p --dtypes float64 --write-baseline bench/bench_ops_baseline.json

# 退行チェック(30 % 超で表を出して exit 1 = CI 用)
PYTHONUTF8=1 py -3.11 tools/bench_ops.py --baseline bench/bench_ops_baseline.json --tolerance 0.30

主なオプション: --ops a,b,c / --set core|cv|all / --sizes 512,2048,1080p|WxH / --dtypes float64,uint8,float32 / --images noisy,quantised,constant / --warm N(既定 1)/ --repeat N(既定 3、中央値)/ --out(既定 out/bench_ops.json)/ --baseline / --write-baseline / --tolerance(既定 0.30)/ --device cpu|cuda / --quiet

6.2 1 行に載るもの(速度だけを見ない)

ms(repeat の中央値。最小値だと外れ値で嘘の改善が出る)、mpx_stm_peak_x(tracemalloc ピーク ÷ 入力バイト)、rss_peak_x(0.4 ms ポーリング)、out_dtype / out_shapefallbacks(backend_safe.mark / events_since で数えた 1 呼び出しあたりの降格件数)、input_mutatedshares_mem(np.shares_memory)、module(実装モジュール)、twin / accel_key「速くなったが uint8 を返すようになった」「速くなったが毎回 fallback している」を同じ表で捕まえるのが狙い(§1.3 の「黙って別物」を速度改善で再生産しないため)。

6.3 入力は「ノイズ入り」と「量子化」の 2 本が既定

§5.1 のとおり median/percentile内容で 10 倍変わる。harness は同じシーンを

で作り、既定は noisy,quantised の 2 本。ノイズ画像を外した --images は CLI が拒否する(最悪側を隠すベンチは退行検出の役に立たない)。実測でも 512² median は noisy 113 ms / quantised 18.6 ms(6.1 倍)、clahe は 20.7 / 7.9 ms と分かれた。seed は固定(既定 7)なので run 間で入力は同一。

6.4 相対比較は同一 run の中でだけ

熱定常でないこの PC では絶対値が 1.5〜1.7 倍動く(§5.1)。そこで:

6.5 ベースラインのキーと判定

キーは "gaussian|2048|float64"。既定でない画像種のときだけ 4 番目の成分が付く("median|2048|float64|quantised")ので、画像種を増やしても既定キーは不変。比較は 4 つを別々に数える:

種別 意味
regressions ms がベースラインの (1 + tolerance) 倍超 → 表にして exit 1
improvements 逆側(1/(1+tolerance) 未満)
vanished ベースラインに在って今回測れなかった行(消えた op / 落ちた op。黙って無視すると「退行ゼロ」に化ける)
dtype_changed out_dtype が変わった行(速くなっても別物なら別物)

6.6 harness の限界(honest)

6.7 初回ベースライン run が拾ったもの(2026-09-03、--sizes 512,2048,1080p --dtypes float64)

420 行 / 384 測定 / 36 skipped(重い op の大サイズ)/ error 0・fallback 0・入力破壊 0、826 s。§1.1 の主表を独立に再現した(2048² gaussian 59.6 ms、median 1834 ms、rss 最大は shape_locate 22.3×)ほかに、この harness が新たに出したもの:

  1. cv_dist は float64 契約なのに float32 を返す(backends.py:270、declared out_sort=IMAGE)。速度ではなく 契約の嘘で、out_dtype 列を置いたから出た(§5.3 の一覧に 5 件目として追加すべき候補)。
  2. cv2 twin が core より遅い組がある: cv_otsu は 2048² で 0.76〜0.92×(= core の scipy 実装のほうが速い)、cv_gaussian は 512² で 1.45× しか出ない(小画像では _u8 往復と cv2 の起動費が支配)。(a′) の twin テーブルは 「速い op だけ載せる」を実測で決める必要がある — 全 cv_ を無条件に既定にすると遅くなる op が混ざる。
  3. 内容依存は median だけではない: 512² で clahe 20.7 → 7.9 ms、equalize 7.1 → 2.7 ms、gerode 4.0 → 1.6 ms も noisy/quantised で 2.5 倍前後動く。ヒストグラム系と rank 系はどちらも「同値の多さ」に効かれる。