← Aung Kaung Myat/Walkthroughsプロジェクト解説
CPU-only robot-learning track · block 2 · repoCPUのみのrobot learningトラック · block 2 · リポジトリ mujoco-clutter-detect

Tabletop Clutter Detector

Three kinds of objects jumbled on a simulated table, found and named by a 381 k-parameter anchor-free detector: mAP 0.911 against a fully fitted classical pipeline's 0.532. The scoring metric was written by hand first so the win could be attributed rather than assumed; the detector was then quantised to INT8 for a one-thread CPU.

シミュレーション上のテーブルに雑然と置かれた3種類の物体を、パラメータ数381 kのanchor-free検出器で検出・分類します:mAP 0.911(十分にfitさせた古典的パイプラインは0.532)。評価指標を先に自作して優位の要因を特定できる形にしたうえで、1スレッドのCPU向けにINT8へ量子化しました。

PyTorch (CPU)MuJoCo 3.12OpenCV ONNX RuntimeCOCO mAP, hand-written domain randomisationno CUDA anywhere
Classical pipeline古典的パイプライン
mAP 0.532
Learned detector学習ベースの検出器
mAP 0.911
Parametersパラメータ数
380 631
End-to-end latencyEnd-to-end latency
1.20 ms · ONNX, 8 thr
Datasetデータセット
28 000 images · 2.9 GB28 000枚 · 2.9 GB
Metric tests評価指標のテスト
15, derived on paper15件(手計算で導出)
00

At a glance要点

Every graph on one accuracy-versus-latency chart, the chosen one marked — before any detail.全グラフを精度対latencyの1枚のチャートに並べ、採用したものを示します。詳細はその後で。

mAP against latency per image — every graph, hard/val, 2000 imagesmAP対1枚あたりのlatency — 全グラフ、hard/val、2000枚
One-thread points are model time as the step 6 tables report it; the 8-thread point is end to end (1.01 ms model + 0.18 ms decode); classical is its whole pipeline.1スレッドの点はstep 6の表どおりモデル時間、8スレッドの点はend-to-end(モデル1.01 ms+decode 0.18 ms)、古典的手法はパイプライン全体の時間です。
0.50.60.70.80.91234latency per image, ms — log scale, left is faster1枚あたりのlatency、ms — 対数軸、左ほど速いmAP@[.5:.95]← faster than classical (2.13 ms)← 古典的手法(2.13 ms)より速いclassical pipeline (watershed + 9 features + logistic regression): mAP 0.532, 2.13 ms per image, single threadclassical pipeline古典的パイプライン0.532 · 2.13 mslearned detector, fp32, ONNX Runtime, 8 threads: mAP 0.911, 1.20 ms end to end (1.01 model + 0.18 decode)fp32 · 8 thrfp32 · 8スレッド0.911 · 1.20 mslearned detector, fp32, ONNX Runtime, 1 thread: mAP 0.9107, 3.69 ms model timefp32 · 1 thrfp32 · 1スレッド0.911 · 3.69 msstatic INT8 (MinMax), ONNX Runtime, 1 thread: mAP 0.9097 (−0.0010), 1.38 ms model time — the chosen graphstatic INT8 · 1 thrstatic INT8 · 1スレッド0.910 · 1.38 mspruned 25 % of channels + INT8, 1 thread: mAP 0.8899, 1.41 ms — rejectedpruned 25 % + INT825 % pruning + INT80.890 · 1.41 mspruned 50 % of channels + INT8, 1 thread: mAP 0.8411, 1.25 ms (−0.065 mAP) — rejectedpruned 50 % + INT850 % pruning + INT80.841 · 1.25 ms
learned detector — fp32 and static INT8 (large dot = chosen)学習ベースの検出器 — fp32とstatic INT8(大きい点=採用)classical pipeline古典的パイプラインpruned + INT8 — rejectedpruning + INT8 — 不採用
Numbers as a table数値を表で見る
GraphグラフthreadsスレッドmAPmswhat the ms ismsの内訳
classical pipeline古典的パイプライン10.5322.13whole pipelineパイプライン全体
fp32 · ONNX Runtime80.9111.20end to end (1.01 + 0.18 decode)end-to-end(1.01+decode 0.18)
fp32 · ONNX Runtime10.91073.69model timeモデル時間
static INT8 · MinMax10.90971.38model time · Δ mAP −0.0010モデル時間 · Δ mAP −0.0010
pruned 25 % + INT825 % pruning + INT810.88991.41model timeモデル時間
pruned 50 % + INT850 % pruning + INT810.84111.25model time · −0.065 mAPモデル時間 · mAP −0.065
  • 01Detector 0.911 mAP against classical 0.532. A 380 631-parameter anchor-free network beats a fully fitted watershed + nine-feature pipeline on 2000 hard/val images, and the per-class spread collapses from 0.304 to 0.006.検出器はmAP 0.911、古典的手法は0.532。パラメータ数380 631のanchor-freeネットワークが、十分にfitさせたwatershed+9特徴量のパイプラインをhard/val 2000枚で上回り、クラス間のばらつきは0.304から0.006に縮小。
  • 02Metric first, labels exact. COCO AP@[.5:.95] written by hand and proved against 15 paper-derived cases; 28 000 rendered images (2.9 GB) with labels read from MuJoCo's segmentation buffer, not drawn.評価指標を先に、ラベルは厳密に。COCO AP@[.5:.95]を自作し、手計算で導いた15ケースで証明。28 000枚(2.9 GB)のレンダリング画像のラベルは、手描きではなくMuJoCoのsegmentation bufferから読み出し。
  • 03Randomised appearance is what transfers. Eight-cell ablation, one seed: un-randomised training collapses to 0.147 under appearance shift, photometric augmentation recovers it only to 0.521, and on top of the randomiser it adds +0.0007.転移を生むのはランダム化した外観。8条件のablation(単一seed):ランダム化なしの学習は外観の変化で0.147に崩れ、photometric augmentationでも0.521までしか回復せず、ランダム化への上乗せ効果は+0.0007。
  • 041.20 ms end to end vs classical 2.13 ms through ONNX Runtime on 8 threads at identical mAP (0.9127 both, 500 images); the first measurement (1.225 ms, hot box) was discarded and kept in the repository. On one thread fp32 loses: 3.69 ms.end-to-endで1.20 ms、古典的手法は2.13 ms(ONNX Runtime、8スレッド、mAPは同一の0.9127・500枚)。最初の計測値(1.225 ms、過熱したマシン)は破棄し、リポジトリに残しています。1スレッドではfp32は3.69 msで負け。
  • 05Static INT8 wins at one thread; pruning rejected. MinMax INT8 cuts model time 3.57 → 1.38 ms (2.6×) at mAP 0.9097 vs 0.9107; pruning half the channels reaches 1.25 ms but costs 0.065 mAP (0.8454 before INT8). Both regimes are rendered — the easy→hard gap stands in for the real one.1スレッドではstatic INT8が勝ち、pruningは不採用。MinMaxのINT8でモデル時間は3.57 → 1.38 ms(2.6倍)、mAPは0.9107に対し0.9097。channelの半分をpruningすると1.25 msに達するもmAPを0.065失う(INT8前で0.8454)。どちらのregimeもレンダリング画像で、easy→hardの差は現実とのギャップの代替です。
01

In plain words概要

The task: find every object on a cluttered table, in images where objects hide each other.

タスク:物体同士が隠し合う画像の中で、雑然としたテーブル上のすべての物体を見つけること。

Object detection is a harder question than it sounds. It is not "is there a box in this picture" — it is "draw a rectangle around every object, say what each one is, and give me a confidence for each". The number of answers is not fixed, some objects hide behind others, and a detector can be wrong in four different ways at once: miss an object, invent one, put the rectangle in the wrong place, or put the right rectangle on the wrong label.

物体検出は、見た目以上に難しい問題です。「この画像に箱があるか」ではなく、「すべての物体を矩形で囲み、それぞれが何かを答え、 それぞれに信頼度を付ける」ことが求められます。答えの数は決まっておらず、他の物体の陰に隠れる物体もあります。 さらに検出器は、物体を見逃す、存在しない物体を作り出す、矩形の位置を誤る、正しい矩形に誤ったラベルを付ける、という4通りの誤りを同時に犯し得ます。

This project builds that from nothing, in a simulator, on a laptop with no graphics card. Three object kinds — box, cylinder, sphere — three to six per scene, in a 192×192 image from a camera tilted 39° above the table.

本プロジェクトでは、これをシミュレータ上で、GPUのないノートPCを使ってゼロから構築します。物体は box、cylinder、sphereの3種類で、1シーンに3〜6個。 テーブルから39°傾けたカメラで撮影した192×192の画像を使います。

Why the order of the work is the design作業の順番そのものが設計である理由

The tempting sequence is: train a detector, compute a score, write it down. That sequence cannot tell you whether the score is good, because there is nothing to compare it to and no proof the scorer is correct. So this project runs the reverse:

ありがちな手順は「検出器を学習し、スコアを計算し、記録する」です。しかしこの手順では、そのスコアが良いのかどうか判断できません。 比較対象がなく、評価器が正しいという証明もないからです。そこで本プロジェクトでは逆の順番で進めます。

  1. Build the dataset and check the labels are right before anything reads them.
  2. Build the metric and prove it against fifteen cases derived on paper — before there is anything to score.
  3. Build the strongest classical pipeline you can, including a fitted classifier, and record its number.
  4. Then train the network, and report the difference.
  1. データセットを作り、ラベルが正しいことを確認する(ラベルを使う処理を書く前に)。
  2. 評価指標を実装し、正しさを証明する。手計算で導いた15ケースで検証します(評価対象が存在する前に)。
  3. 可能な限り強い古典的パイプラインを作る。fitさせた分類器も含め、その数値を記録します。
  4. そのうえでネットワークを学習し、差分を報告する。
Why that order, specificallyこの順番にした具体的な理由

The previous project in this track found that half a network's apparent advantage was a single calibration constant its baseline had never been given. Building the strong baseline second inflates the model. So here the baseline gets everything a classical pipeline can fairly have — including a fitted classifier and its own tuning done on the training split — before the network is allowed to compete.

このトラックの前のプロジェクトでは、ネットワークの見かけ上の優位の半分が、baselineに与えられていなかった calibration定数1つによるものだと判明しました。強いbaselineを後から作ると、モデルを過大評価してしまいます。 そのため今回は、ネットワークと比較する前に、古典的パイプラインが公平に持ち得るものをすべてbaselineに与えています。 fitさせた分類器や、training splitで行ったbaseline自身のチューニングも含みます。

Terms used on this pageこのページの用語

occlusionOne object hiding part of another. The reason a clutter dataset is harder than a tidy one.ある物体が別の物体の一部を隠すこと。雑然としたデータセットが整然としたものより難しい理由です。
IoUIntersection over union — overlap of two boxes divided by their combined area. The standard test for whether a prediction matches a label.Intersection over union。2つの矩形の重なり面積を、両者を合わせた面積で割った値です。予測がラベルと一致するかを判定する標準的な基準です。
mAP@[.5:.95]Average precision averaged over ten IoU thresholds from 0.50 to 0.95. Strict: a loose box scores at the low thresholds only.0.50〜0.95の10個のIoU閾値でaverage precisionを求めて平均したもの。厳しい指標で、位置の甘い矩形は低い閾値でしか点になりません。
anchor-freePredicts object centres directly on a grid, instead of scoring thousands of pre-set candidate boxes.事前に用意した数千個の候補矩形を評価するのではなく、グリッド上で物体中心を直接予測する方式。
stride 4The output grid is one quarter of the input resolution — one cell per 4x4 pixel block.出力グリッドの解像度が入力の1/4であること。4x4ピクセルのブロックごとに1セルになります。
focal lossA loss that down-weights the easy, empty background cells so they cannot drown out the few cells holding objects.簡単で空の背景セルの重みを下げ、物体を含む少数のセルがそれらに埋もれないようにするloss。
NMSNon-maximum suppression — discarding duplicate boxes that fire on the same object.Non-maximum suppression。同じ物体に対して重複して出た矩形を取り除く処理。
domain randomisationVarying colours, lighting and textures while rendering, so the model cannot learn to depend on any of them.レンダリング時に色・照明・テクスチャを変化させ、モデルがそのどれにも依存できないようにする手法。
02

The six steps and what each one produced6つのステップと各成果物

The six build steps, and the concrete artefact each one produced.

構築の6ステップと、それぞれが生み出した具体的な成果物です。

step 1Scene & datasetシーンとデータセット Two cameras, exact labels read out of the renderer's segmentation buffer, occlusion measured by a pruned second pass. カメラ2台。ラベルはレンダラのsegmentation bufferから正確に読み出し、occlusionは枝刈りした2回目のパスで計測。 28 000 images · 2.9 GB · 108 s per 12 k28 000枚 · 2.9 GB · 12 kあたり108 s
step 2aThe AP metricAP指標 COCO-style AP@[.5:.95] written by hand, not imported. Fifteen paper-derived unit cases plus end-to-end controls. COCO形式のAP@[.5:.95]をライブラリを使わず自作。手計算で導いた15件のunit testとend-to-endの対照実験付き。 436 ms for 2000 images2000枚で436 ms
step 2bClassical baseline古典的baseline Background estimate → threshold → seed-counted watershed → nine shape features → multinomial logistic regression. 背景推定 → 二値化 → seed数を数えるwatershed → 9個の形状特徴量 → 多クラスlogistic regression。 mAP 0.532 · 2.02 ms
step 3Anchor-free detectorAnchor-free検出器 Centre heatmap + size + offset at stride 4, read by peak-picking. No anchors, no IoU suppression. stride 4での中心heatmap+サイズ+offsetを、ピーク抽出で読み取る。anchorもIoUによる抑制もなし。 mAP 0.911 · 381 k params
step 4Augmentation ablationAugmentationのablation Two regimes × four augmentations, one identical budget, every cell scored on both validation sets. 2つのregime × 4種のaugmentation。学習量は全条件で同一で、すべてを両方のvalidation setで評価。 +0.0007 mAP · 96 min
step 5ONNX & latencyONNXとlatency Export, verify end to end through the same metric, then time on an idle box — and throw away the first measurement. exportし、同じ指標でend-to-endに検証してから、アイドル状態のマシンで計測。最初の計測値は破棄。 1.20 ms vs classical 2.13 ms1.20 ms(古典的手法は2.13 ms)
step 6INT8 & pruningINT8とpruning Static INT8 with three calibration methods, structured pruning with the channels physically removed, one mAP-versus-latency curve at one thread. 3種類のcalibration手法によるstatic INT8、channelを物理的に削除するstructured pruning、1スレッドでのmAP対latency曲線。 INT8 1.38 ms · mAP 0.910
03

Every file, and what it is for全ファイルとその役割

Every source file, how big it is, and the single job it owns.

すべてのソースファイルについて、規模と担当する1つの役割を示します。

Coreコア

FileファイルLines行数What it does役割
common.py139 Scene constants, the class list, the yaw fold carried over from the previous project, the overhead projection, IoU between box sets, mask→box conversion, the HSV→RGB helper, the split loader, and the label column order. Also two thresholds that decide what counts as a target at all: MIN_VISIBLE_PX = 12 and MIN_VISIBLE_FRAC = 0.10.シーン定数、クラス一覧、前のプロジェクトから引き継いだyawの折り返し処理、真上からの投影、矩形集合間のIoU、 mask→矩形の変換、HSV→RGBのヘルパー、splitのローダー、ラベルの列順。さらに、そもそも何を検出対象とみなすかを決める2つの閾値 MIN_VISIBLE_PX = 12とMIN_VISIBLE_FRAC = 0.10も定義しています。
scene.xml39 The MuJoCo scene. A 1.5 m table plane, one light, two cameras, and six static object slots with no joints. Shape, size, colour and pose are all mjModel fields, so one XML covers every class and every appearance — unused slots are parked 10 m away, outside both fields of view.MuJoCoのシーン。1.5 mのテーブル平面、照明1つ、カメラ2台、jointを持たない静的な物体スロット6つで構成されます。 形状・サイズ・色・姿勢はすべてmjModelのフィールドなので、1つのXMLで全クラス・全外観を表現できます。 使わないスロットは10 m離れた、両カメラの視野外に退避させます。
gen_dataset.py239 Renders the four splits. Rejection-samples object positions at least 0.075 m apart, writes shape and colour into the model, renders RGB, renders again with segmentation enabled, converts each instance mask to a tight box, and computes visible_frac with a pruned second pass.4つのsplitをレンダリングします。物体位置を0.075 m以上離れるようrejection samplingし、形状と色をモデルに書き込み、 RGBをレンダリングし、segmentationを有効にして再度レンダリングします。各instance maskを外接矩形に変換し、 枝刈りした2回目のパスでvisible_fracを計算します。
view_dataset.py69 Draws the labels back onto sample images (out/labels.png) so a human can confirm the boxes land where they should before anything trains on them.サンプル画像にラベルを描き戻します(out/labels.png)。学習に使う前に、矩形が正しい位置にあることを人の目で確認するためです。

Metric評価指標

FileファイルLines行数What it does役割
ap.py212 The AP implementation. Ten IoU thresholds from 0.50 to 0.95, 101 recall levels, greedy matching that cannot cross image boundaries, ignore semantics, per-class averaging with absent classes excluded rather than scored zero. Also size-stratified AP and recall-by-visibility.APの実装本体。0.50〜0.95の10個のIoU閾値、101点のrecallレベル、画像の境界をまたがないgreedyマッチング、 ignoreの扱い、クラスごとの平均(存在しないクラスは0点ではなく除外)。サイズ別APと可視率別recallも実装しています。
test_ap.py125 Fifteen unit cases. Every expected value derived on paper — a test that records whatever the code printed last time proves only that the code is deterministic.15件のunit test。期待値はすべて手計算で導出しています。前回コードが出力した値をそのまま記録するテストでは、 コードが決定的であることしか証明できないからです。
sanity_ap.py116 End-to-end controls on real data: feed ground truth back as detections (must be exactly 1.0), shuffle the classes, randomise the boxes, keep a known fraction of hits, shift every box by a known pixel count.実データでのend-to-endの対照実験。ground truthを検出結果として入力する(ちょうど1.0になる必要あり)、 クラスをシャッフルする、矩形をランダムにする、正解の一定割合だけを残す、全矩形を既知のピクセル数だけずらす、の各ケースです。

Classical baseline古典的baseline

FileファイルLines行数What it does役割
baseline_cv.py383 Three segmenters (otsu, bgsub, bgsub+ws), a seed-counting watershed splitter, a nine-feature region descriptor, and a class-balanced multinomial logistic regression with a fourth background class so the pipeline can suppress its own false positives. Deliberately linear.3種類のセグメンテーション(otsu、bgsub、bgsub+ws)、seed数を数えるwatershedによる領域分割、 9個の特徴量による領域記述子、そしてクラス重みを均衡させた多クラスlogistic regression。4番目のクラスとして backgroundを持たせ、パイプライン自身が誤検出を抑制できるようにしています。あえて線形にしています。

Learned detector学習ベースの検出器

FileファイルLines行数What it does役割
detector.py227 The architecture and the target encoding. Gaussian radius, Gaussian splatting, encode, focal loss, masked L1 for the regression heads, the encoder–decoder network, and the decode function whose 3×3 max-pool is the entire non-maximum suppression.アーキテクチャとターゲットのencoding。Gaussian半径、Gaussian splatting、encode、focal loss、 regression head用のmasked L1、encoder–decoderネットワーク、そしてdecode関数です。decode内の3×3 max-poolが、 non-maximum suppressionのすべてを担います。
train_det.py273 The training loop. Holds 1500 images out of train as a tune split so validation is touched exactly once, at the end. Adam + OneCycle, 25 epochs, 34 minutes at 112–128 img/s on 8 threads. Contains the photometric and geometric augmentation implementations.学習ループ。trainから1500枚をtune用splitとして取り分け、validationには最後に一度だけ触れるようにしています。 Adam + OneCycle、25 epoch、8スレッドで112–128 img/s、所要34分。photometricおよびgeometric augmentationの実装も含みます。
test_detector.py106 Proves encode and decode are exact inverses before any training. This bug class does not crash — it trains to a low loss and puts the boxes in the wrong place. Also measures worst-case centre error at 2.00 px, which at stride 4 is exactly the quantisation the offset head exists to undo.学習前に、encodeとdecodeが厳密な逆変換であることを証明します。 この種のバグはクラッシュしません。lossは低く収束し、矩形だけが誤った位置に出ます。 中心誤差の最悪値も計測しており2.00 pxでした。これはstride 4での量子化誤差そのもので、offset headが打ち消すべき量です。
run_ablation.py81 Drives the 2×4 grid: two training regimes × four augmentation settings, one identical budget per cell, every model scored on both validation splits.2×4のグリッドを実行します。2つの学習regime × 4種のaugmentation設定で、各条件の学習量は同一、 全モデルを両方のvalidation splitで評価します。
export_onnx.py188 Exports at fixed batch 1, checks max absolute difference per head, re-runs the full detection pipeline through ONNX Runtime and scores it with the same ap.py, and times model, decode, and end-to-end at 1, 4 and 8 threads.batch 1固定でexportし、headごとの最大絶対誤差を確認します。さらに検出パイプライン全体をONNX Runtimeで再実行して 同じap.pyで評価し、モデル・decode・end-to-endの時間を1、4、8スレッドで計測します。
quantize.py213 Static INT8 through ONNX Runtime in QDQ form, U8S8, per-channel weights; MinMax, Percentile and Entropy calibration on 200 training images, plus a dynamic row for contrast. Scores every graph end to end with ap.py and times it at 1 and 4 threads with the load average recorded.ONNX Runtimeによるstatic INT8(QDQ形式、U8S8、per-channelの重み)。学習画像200枚でMinMax・Percentile・Entropyの calibrationを行い、比較用にdynamicの行も加えています。全グラフをap.pyでend-to-endに評価し、 load averageを記録したうえで1スレッドと4スレッドで計測します。
prune.py342 Ranks channels by BatchNorm γ, builds a physically narrower network, copies the surviving weights, slices every consumer's input channels to match, fine-tunes with step 3's losses, exports, and optionally quantises the result.BatchNormのγでchannelを順位付けし、物理的に幅の狭いネットワークを構築して、残ったweightをコピーします。 後段の入力channelもすべて対応するようスライスし、step 3と同じlossでfine-tuneしてexportします。結果をさらに量子化することもできます。
edge_curve.py102 Draws mAP against one-thread latency for every graph the two scripts wrote, with the classical pipeline as a dashed line.上記2つのスクリプトが出力した全グラフについて、1スレッドlatencyに対するmAPを描画します。古典的パイプラインは破線で示します。
test_edge.py148 INT8 round trip, pruning at ratio 0 reproducing the original to 1e-5, and the MAC count against a hand count. Needs no data and no weights.INT8の往復変換、ratio 0のpruningが元のモデルを1e-5の精度で再現すること、MAC数と手計算の一致を確認します。データもweightも不要です。

Inspection and reproduction確認と再現

FileファイルWhat it does役割
view_detections.py · view_cnn.py Qualitative figures. view_cnn.py pairs each image with the centre heatmap the boxes were read from — an average cannot show a failure mode, it can only tell you one exists.定性評価用の図。view_cnn.pyは各画像と、矩形の読み取り元である中心heatmapを並べて表示します。 平均値では失敗の様子は見えず、失敗が存在することしか分からないからです。
view_live.py · render_showcase.py Open the scene in MuJoCo's interactive viewer with both cameras in the dropdown, and render presentation stills with shadows and a textured floor. Shadows cost 6× throughput (~130 img/s against 800+), which is why the dataset renders plain.MuJoCoのインタラクティブviewerで、両カメラをドロップダウンから選べる状態でシーンを開きます。また、影とテクスチャ付きの床で 発表用の静止画をレンダリングします。影を付けるとthroughputが6分の1になる(約130 img/s対800以上)ため、データセットは影なしでレンダリングしています。
verify.sh Six tiers, cheapest first. Tiers 1–3 need no dataset and no weights and run in under a minute; the later tiers explain how to regenerate what they need rather than failing.軽い順に6段階。段階1–3はデータセットもweightも不要で、1分以内に終わります。後半の段階は、必要なものがない場合に 失敗するのではなく、その再生成方法を案内します。
after_chain.sh · run_rest.sh Job-chaining wrappers used to queue the long training runs on a shared machine.共有マシン上で長時間の学習を順番に実行するための、ジョブ連結用ラッパー。
requirements.txt Pinned CPU wheels — torch==2.14.0+cpu, mujoco==3.12.0, onnxruntime==1.23.2. The versions every number was measured on.バージョンを固定したCPU版wheel(torch==2.14.0+cpu、mujoco==3.12.0、 onnxruntime==1.23.2)。すべての数値はこのバージョンで計測しています。
runs/ · out/ · data/ Everything the README quotes is tracked: metrics JSON, per-epoch transcripts, both checkpoints, and detector.onnx. Only data/ (2.9 GB) is gitignored.READMEが引用するものはすべてgitで管理しています(指標のJSON、epochごとのログ、両方のcheckpoint、 detector.onnx)。gitignoreしているのはdata/(2.9 GB)だけです。
04

The scene, and where the labels come fromシーンとラベルの出どころ

How the MuJoCo scene is arranged, and why the labels are exact rather than drawn by hand.

MuJoCoシーンの構成と、ラベルが手描きではなく厳密である理由。

Why the camera was tiltedカメラを傾けた理由

The previous project used an overhead camera, which was the right choice there — a top-down image has an exact affine pixel-to-world map, ideal for regressing one pose precisely. But it makes detection degenerate. Objects resting on a flat table can never overlap in a top-down image, so there is no occlusion, no perspective, and no depth-dependent scale. A detector trained on that view would learn none of the three things that make detection hard.

前のプロジェクトでは真上からのカメラを使っており、そこではそれが正しい選択でした。真上からの画像はピクセルとワールド座標の間に 厳密なアフィン写像があり、1つの姿勢を精密にregressionするには理想的です。しかし検出タスクとしては退化してしまいます。 平らなテーブルに置かれた物体は真上から見ると決して重ならないため、occlusionも遠近感も、奥行きによるスケール変化もありません。 その視点で学習した検出器は、検出を難しくしている3つの要素をどれも学べません。

Overhead — no overlap is possible same size regardless of position · flat separable by blob-finding Tilted 39° — occlusion and perspective return far objects smaller · near ones hide far ones silhouettes change with yaw
The overhead camera is kept in the scene anyway — it is the only view with an exact pixel↔world map, so it verifies the labels and supplies the world coordinates later blocks need. Keeping both costs nothing and sets up a later experiment: train under one camera pose, evaluate under another.
それでも真上のカメラはシーンに残しています。ピクセル↔ワールドの厳密な写像を持つ唯一の視点なので、ラベルの検証や、 後のblockで必要になるワールド座標の取得に使えるからです。両方残すコストはゼロで、「あるカメラ姿勢で学習し、別の姿勢で評価する」 という将来の実験の準備にもなります。

Labels read out of the renderer, not annotatedアノテーションではなくレンダラから読み出すラベル

Each scene is rendered twice. The first pass produces the RGB image. The second runs with enable_segmentation_rendering(), which returns, per pixel, which geometry won it. A tight box around one instance mask is therefore already correct under perspective, already correct for the silhouette of a rotated box, and already correct about what is hidden behind what.

各シーンは2回レンダリングします。1回目でRGB画像を生成します。2回目は enable_segmentation_rendering()を有効にして実行し、ピクセルごとにどのgeometryが手前にあるかを取得します。 そのため、1つのinstance maskの外接矩形は、遠近法の下でも、回転した箱のシルエットに対しても、どの物体の陰に何が隠れているかについても、 最初から正確です。

This is the concrete reason robot-learning work starts in simulation. The label is not produced by a human — it is read out.

これが、robot learningの仕事がシミュレーションから始まる具体的な理由です。ラベルは人が作るのではなく、読み出すものです。

The occlusion label needs a third pass, and a pruning trickocclusionラベルには3回目のパスと枝刈りの工夫が必要

The segmentation buffer says which object won each pixel. It cannot say how many pixels an object would have had alone — which is exactly what "what fraction of this object is visible" needs. So each object is re-rendered with the others parked outside the frame, and the two counts are divided.

segmentation bufferから分かるのは、各ピクセルでどの物体が手前にあるかだけです。その物体が単独で置かれていたら何ピクセル 占めたはずかは分かりません。しかし「この物体の何割が見えているか」を求めるには、まさにその値が必要です。 そこで、他の物体をフレーム外に退避させた状態で各物体を再レンダリングし、2つのピクセル数の比を取ります。

That is N extra renders per scene and it dominates the cost. The optimisation: skip any object whose visible box intersects no other visible box — such an object is provably unoccluded, so the pruning is exact rather than approximate. It cut the second pass from 4.50 solo renders per image to 2.19, removing 51 % of those renders and 35 % of the total generation cost.

これは1シーンあたりN回の追加レンダリングとなり、コストの大半を占めます。最適化として、見えている矩形が他のどの見えている矩形とも 交差しない物体はスキップします。そのような物体は隠れていないことが証明できるため、この枝刈りは近似ではなく厳密です。 これにより2回目のパスは画像1枚あたり4.50回の単独レンダリングから2.19回に減り、そのレンダリングの51 %、生成コスト全体の35 %を削減しました。

Decisions frozen into the scene contractシーン仕様として固定した決定事項

only boxes get a yawyawを持つのはboxのみ
A sphere has no observable orientation, and a cylinder's rotation about its own axis is unobservable too. Labelling them would train the network to fit noise. yaw is NaN for both, and the loss masks it.球には観測できる向きがなく、円柱の自軸まわりの回転も観測できません。これらにラベルを付けると、ネットワークにノイズを fitさせることになります。両者のyawはNaNとし、lossではmaskします。
192 px, not 128128 pxではなく192 px
Objects are a median 28 px on a side. At 128 px they would be ≈19 px — below COCO's "small object" threshold, and the task would be measuring the renderer rather than the detector.物体の一辺は中央値で28 pxです。128 pxでは約19 pxとなりCOCOの「small object」の閾値を下回り、検出器ではなく レンダラを評価することになってしまいます。
table half-size 1.5 mテーブルの半幅1.5 m
Not 0.5 m. The tilted camera's top frame ray only meets the ground 1.15 m out; a smaller plane leaves a constant black band across the top of every image — dead pixels, and a trivial cue for the network to latch onto.0.5 mではありません。傾けたカメラのフレーム上端の光線は1.15 m先でようやく地面に当たります。平面が小さいと全画像の上部に 常に黒い帯が残り、無駄なピクセルになるうえ、ネットワークが頼ってしまう自明な手がかりになります。
no physics物理演算なし
Objects are placed, not simulated. No mj_step; poses are written directly and mj_forward recomputes the derived state.物体は配置するだけで、シミュレーションはしません。mj_stepは使わず、姿勢を直接書き込み、 mj_forwardで派生する状態を再計算します。
0.075 m minimum separation最小間隔0.075 m
Objects can touch but not interpenetrate. This has a consequence used later: no two object centres can land in the same stride-4 cell, which is what makes peak-picking exact.物体同士は接触できますが、めり込むことはありません。これは後で利用する性質につながります。2つの物体中心が stride 4の同じセルに入ることはなく、そのためピーク抽出が厳密になります。

What was generated生成したデータ

SplitImages画像数Objects物体数Obj/img物体/枚Occluded <0.9可視率 <0.9<0.5img/s
hard/train12 00053 9614.5010.0 %0.6 %111.1
hard/val2 0008 9924.5010.2 %0.6 %115.1
easy/train12 00053 8354.4910.1 %0.5 %106.4
easy/val2 0009 0494.529.8 %0.5 %103.6
A limitation stated before any detector existed検出器を作る前に明記した制約

Only 10 % of objects are occluded at all and 0.6 % past half. That is what a 39° elevation and a 0.075 m minimum separation produce. So mAP measured here is not stressed by occlusion, and any claim that the detector "handles occlusion" would be unsupported by this dataset. The honest fix is a lower camera and a regenerated set — not a softer sentence.

少しでも隠れている物体は全体の10 %だけで、半分以上隠れている物体は0.6 %です。仰角39°と最小間隔0.075 mの結果です。 したがって、ここで計測したmAPはocclusionによる負荷を受けていません。検出器が「occlusionに対応できる」という主張は、 このデータセットでは裏付けられません。正しい対処は、表現を和らげることではなく、カメラを低くしてデータセットを作り直すことです。

05

The scoring metric, written by hand自作の評価指標

COCO mAP implemented from scratch — ten IoU thresholds, 101 recall points, and the matching rule.

COCO mAPをゼロから実装。10個のIoU閾値、101点のrecall、そしてマッチングの規則。

What mAP@[.5:.95] actually ismAP@[.5:.95]の中身

Step by step, because the name hides the mechanism:

名前だけでは仕組みが見えないので、順を追って説明します。

  1. IoU — intersection over union — measures how well a predicted box overlaps a true one. 1.0 is perfect, 0 is no overlap.
  2. Pick an IoU threshold, say 0.5. Sort all detections by confidence. Walk down the list; each one is a true positive if it overlaps an unclaimed ground-truth box by at least the threshold, otherwise a false positive. Matching is greedy and cannot cross image boundaries.
  3. That walk traces a precision–recall curve. Average precision is the area under it, sampled at 101 fixed recall levels.
  4. Repeat at ten thresholds from 0.50 to 0.95 and average. Repeat per class and average again. That is mAP@[.5:.95].
  1. IoU(intersection over union)は、予測矩形が正解矩形とどれだけ重なるかを表します。1.0が完全一致、0は重なりなしです。
  2. IoU閾値を1つ決めます(例えば0.5)。全検出結果を信頼度順に並べ、上から順に見ていきます。まだ割り当てられていないground truthの矩形と 閾値以上重なればtrue positive、そうでなければfalse positiveです。マッチングはgreedyで、画像の境界はまたぎません。
  3. この走査でprecision–recall曲線が描けます。average precisionはその曲線下の面積で、固定された101点のrecallレベルでサンプリングします。
  4. これを0.50〜0.95の10個の閾値で繰り返して平均し、さらにクラスごとに繰り返して平均します。これがmAP@[.5:.95]です。

pycocotools is one import away and is deliberately not used. Everything interesting about detection evaluation lives inside those four steps; calling a library teaches none of it. The semantics follow pycocotools closely enough to be comparable, and the places they differ are marked in the source.

pycocotoolsはimport 1行で使えますが、あえて使っていません。検出の評価で重要なことはすべてこの4ステップの中にあり、 ライブラリを呼ぶだけでは何も身に付かないからです。挙動は比較可能な程度にpycocotoolsに合わせてあり、異なる箇所はソースコード中に明記しています。

Three unit cases that are counter-intuitive — and each one catches a real bug直感に反する3つのunit test(それぞれが実際のバグを検出する)

CaseケースAP@0.5Why理由
1 object, 1 correct hit, then 1 false positive物体1個、正しい検出1個、その後にfalse positive 1個1.000 The false positive arrives after recall reached 1.0, so no recall level was ever achievable at lower precision. It costs nothing.false positiveはrecallが1.0に達した後に来るため、低いprecisionでしか達成できないrecallレベルは存在しません。コストはゼロです。
the same false positive placed before the hit同じfalse positiveを正しい検出の前に置く0.500 Identical detections, identical recall, half the AP. Ranking is part of the score.検出結果もrecallも同じなのに、APは半分。順位付けもスコアの一部です。
1 of 2 objects found, no false positives2個中1個を検出、false positiveなし0.5050 Not 0.5 — 51 of the 101 recall levels are reachable, so it is 51/101.0.5ではありません。101点のrecallレベルのうち51点に到達できるため、51/101です。

Ignore semantics, and why they get their own testignoreの扱いと、それ専用のテストがある理由

Stratified AP — "how well does it do on small objects only" — depends entirely on this. Ground truth outside the stratum is marked ignore, not deleted. Deleting it turns every correct detection of an out-of-stratum object into a false positive. The test shows the same data scoring 1.000 with ignore and 0.500 with deletion.

層別AP(「小さい物体だけならどの程度か」など)は、これに完全に依存します。層の外にあるground truthは削除せず ignoreとして扱います。削除すると、層の外の物体に対する正しい検出がすべてfalse positiveになってしまうからです。 テストでは同じデータがignoreなら1.000、削除なら0.500になることを示しています。

End-to-end controls: known inputs have known outputsEnd-to-endの対照実験:既知の入力には既知の出力

Input入力mAPExpected期待値
ground truth fed back as detectionsground truthを検出結果として入力1.0000exactly 1ちょうど1
correct boxes, classes shuffled矩形は正しく、クラスをシャッフル0.1130≈ 1/9 — precision ⅓ × recall ⅓
random boxes, correct classes矩形はランダム、クラスは正しい0.00000
keep 75 % / 50 % / 25 % of hits正しい検出の75 % / 50 % / 25 %を残す0.7492 / 0.4983 / 0.2541≈ the fraction kept≈ 残した割合

Why the range of thresholds matters — a controlled degradation閾値の範囲が重要な理由:制御された劣化実験

Shifting every box by d pixels in x gives an IoU of exactly (w−d)/(w+d), so this test has a predicted answer, not just an observed one.

全矩形をx方向にdピクセルずらすと、IoUはちょうど(w−d)/(w+d)になります。そのためこのテストには、 観測値だけでなく事前に予測できる答えがあります。

Shiftずらし量Predicted IoU (w = 28)予測IoU(w = 28)mAPAP50
0 px1.0001.00001.0000
2 px0.8670.73091.0000
4 px0.7500.46391.0000
7 px0.6000.14150.6873
12 px0.4000.00570.0480
The argument for AP@[.5:.95] in one rowAP@[.5:.95]を使う理由を1行で

At a 4 px shift, AP50 is still a perfect 1.0000 while mAP has already lost more than half its value. AP50 alone cannot see localisation quality at all.

4 pxずらした時点で、AP50はまだ満点の1.0000ですが、mAPはすでに半分以上失われています。AP50だけでは位置精度をまったく捉えられません。

Two stratification decisions層別化に関する2つの判断

size bandsサイズ帯
Terciles of this dataset (cut at 25.5 and 31.9 px), not COCO's absolute 32/96 px, which were chosen for ~640 px images. At 192 px every object here is "small" and COCO's stratification would carry no information. The terciles show what a single number hides: a uniform 3 px shift costs mAP 0.517 on the smallest third against 0.701 on the largest, because IoU is scale-relative.COCOの絶対基準32/96 px(約640 pxの画像向けに決められたもの)ではなく、このデータセットの三分位(25.5 pxと31.9 pxで区切る)を使います。 192 pxの画像ではすべての物体が「small」になり、COCOの層別化では情報が得られないからです。三分位にすると、1つの数値に隠れていたものが見えます。 一律3 pxずらすと、最も小さい1/3ではmAPが0.517、最も大きい1/3では0.701になります。IoUがスケールに対して相対的だからです。
visibility bands report recall, not AP可視率帯ではAPではなくrecallを報告
A visibility band can be applied to ground truth but not to a detection — the detector never says how occluded it thought an object was. So unmatched detections cannot be attributed to a band, precision is undefined, and an "AP for heavily occluded objects" computed this way would be an artefact of where the other bands' false positives landed.可視率による区分はground truthには適用できますが、検出結果には適用できません。検出器は、物体がどの程度隠れていると判断したかを出力しないからです。 そのためマッチしなかった検出をどの帯にも割り当てられず、precisionが定義できません。この方法で「大きく隠れた物体のAP」を計算しても、 他の帯のfalse positiveがどこに落ちたかによる見かけ上の値にしかなりません。

Cost: 436 ms for 2000 images × 8992 objects × 3 classes × 10 thresholds. IoU is built once per image and reused across all ten thresholds — the only optimisation the metric needs.

計算コスト:2000枚 × 8992物体 × 3クラス × 10閾値で436 ms。IoUは画像ごとに1回だけ計算し、10個の閾値すべてで再利用します。 この指標に必要な最適化はこれだけです。

06

The classical baseline古典的baseline

The classical baseline: watershed segmentation, nine hand-designed shape features, and a linear classifier.

古典的baseline:watershedによるセグメンテーション、手設計の9個の形状特徴量、線形分類器。

The pipelineパイプライン

  1. Estimate the table, per image. Either a heavily downsampled median filter or a morphological opening — both produce "what this image would look like with no objects on it".
  2. Threshold the residual. What remains after subtracting that background is object.
  3. Split touching regions with a distance-transform watershed — but only where it is needed (see below).
  4. Describe each region with nine hand-designed shape features: log area, scale, aspect ratio, extent, circularity, solidity, normalised vertical position, top fill, elongation.
  5. Classify with a class-balanced multinomial logistic regression over those nine features, with a fourth background class so the pipeline can suppress its own false positives — a hand-built objectness. The confidence is the softmax probability of the chosen class.
  1. 画像ごとにテーブルを推定する。強く縮小したmedian filterか、morphological openingを使います。どちらも 「物体がない場合にこの画像がどう見えるか」を推定します。
  2. 残差を二値化する。背景を差し引いた残りが物体です。
  3. 接触している領域を分割する。距離変換によるwatershedを使いますが、必要な箇所だけに適用します(後述)。
  4. 各領域を記述する。手設計の9個の形状特徴量(面積の対数、スケール、アスペクト比、extent、円形度、solidity、 正規化した縦位置、上部の充填率、細長さ)を使います。
  5. 分類する。9個の特徴量に対し、クラス重みを均衡させた多クラスlogistic regressionを使います。4番目のクラスとして backgroundを持たせ、パイプライン自身が誤検出を抑制できるようにしています(手作りのobjectness)。 信頼度は選ばれたクラスのsoftmax確率です。
Why the classifier is linear on purpose分類器をあえて線形にした理由

A linear model on hand-designed features is what the classical convention is — a linear model on HOG, on SIFT, on shape moments. Making it an MLP would quietly turn the baseline into a small neural network and destroy the comparison it exists for.

手設計の特徴量に線形モデルを組み合わせることこそが、古典的手法の定石そのものです(HOG、SIFT、形状モーメントに線形モデル)。 MLPにすると、baselineがいつの間にか小さなニューラルネットワークになり、比較の意味がなくなってしまいます。

Two more rules that keep it honest: nothing is thresholded before AP, because AP integrates over every score cut-off and discarding low-confidence detections in advance only removes recall the metric would have credited; and all tuning was done on train and reported on val, because choosing the watershed's smoothing parameter on the split you then report is exactly how a baseline gets silently inflated.

公正さを保つためのルールがさらに2つあります。1つ目はAP計算の前にスコアで足切りしないことです。APはすべてのスコア閾値に わたって積分するため、信頼度の低い検出を事前に捨てても、指標が本来評価したはずのrecallを失うだけです。2つ目は チューニングはすべてtrainで行い、valで報告することです。watershedの平滑化パラメータを報告対象のsplitで選ぶことは、 まさにbaselineが気付かないうちに水増しされる典型的なパターンだからです。

Results on val, 2000 imagesvalでの結果(2000枚)

Method手法RegimemAPAP50AP75Agnostic mAPDet rate検出率ms/img
otsueasy0.44600.51780.44880.51380.54820.36
otsuhard0.14300.21820.14010.16990.23160.19
bgsubeasy0.50020.56390.49620.53070.55241.04
bgsubhard0.41140.49790.41280.47610.52511.12
bgsub + watershedeasy0.58950.76720.58530.66580.71641.88
bgsub + watershedhard0.53220.69790.56370.61080.68472.02
01

A repair applied to something that is not broken is damage壊れていないものへの修正は破壊になる

bgsub alone leaves a bimodal error distribution: 47.3 % of objects recovered at IoU ≥ 0.9, and 38.6 % below 0.5 because touching objects merge into one region. The obvious fix — distance-transform watershed — made things worse when applied to every region.

bgsub単体では誤差の分布が二峰性になります。47.3 %の物体はIoU ≥ 0.9で検出できる一方、38.6 %は 接触した物体が1つの領域に融合するためIoU 0.5未満になります。素直な対策である距離変換watershedを全領域に適用すると、結果は悪化しました。

Variant条件Regions/img領域数/枚Det rate検出率Agnostic mAPIoU ≥ 0.9IoU < 0.5
bgsub3.400.52110.374647.3 %38.6 %
watershed on every region全領域にwatershed4.290.49330.30046.3 %25.0 %
watershed on multi-seed regions onlyseedが複数ある領域のみにwatershed4.290.70670.552552.6 %14.1 %

Running the watershed everywhere redraws the boundary of regions that were already correct: the near-perfect tier collapsed from 47.3 % to 6.3 % while the merged tier only fell from 38.6 % to 25.0 %. Counting distance-transform seeds inside each connected region first, and leaving single-seed regions untouched, keeps the good tier and fixes the merges. A parameter sweep would never have found this — the sweep converged towards "split less", which was the wrong axis entirely.

watershedを全領域に適用すると、すでに正しかった領域の境界まで引き直されます。ほぼ完璧だった層は47.3 %から6.3 %に崩れ、 融合していた層は38.6 %から25.0 %にしか減りませんでした。先に連結領域ごとに距離変換のseedを数え、seedが1つの領域には手を付けないようにすると、 良い層を保ったまま融合も解消できます。パラメータsweepではこれは見つかりませんでした。sweepは「分割を減らす」方向に収束しましたが、 それはそもそも見当違いの軸だったからです。

02

A wrong prior still returns an answer誤った前提でも答えは返ってくる

Global Otsu thresholding on greyscale is a reasonable-looking method: objects and table differ in brightness, so split the histogram. Under easy it scores 0.446. Under hard, where table value is uniform(0.25, 0.85) and object value uniform(0.45, 1.0), the two distributions overlap and the split lands inside the objects — mAP 0.143, a 3.1× collapse, detection rate 0.232. It never raised an error. It returned a mask every time, and its per-class numbers read like a weak detector rather than a broken one.

グレースケールに対する大域的なOtsu二値化は、一見妥当な手法です。物体とテーブルは明るさが違うので、ヒストグラムを分ければよい、という発想です。 easyでは0.446を出します。しかしhardではテーブルの明度がuniform(0.25, 0.85)、物体の明度がuniform(0.45, 1.0)で 2つの分布が重なり、閾値が物体の内側に来てしまいます。mAPは0.143と3.1分の1に崩れ、検出率は0.232でした。 それでもエラーは一度も出ませんでした。毎回maskを返し、クラスごとの数値も「壊れた検出器」ではなく「弱い検出器」のように見えました。

03

Classification is the bottleneck, not localisationボトルネックは位置推定ではなく分類

On hard, class-agnostic AP50 is 0.823 against class-aware AP50 0.698. The boxes are in the right place; the label on them is wrong 15 % of the time. Per class: sphere 0.695, cylinder 0.510, box 0.391. A sphere's silhouette is a circle from every direction, so nine shape features describe it completely. A box's silhouette under a tilted camera changes with yaw, and at 28 px there is not enough of it left for a linear rule. That gap is the specific thing a learned feature extractor should close — and it is now measured rather than assumed.

hardでは、クラスを区別しないAP50が0.823、クラスを区別するAP50が0.698です。 矩形の位置は合っていますが、付いているラベルが15 %の割合で間違っています。クラス別ではsphere 0.695、cylinder 0.510、box 0.391です。 球のシルエットはどの方向から見ても円なので、9個の形状特徴量で完全に記述できます。一方、傾いたカメラから見た箱のシルエットはyawによって変わり、 28 pxでは線形のルールで扱えるほどの情報が残りません。この差こそ学習ベースの特徴抽出器が埋めるべきもので、今回それを想定ではなく計測で示しました。

04

Occlusion is where the classical pipeline stops, not degradesocclusionで古典的パイプラインは「劣化」ではなく「停止」する

Recall at IoU 0.5 by visible fraction: 0.785 above 0.9 visibility, 0.348 between 0.5 and 0.9, and 0.000 below 0.5. Not a slope — a cliff. A partially hidden object has the wrong silhouette, so every shape feature it produces is wrong at once.

可視率別のIoU 0.5でのrecallは、可視率0.9超で0.785、0.5〜0.9で0.348、0.5未満で0.000です。 緩やかな低下ではなく崖です。一部が隠れた物体はシルエットが正しくないため、そこから得られる形状特徴量がすべて同時に狂います。

07

The anchor-free detectorAnchor-free検出器

The anchor-free detector: heatmap, size and offset heads, focal loss, and max-pooling used as suppression.

Anchor-free検出器:heatmap・サイズ・offsetの各head、focal loss、抑制処理として使うmax-pooling。

Where the idea comes from発想の出どころ

The previous project's winning head was a spatial soft-argmax: reduce a feature map to one expected location. That is exactly why it cannot be used here — there are three to six objects, and one expectation cannot describe them.

前のプロジェクトで最も良かったheadは空間的なsoft-argmaxで、feature mapを1つの期待位置に集約するものでした。 まさにそれが、ここでは使えない理由です。物体は3〜6個あり、1つの期待値では表現できません。

The generalisation is to stop reducing the map at all. Keep it at stride 4 (48×48 for a 192 px input), predict a per-class centre heatmap on it, and read every local maximum instead of the mean. Size and offset ride along as two more heads on the same feature map. No anchor boxes, no region proposals, no IoU-based suppression anywhere.

一般化の方法は、mapを集約するのをやめることです。stride 4(192 pxの入力なら48×48)のまま保ち、その上でクラスごとの中心heatmapを予測し、 平均ではなくすべての極大値を読み取ります。サイズとoffsetは、同じfeature map上の2つの追加headとして予測します。 anchor boxもregion proposalも、IoUに基づく抑制処理も一切ありません。

Three heads on one feature map1つのfeature map上の3つのhead

HeadChannelsPredicts予測対象Loss
hm — heatmap3One map per class. A peak means "an object centre is here".クラスごとに1枚のmap。ピークは「ここに物体中心がある」ことを意味します。Focal loss on a Gaussian-splatted targetGaussian splattingしたターゲットに対するfocal loss
wh — sizeサイズ2Width and height in cells, read only at the peak.セル単位の幅と高さ。ピーク位置でのみ読み取ります。Masked L1
off — offset2The sub-cell remainder — where inside the 4×4 pixel cell the true centre actually was.セル未満の端数。4×4ピクセルのセル内のどこに真の中心があったかを表します。Masked L1

The network shape, and why there is a decoderネットワーク構造と、decoderがある理由

c1  3→16   stride 2   →  96×96
c2 16→32   stride 2   →  48×48
c3 32→64   stride 2   →  24×24
c4 64→128  stride 2   →  12×12          encoder, down to stride 16

y3 = s3( l3(c3) + upsample(p4(c4)) )     FPN-style decoder
y2 = s2( l2(c2) + upsample(y3)     )     back to 48×48, stride 4

hm  = 1×1 conv → 3 channels  (sigmoid)
wh  = 1×1 conv → 2 channels
off = 1×1 conv → 2 channels

The decoder exists because the heads need both halves. Stride 16 has the receptive field to say "object"; stride 4 has the resolution to say "here". Predicting at stride 16 would give a 28 px object a 1.75-cell footprint, and the offset head would have to absorb an 8 px quantisation — larger than the localisation error the classical baseline already achieves. Total: 380 631 parameters.

decoderがあるのは、headが両方の情報を必要とするからです。stride 16には「物体だ」と言える受容野があり、stride 4には 「ここだ」と言える解像度があります。stride 16で予測すると28 pxの物体は1.75セル分の大きさしかなく、offset headは8 pxの量子化誤差を 吸収しなければなりません。これは古典的baselineがすでに達成している位置誤差よりも大きい値です。パラメータ数は合計380 631です。

Three details that are load-bearing rather than decorative飾りではなく結果を左右する3つの細部

bias starts at −4.6biasの初期値は−4.6
That is logit(0.01). 99.8 % of the 2304 cells in a target map are zeros. Initialised at p = 0.5, the focal loss is dominated by pushing background down and the first epochs are spent walking the bias there — and training can diverge doing it.これはlogit(0.01)です。ターゲットmapの2304セルのうち99.8 %はゼロです。p = 0.5で初期化すると、focal lossは 背景を押し下げる項に支配され、最初の数epochはbiasをそこまで動かすことに費やされます。その過程で学習が発散することもあります。
3×3 max-pool is the NMS3×3 max-poolがNMS
A cell survives only if it is the maximum of its own 3×3 neighbourhood. That removes the ridge of near-peak cells the Gaussian target deliberately creates. It is not an approximation here: the 0.075 m minimum separation guarantees no two object centres land in the same cell, so peak-picking is exact and there is no IoU-based suppression in the pipeline at all.セルは、自身の3×3近傍で最大値である場合にのみ残ります。これにより、Gaussianターゲットが意図的に作るピーク付近の尾根状のセルが取り除かれます。 ここではこれは近似ではありません。最小間隔0.075 mにより2つの物体中心が同じセルに入らないことが保証されているので、ピーク抽出は 厳密であり、パイプラインにIoUに基づく抑制処理は一切ありません。
the larger quadratic root二次方程式の大きい方の解
The Gaussian radius comes from solving a quadratic for the overlap constraint. The CornerNet and CenterNet reference implementations take the larger root, which is not what the algebra calls for. The smaller root was implemented first, and measured: at stride 4 a 28 px object is 7 cells across, and the smaller root gives r = 0.57, which floors to 0 and collapses the soft target back to a single hot pixel — destroying the only thing it exists for. The larger root gives r = 1.91. The discrepancy is recorded in the source rather than quietly papered over.Gaussian半径は、重なりの制約に関する二次方程式を解いて求めます。CornerNetとCenterNetの参照実装は大きい方の解を採用していますが、 これは代数的に導かれる解ではありません。最初に小さい方の解で実装して計測しました。stride 4では28 pxの物体は7セル幅で、小さい方の解は r = 0.57となり、切り捨てで0になります。するとsoftなターゲットが1ピクセルだけのhotな点に戻ってしまい、存在意義そのものが失われます。 大きい方の解ではr = 1.91です。この食い違いは、黙って取り繕わずにソースコード中に記録しています。

Encode and decode were verified as exact inverses before any training, the same discipline the metric got. This bug class does not crash — it trains to a low loss and puts the boxes in the wrong place.

評価指標と同じ方針で、encodeとdecodeが厳密な逆変換であることを学習前に検証しました。この種のバグはクラッシュせず、 lossは低く収束したまま矩形を誤った位置に出します。

Results, hard/val, 2000 images結果(hard/val、2000枚)

Metric指標Classical古典的手法 bgsub+wsDetector検出器Change差分
mAP@[.5:.95]0.53220.9107+0.379
AP500.69790.9899+0.292
AP750.56370.9896+0.426
class-agnostic mAPクラス非区別mAP0.61080.9099+0.299
detection rate @ 0.5検出率 @ 0.50.68470.9433+0.259
latency (eager, in-process)latency(eager、プロセス内)2.02 ms3.87 ms1.9× slower1.9倍遅い

Training: 10 500 images with 1500 held out of train to watch for divergence; val was touched exactly once, at the very end. 25 epochs, Adam + OneCycle, 34 min at 112–128 img/s on 8 threads. The latency row was measured in-process immediately after that 34-minute run — step 5 re-measures it on an idle box and gets a different answer.

学習:10 500枚を使用し、発散を監視するためtrainから1500枚を取り分けました。 valには最後に一度だけ触れています。25 epoch、Adam + OneCycle、8スレッドで112–128 img/s、所要34分。 latencyの行は、その34分の学習直後に同じプロセス内で計測したものです。step 5でアイドル状態のマシンで再計測すると、異なる結果になりました。

01

The classification gap closed — which the baseline predicted分類の差は解消された(baselineから予測した通り)

Per-class AP: box 0.391 → 0.914, cylinder 0.510 → 0.910, sphere 0.695 → 0.908. The spread across classes collapses from 0.304 to 0.006. The learned extractor does not just score higher, it scores evenly — the class that was hardest for hand-designed features is now indistinguishable from the easiest. That was a stated prediction before training.

クラス別AP:box 0.391 → 0.914、cylinder 0.510 → 0.910、sphere 0.695 → 0.908。クラス間のばらつきは 0.304から0.006に縮まりました。学習した特徴抽出器はスコアが高いだけでなく、クラス間で均一です。 手設計の特徴量で最も難しかったクラスが、今では最も簡単なクラスと区別がつきません。これは学習前に明言していた予測です。

02

The occlusion cliff is gone — with a caveat that matters more than the numberocclusionの崖は消えた(ただし数値以上に重要な注意点あり)

Recall at IoU 0.5 by visible fraction: ≥0.9 → 0.785 becomes 1.000; 0.5–0.9 → 0.348 becomes 0.999; <0.5 → 0.000 becomes 0.910. A centre heatmap has no coupling to the silhouette — the centre of a half-occluded object is still a centre, and the size head regresses the full extent from partial evidence.

可視率別のIoU 0.5でのrecall:≥0.9では0.785が1.000に、0.5–0.9では0.348が0.999に、<0.5では0.000が0.910になりました。 中心heatmapはシルエットに依存しません。半分隠れた物体の中心もやはり中心であり、サイズheadは部分的な情報から全体の大きさをregressionします。

The caveat: n = 53. That is 0.6 % of the dataset, and 0.910 on 53 samples carries about ±0.04 at one sigma. The honest claim is narrow — the specific failure mode that stops the classical pipeline does not appear here. It is not evidence of robustness to heavy occlusion in general.

注意点:n = 53。これはデータセットの0.6 %で、53サンプルでの0.910には1σで約±0.04の幅があります。 正直に言える主張は狭く、古典的パイプラインを停止させる特定の失敗パターンがここでは現れないということだけです。 強いocclusion全般に対する頑健性の証拠ではありません。

03

Localisation is essentially exact, and AP75 is where it shows位置推定はほぼ厳密で、それはAP75に表れる

AP75 0.9896 against AP50 0.9899 — a gap of 0.0003. The baseline lost 0.134 between the same two thresholds. The metric's shift table gives the reading: a uniform 4 px error leaves AP50 at 1.000 while mAP has already fallen to 0.464, so AP50 alone cannot see localisation. The offset head is what buys this; it is measured at 2.00 px of worst-case centre error, which at stride 4 is exactly the quantisation it exists to undo.

AP75 0.9896に対しAP50 0.9899で、差は0.0003です。baselineは同じ2つの閾値の間で0.134を失っていました。 評価指標のずらし実験の表が読み方を示しています。一律4 pxの誤差ではAP50は1.000のままですが、mAPはすでに0.464まで下がっており、 AP50だけでは位置精度は見えません。これを実現しているのはoffset headです。中心誤差の最悪値は2.00 pxと計測されており、 これはstride 4での量子化誤差、つまりoffset headが打ち消すべき量そのものです。

04

The learned detector is slower, not faster学習ベースの検出器は速くない(遅い)

3.87 ms against 2.02 ms, single image, PyTorch eager, decode included. Worth stating plainly because the convenient story would be that the network wins on every axis, and it does not. Step 5 was set up to find out whether that was the architecture or the runtime.

画像1枚、PyTorch eager、decode込みで3.87 ms対2.02 msです。ネットワークがすべての面で勝つという都合の良い話にはならないので、 はっきり書いておきます。step 5は、この差がアーキテクチャによるものかruntimeによるものかを確かめるために設計しました。

05

Small objects still cost, in the expected direction小さい物体は依然として不利(予想通りの方向)

By size tercile: 0.805 (<25.5 px), 0.854 (25.5–31.9 px), 0.884 (>31.9 px). IoU is scale-relative, so a fixed pixel error costs a small box more. Output stride 4 means centres quantise to 4 px cells, which is 16 % of a 25 px object and 9 % of a 43 px one. The offset head removes most of that; the residual 0.079 spread is what is left.

サイズの三分位別:0.805(<25.5 px)、0.854(25.5–31.9 px)、0.884(>31.9 px)。IoUはスケールに対して相対的なので、 同じピクセル誤差でも小さい矩形ほど影響が大きくなります。出力stride 4では中心が4 pxのセルに量子化され、これは25 pxの物体の16 %、 43 pxの物体の9 %に当たります。offset headがその大部分を取り除き、残ったのが0.079のばらつきです。

08

Augmentation against domain randomisationAugmentation対domain randomisation

Photometric augmentation against render-time randomisation, and the asymmetry the comparison exposed.

photometric augmentationとレンダリング時のランダム化の比較、そしてそこで明らかになった非対称性。

The question, stated before the runs実験前に立てた問い

Photometric augmentation — randomly changing brightness, contrast, gamma, channel gain during training — exists because real datasets are captured under one set of lights and deployed under another. But the hard regime already randomises object hue, table shade, light position and light intensity at render time. Does augmentation add anything the randomiser does not already cover?

photometric augmentation(学習中に明るさ・コントラスト・ガンマ・チャンネルゲインをランダムに変える手法)が存在するのは、 実データセットがある照明条件で撮影され、別の照明条件で運用されるからです。しかしhard regimeでは、物体の色相・テーブルの濃淡・ 光源の位置と強度を、レンダリング時にすでにランダム化しています。augmentationは、ランダム化でカバーされていない何かを加えるのでしょうか。

Two regimes × four augmentation settings, one identical budget per cell: 6000 training images, 12 epochs, same seed, same schedule. Every model scored on both validation splits, so each row has an in-distribution number and a cross-regime number. The easy→hard column is the sim-to-real question in miniature — easy is a simulator nobody randomised, hard is the world it has to survive.

2つのregime × 4種のaugmentation設定で、各条件の学習量は同一です(学習画像6000枚、12 epoch、同じseed、同じスケジュール)。 全モデルを両方のvalidation splitで評価するため、各行に分布内の数値とregimeをまたいだ数値があります。 easy→hardの列はsim-to-realの問題の縮図です。easyは誰もランダム化していないシミュレータ、 hardはモデルが生き残らなければならない世界に相当します。

Trained on学習データAugmentationeasy valhard valTrain s学習時間 s
easynoneなし0.90130.1465595
easyphotometric0.89450.5214783
easygeometric0.90930.1808608
easyboth両方0.90600.5065737
hardnoneなし0.86830.8735589
hardphotometric0.86430.8742782
hardgeometric0.88440.8893648
hardboth両方0.88060.8857839
01

The gap is appearance, and it is a cliff, not a slope差の原因は外観で、緩やかな低下ではなく崖

A detector trained on fixed appearance scores 0.90 on what it saw and 0.15 when the table colour and the light move. Detection rate falls to 0.29 — it does not misclassify seven objects in ten, it fails to see them. Nothing about the geometry, the camera, the shapes or the sizes changed. This is the number the whole track is about: a policy trained in an un-randomised simulator meets the real world in exactly this way, and the failure is silent — no error, just boxes missing.

外観を固定して学習した検出器は、学習時と同じ条件では0.90ですが、テーブルの色と照明が変わると0.15になります。 検出率は0.29まで落ちます。10個中7個を誤分類しているのではなく、そもそも見えていないのです。形状配置・カメラ・物体の形・大きさは 何も変わっていません。これがこのトラック全体のテーマとなる数値です。ランダム化していないシミュレータで学習したpolicyは、 まさにこの形で現実世界に出会います。しかも失敗は静かで、エラーは出ず、矩形が欠けるだけです。

02

Photometric augmentation recovers half the gap and stopsphotometric augmentationは差の半分を取り戻し、そこで止まる

0.147 → 0.521, still 0.35 short of simply training on hard (0.874). The mechanism is visible in what each one varies. Augmentation perturbs the whole image with one brightness, one contrast, one gamma, one channel gain — a global nuisance model. The randomiser gives every object its own hue and moves the light source, which changes the shading direction on every face. Augmentation is a hand-written model of the nuisance; the randomiser samples the nuisance itself. A model cannot be augmented towards a variation nobody wrote down.

0.147 → 0.521となりましたが、単純にhardで学習した場合(0.874)にはまだ0.35届きません。仕組みは、それぞれが何を変化させるかを見ると分かります。 augmentationは画像全体に、1つの明るさ・1つのコントラスト・1つのガンマ・1つのチャンネルゲインを適用します。つまり大域的な外乱モデルです。 一方ランダム化は、物体ごとに別々の色相を与え、光源を動かすため、各面の陰影の向きまで変わります。augmentationは外乱を人が書いたモデルであり、 ランダム化は外乱そのものをサンプリングします。誰も書き下していない変動に向けてaugmentationすることはできません。

03

Geometric augmentation does nothing across the gapgeometric augmentationはこの差に対して効果なし

0.147 → 0.181, within the noise of a single seed. Flip and translation add placement diversity, and placement was never the problem — the two regimes share one camera and one placement distribution. An augmentation only buys invariance along the axis it perturbs. Worth stating because the default recipe applies everything at once and then cannot say which part worked.

0.147 → 0.181で、単一seedのノイズの範囲内です。反転と平行移動は配置の多様性を増やしますが、配置はそもそも問題ではありませんでした。 2つのregimeは同じカメラと同じ配置分布を共有しているからです。augmentationで得られる不変性は、それが摂動させる軸に沿ったものだけです。 よくあるレシピはすべてを同時に適用するため、どれが効いたのか説明できなくなります。だからこそ明記しておく価値があります。

04

On top of the randomiser, photometric augmentation buys nothingランダム化に上乗せしたphotometric augmentationは効果なし

hard/none 0.8735 against hard/photo 0.8742: +0.0007, for 33 % more training time. The randomiser already covers the axis photometric augmentation would add. Every hour spent on an augmentation pipeline for a randomised simulator is an hour spent on nothing.

hard/none 0.8735に対しhard/photo 0.8742で+0.0007、しかも学習時間は33 %増えます。 photometric augmentationが加えるはずの軸は、ランダム化がすでにカバーしています。ランダム化したシミュレータ向けにaugmentationパイプラインを 作る時間は、何も生まない時間です。

Geometric augmentation does add +0.016 within hard. Single seed, so suggestive rather than proven, but the direction is expected for a 6000-image dataset: flip and shift are extra placements, which the randomiser only samples 6000 times.

geometric augmentationは、hard内では+0.016の効果があります。単一seedなので証明ではなく示唆にとどまりますが、 6000枚のデータセットとしては予想通りの方向です。反転とずらしは追加の配置であり、ランダム化では配置は6000回しかサンプリングされないからです。

05

The randomised model transfers to the fixed regime for freeランダム化したモデルは固定regimeにそのまま転移する

hard/none loses 0.005 going to easy. easy/none loses 0.75 going the other way. That asymmetry is the entire argument for domain randomisation: the randomised distribution contains the fixed one, so a model trained on it has already seen the fixed regime as a special case. Nothing was tuned for easy and nothing needed to be.

hard/noneはeasyに移っても0.005しか下がりません。逆方向のeasy/noneは 0.75下がります。この非対称性こそが、domain randomisationを使う理由のすべてです。ランダム化した分布は固定された分布を 包含しているため、そこで学習したモデルは固定regimeを特殊ケースとしてすでに見ています。easy向けの調整は一切しておらず、 その必要もありませんでした。

06

More data does not close an appearance gapデータを増やしても外観の差は埋まらない

The full-budget easy/none model saw 75 % more images and 67 % more epochs and gained +0.027 on easy — and only +0.033 on hard, from 0.147 to 0.180. Coverage of the distribution is what transfers, not the number of samples drawn from the wrong one. When a model fails out of distribution, "collect more data" is only the right answer if the new data comes from somewhere new.

フル学習量のeasy/noneモデルは、画像を75 %、epochを67 %多く使い、easyでは+0.027向上しましたが、 hardでは0.147から0.180へ+0.033しか向上しませんでした。転移を決めるのは分布のカバー範囲であり、誤った分布から取ったサンプルの数ではありません。 モデルが分布外で失敗するとき、「データを増やす」が正解になるのは、新しいデータが新しい場所から来る場合だけです。

09

Export, latency, and a measurement that was thrown awayExport、latency、そして破棄した計測値

ONNX export, thread scaling, and a latency measurement that was thrown away and kept in the repository.

ONNX export、スレッド数によるスケーリング、そして破棄したうえでリポジトリに残したlatencyの計測値。

Verifying the export end to endexportをend-to-endで検証する

An export that is 3× faster and 2 % wrong is not an optimisation, and a max-absolute-difference on raw tensors is not a proof: a small logit drift can move a heatmap peak by one cell and change a box entirely. So the check runs full detections through the ONNX graph and scores them with the same ap.py.

3倍速くても2 %間違うexportは最適化とは言えません。また生のtensorでの最大絶対誤差は証明になりません。わずかなlogitのずれで heatmapのピークが1セル動き、矩形がまったく別物になり得るからです。そこで検出処理全体をONNXグラフで実行し、同じap.pyで評価します。

Check確認項目Result結果
max |torch − onnx| per headheadごとの max |torch − onnx|heatmap 2.4e−6 · size 1.1e−5 · offset 4.8e−6
mAP, first 500 hard/val images, PyTorchmAP、hard/valの最初の500枚、PyTorch0.9127
mAP, same 500 images, ONNX RuntimemAP、同じ500枚、ONNX Runtime0.9127
graphグラフ1.53 MB · 380 631 params · opset 17 · batch fixed at 11.53 MB · 380 631 params · opset 17 · batch 1固定

Batch is fixed at 1 deliberately: an edge camera delivers one frame at a time, and a graph exported for the shape it will actually see is the graph the runtime can plan for.

batchはあえて1に固定しています。エッジのカメラは1フレームずつ届けるものであり、実際に入力される形状に合わせてexportしたグラフこそ、 runtimeが最適な実行計画を立てられるグラフだからです。

Latency, batch 1, 200 images, idle boxLatency(batch 1、200枚、アイドル状態のマシン)

Decode — the 3×3 max-pool, the top-k and the gather — is 0.18 ms and does not depend on the runtime. It is added to every row, because a heatmap is not a detection.

decode(3×3 max-pool、top-k、gather)は0.18 msで、runtimeには依存しません。heatmapは検出結果そのものではないため、全行に加算しています。

RuntimeThreadsスレッド数Model msモデル msEnd to end msvs classical 2.13 ms古典的手法2.13 ms比
ONNX Runtime81.011.201.8× faster1.8倍速い
ONNX Runtime41.121.301.6× faster1.6倍速い
PyTorch eager42.542.720.78×
PyTorch eager82.752.930.73×
ONNX Runtime13.693.870.55×
PyTorch eager15.155.330.40×
01

The runtime is worth 2.7× at eight threads — and 1.4× at oneruntimeの効果は8スレッドで2.7倍、1スレッドでは1.4倍

The previous project found ONNX Runtime on one thread beating PyTorch eager on eight. That does not repeat here. The difference is the model: that network was 27 k parameters with a 16×16 output, so per-op dispatch overhead was most of the time and a runtime that removes overhead removes most of the time. This one is 14× larger with a 48×48×7 output; the convolutions themselves are the cost, and they cost the same arithmetic in either runtime. The refined rule: the runtime dominates while the model is small enough to be overhead-bound; once it is compute-bound, the thread budget matters as much as the engine. The earlier number was true and its generalisation was not.

前のプロジェクトでは、1スレッドのONNX Runtimeが8スレッドのPyTorch eagerに勝ちました。ここではそれが再現しません。 違いはモデルです。前のネットワークはパラメータ27 k、出力16×16で、処理時間の大半がop単位のdispatchオーバーヘッドでした。 そのためオーバーヘッドを取り除くruntimeは、処理時間の大半を取り除けました。今回のモデルは14倍大きく出力は48×48×7で、コストは畳み込みそのものにあり、 その演算量はどちらのruntimeでも変わりません。修正したルール:モデルがオーバーヘッド律速なほど小さいうちはruntimeが支配的だが、 計算律速になるとスレッド数の割り当てがエンジンと同じくらい重要になる。以前の数値は正しく、その一般化が誤っていたということです。

02

Four threads is the knee, and eight is worse for eager4スレッドが変曲点で、eagerは8スレッドの方が遅い

ONNX Runtime: 3.69 → 1.12 → 1.01 ms for 1 → 4 → 8 threads. 3.3× from the first four, 10 % from the next four. PyTorch eager on 8 threads (2.75) is slower than on 4 (2.54) — thread oversubscription on a batch-1 forward costs more than the parallelism returns. A node given 4 cores gets essentially everything this model can deliver; a node given 1 core is 1.8× slower than the classical pipeline it was meant to replace. The deployment question is not "how fast is the detector" but "how fast is the detector on the cores it will actually be given" — and that answer can land on either side of the baseline.

ONNX Runtime:1 → 4 → 8スレッドで3.69 → 1.12 → 1.01 ms。最初の4スレッドで3.3倍、次の4スレッドでは10 %です。 PyTorch eagerは8スレッド(2.75)の方が4スレッド(2.54)より遅くなります。batch 1のforwardではスレッドの過剰割り当てによるコストが、 並列化で得られる効果を上回るからです。4コアを割り当てられたnodeは、このモデルが出せる性能をほぼすべて引き出せます。1コアしか割り当てられないnodeでは、 置き換えるはずだった古典的パイプラインより1.8倍遅くなります。デプロイで問うべきは「検出器はどれだけ速いか」ではなく 「実際に割り当てられるコア数で検出器はどれだけ速いか」です。その答えはbaselineのどちら側にもなり得ます。

03

Decode is 15 % of the end-to-end latency at the best setting最良の設定ではdecodeがend-to-end latencyの15 %を占める

0.18 ms against 1.01 ms of model, and it does not shrink with the runtime. On a small, fast model the post-processing stops being free — any deployment budget that lists only the forward pass is under-reporting by that much.

モデル1.01 msに対してdecodeは0.18 msで、runtimeを変えても縮みません。小さく高速なモデルでは後処理はもはや無視できず、 forward passだけを計上したデプロイ時の時間見積もりは、その分だけ過小になります。

04

The first measurement was discarded, and why is the most reusable finding here最初の計測値は破棄した。その理由がここで最も応用の利く知見

Step 5 first ran unattended immediately after the 96-minute ablation and recorded ONNX-8t 1.225 ms and eager-8t 3.773 ms. Same weights, same code, same box, re-run idle the next morning: 1.014 and 2.750 — 17 % and 27 % faster. The single-threaded classical baseline moved only 5 % in the same comparison.

step 5は当初、96分のablationの直後に無人で実行され、ONNX-8tで1.225 ms、eager-8tで3.773 msを記録しました。 同じweight、同じコード、同じマシンで、翌朝アイドル状態で再実行すると1.014と2.750で、それぞれ17 %と27 %速くなりました。 同じ比較で、シングルスレッドの古典的baselineは5 %しか変わりませんでした。

WSL exposes no temperature sensor, so the cause cannot be read directly. But the box had just finished 1.5 hours at sustained 8-thread load, and the multithreaded numbers moved while the single-threaded one barely did. Three rules adopted for the rest of the track: a latency number is only reported with the conditions it was taken under; nothing timed directly after a training run is trusted; and both measurements stay in the repository, because a number that was silently replaced is worse than a number that was wrong.

WSLでは温度センサーが見えないため、原因を直接読み取ることはできません。しかしマシンは8スレッドの連続負荷を1.5時間続けた直後で、 マルチスレッドの数値は変動し、シングルスレッドの数値はほとんど変わりませんでした。そこでトラックの残りでは3つのルールを採用しました。 latencyの数値は計測条件とセットでのみ報告する。学習直後に計測した値は信用しない。そして両方の計測値をリポジトリに残す。 黙って差し替えられた数値は、間違っていた数値よりも悪いからです。

10

INT8 and pruning: the one-thread caseINT8とpruning:1スレッドの場合

Static INT8 through ONNX Runtime, structured channel pruning with the channels physically removed, and one curve that puts every graph on the same axes.

ONNX Runtimeによるstatic INT8、channelを物理的に削除するstructured pruning、そして全グラフを同じ軸に並べた1本の曲線。

Step 5 left a loose end. On eight threads the detector beats the classical pipeline; on one thread it runs at 3.69 ms against 2.13 ms, and one thread is the budget a ROS 2 node on a shared CPU is most likely to get. Step 6 asks whether that can be fixed without touching the architecture, using the two tools that actually shrink CPU latency: fewer bits per multiply, and fewer multiplies.

step 5には未解決の課題が残りました。8スレッドなら検出器は古典的パイプラインに勝ちますが、1スレッドでは3.69 ms対2.13 msで負けます。 そして共有CPU上のROS 2 nodeに割り当てられる可能性が最も高いのは1スレッドです。step 6では、CPU latencyを実際に縮める2つの手段 (乗算あたりのビット数を減らす、乗算の回数を減らす)を使い、アーキテクチャに手を加えずにこれを解決できるかを検証します。

Measured 2026-09-23 on the full hard/val split (2000 images), box idle, load average 0.28 at the start, thread count pinned inside the timing harness. The smoke run that preceded it (100 images, box busy) proved the pipeline executes and is not quoted anywhere.

2026-09-23にhard/val split全体(2000枚)で計測しました。マシンはアイドル状態、開始時のload averageは0.28、 スレッド数は計測ハーネス内で固定しています。事前に行ったsmoke run(100枚、マシン使用中)はパイプラインが動くことの確認であり、どこにも引用していません。

6a — static INT8 (quantize.py)6a — static INT8(quantize.py)

Static, not dynamic, because this is a convolutional network. Dynamic quantisation stores INT8 weights and works out each activation's scale at run time, so every convolution pays for a range pass and a quantise step before it runs. Static quantisation fixes the activation scales once, from a calibration pass, and the runtime executes the whole conv chain in INT8 with fused requantisation. Both are measured; the dynamic row is there to show the gap, and the gap came out smaller on one thread than expected before the run (finding 03 below).

畳み込みネットワークなので、dynamicではなくstaticを使います。dynamic quantisationはINT8のweightを保持し、各activationのscaleを 実行時に求めるため、すべての畳み込みが実行前にレンジ計算と量子化のコストを払います。static quantisationはcalibrationで activationのscaleを一度だけ確定し、runtimeは畳み込みの連鎖全体を、再量子化を融合した形でINT8のまま実行します。両方を計測しました。 dynamicの行は差を示すためのものですが、1スレッドでのその差は実験前の予想より小さくなりました(後述のfinding 03)。

The graph is written in QDQ form (plain QuantizeLinear / DequantizeLinear nodes that ONNX Runtime fuses into QLinearConv at session load), activations unsigned and weights signed per channel, which is the U8S8 pairing the x86 INT8 kernels want. Calibration images come from the training split, never from val: val is what the INT8 graph is scored on, and calibrating on it would make the drop look smaller than it is on new frames. Three calibration methods are compared on the same 200 training images, and every graph is scored end to end through the same ap.py as step 5.

グラフはQDQ形式で書き出します(素のQuantizeLinear / DequantizeLinear nodeで、ONNX Runtimeがsession読み込み時に QLinearConvへ融合します)。activationはunsigned、weightはper-channelのsignedで、x86のINT8カーネルが想定するU8S8の組み合わせです。 calibration画像はtraining splitから取り、valからは決して取りません。valはINT8グラフの評価に使うデータであり、そこでcalibrationすると 新しいフレームでの実際の低下より小さく見えてしまうからです。同じ学習画像200枚で3種類のcalibration手法を比較し、全グラフをstep 5と同じ ap.pyでend-to-endに評価しました。

GraphグラフmAPΔ mAPms, 1 threadms、1スレッドms, 4 threadsms、4スレッドMBrecall, vis < 0.5
fp32 ORT, same pre-processed graphfp32 ORT、同じ前処理済みグラフ0.9107—3.571.521.530.910
INT8 static, MinMax0.9097-0.00101.380.610.420.910
INT8 static, Percentile 99.990.9060-0.00481.380.620.420.910
INT8 static, Entropy0.9097-0.00101.350.600.420.910
INT8 dynamic, weights onlyINT8 dynamic、weightのみ0.9097-0.00101.821.540.410.898

Model time only; add step 5's 0.18 ms decode to every row for end to end. The last column is the recall on objects less than half visible, the small low-contrast detections quantisation noise takes first and the ones the classical pipeline never found.

モデル部分の時間のみです。end-to-endにはstep 5のdecode 0.18 msを各行に加えてください。最後の列は可視率が半分未満の物体に対するrecallです。 小さくコントラストの低いこれらの検出は、量子化ノイズで最初に失われやすく、古典的パイプラインが一度も見つけられなかったものです。

6b — structured channel pruning (prune.py)6b — structured channel pruning(prune.py)

Structured, not unstructured, because the target is latency. Zeroing half the individual weights leaves every convolution the same shape and the same cost; a dense kernel does not skip zeros. Removing whole output channels makes the next layer's input narrower too, and the multiply-accumulates go down in the shape of the network rather than on paper.

目的がlatencyなので、unstructuredではなくstructuredです。個々のweightの半分をゼロにしても、畳み込みの形状とコストは変わりません。 denseなカーネルはゼロをスキップしないからです。出力channelを丸ごと削除すれば次の層の入力も狭くなり、積和演算が紙の上ではなく ネットワークの形状として実際に減ります。

Channels are ranked by the BatchNorm scale γ of their conv+BN block: a channel whose γ is near zero is one the network has already learned to ignore. A global threshold across all blocks lets the layers the network uses least lose the most. Masks are not pruning. A masked model proves an accuracy claim and not a latency claim, so the surgery is explicit: a physically narrower network is built, the surviving weights are copied in, and every consumer of a pruned tensor has its input channels sliced to match. The FPN makes that non-trivial (one block feeds both the next stage and a lateral connection) and the wiring is spelt out in the file. With ratio 0 the pruned network reproduces the original to 1e-5 (test_edge.py), which is the check that catches a wrong channel index: that bug does not crash, it trains to a low loss with the wrong channels wired together.

channelはconv+BNブロックのBatchNorm scale γで順位付けします。γがゼロに近いchannelは、ネットワークがすでに無視するよう学習したものです。 全ブロック共通の大域閾値を使うことで、ネットワークが最も使っていない層ほど多く削られます。maskはpruningではありません。 maskしたモデルで証明できるのは精度の主張であってlatencyの主張ではないため、手術は明示的に行います。物理的に幅の狭いネットワークを構築し、 残ったweightをコピーし、削られたtensorを受け取るすべての後段の入力channelを対応するようスライスします。FPNがあるためこれは自明ではなく (1つのブロックが次の段と横方向の接続の両方に出力する)、配線はファイル中に明記しています。ratio 0ではpruning後のネットワークが元のモデルを 1e-5の精度で再現します(test_edge.py)。これはchannelのインデックス誤りを検出するためのチェックです。このバグはクラッシュせず、 間違ったchannel同士がつながったまま低いlossに収束します。

After pruning, a three-epoch fine-tune with the same dataset class and the same losses as step 3, then export through the same harness. --int8 quantises the pruned graph too.

pruningの後、step 3と同じデータセットクラスと同じlossで3 epochのfine-tuneを行い、同じハーネスでexportします。 --int8を付けるとpruning後のグラフも量子化します。

GraphグラフParamsMACsmAP before fine-tunefine-tune前mAPmAP afterfine-tune後mAPms, 1 threadms、1スレッドMB
full, step 3フルモデル、step 3380,631215.0 M0.9107—3.691.53
pruned 25 %, global25 % pruning、大域252,978155.6 M0.21990.89192.841.02
pruned 50 %, global50 % pruning、大域144,427101.8 M0.00000.84542.420.58
pruned 25 % + INT825 % pruning + INT8252,978155.6 M—0.88991.410.29
pruned 50 % + INT850 % pruning + INT8144,427101.8 M—0.84111.250.18

MACs and parameters are counted, convolutions only, and checked against a hand count in test_edge.py.

MAC数とパラメータ数は畳み込みのみを数えたもので、test_edge.pyで手計算と照合しています。

6c — the curve (edge_curve.py)6c — 曲線(edge_curve.py)

One figure: model latency at one thread on the x axis, mAP on the y axis, every graph above as a labelled point, the classical pipeline's 2.13 ms as a dashed line. One thread because that is the setting step 5 left unresolved and the only one that does not depend on how many cores the box was given.

1枚の図にまとめます。x軸が1スレッドでのモデルlatency、y軸がmAPで、上記の全グラフをラベル付きの点として、古典的パイプラインの2.13 msを 破線で示します。1スレッドにしたのは、step 5で未解決だった設定であり、マシンに何コア割り当てられたかに依存しない唯一の設定だからです。

mAP against one-thread latency for every graph in step 6
01

Static INT8 turns the one-thread loss into a win, and takes nothing measurable with itstatic INT8は1スレッドでの負けを勝ちに変え、計測可能な損失はない

fp32 through ONNX Runtime: 3.57 ms on one thread. Static INT8: 1.38 ms, a 2.6× cut, under the classical pipeline's 2.13 ms with room to spare. (The MinMax and Entropy rows are the same file timed twice, see finding 02, so read 1.38 and 1.35 as one number with its spread.) On four threads 1.52 ms became 0.60 ms. mAP moved from 0.9107 to 0.9097, a drop of 0.0010; every class moved by 0.001 or less; recall on objects less than half visible stayed at 0.910 on the same 53 objects. The number step 5 could not deliver on one core is delivered by changing the arithmetic, not the network.

ONNX Runtimeでのfp32は1スレッドで3.57 ms。static INT8では1.38 msで2.6分の1になり、古典的パイプラインの2.13 msを余裕を持って下回りました (MinMaxとEntropyの行は同じファイルを2回計測したものなので(finding 02参照)、1.38と1.35はばらつきを含む1つの数値として読んでください)。 4スレッドでは1.52 msが0.60 msになりました。mAPは0.9107から0.9097へ0.0010低下し、どのクラスも変化は0.001以下、 半分未満しか見えない物体へのrecallも同じ53物体で0.910のままでした。step 5で1コアでは達成できなかった数値を、ネットワークではなく演算方式を変えることで達成しました。

02

Two of three calibration methods produce the same graph, and the elaborate one loses3つのcalibration手法のうち2つは同じグラフを生成し、凝った手法は負ける

MinMax and Entropy do not merely give the same mAP to four decimals: the two output files have the same SHA-256. The entropy histograms were built, and took four times as long, but the KL search kept the full range on all 21 quantized activations, which is what bounded activations without heavy tails give you. Percentile 99.99 costs 0.0048, five times the other two, and almost all of it on the box class (0.914 to 0.904). With 200 calibration images the percentile clip chose a range too tight for at least one tensor the box detections depend on. The default would have been fine here; the comparison is what makes that a measurement rather than a guess. Timing repeatability, measured on the same file in three fresh processes later that night: 1.36, 1.44 and 1.80 ms against fp32's 3.72, 3.96 and 4.51 ms, a ratio of 2.5 to 2.75×. Each table row is one sample from one quiet window, so the ratio is the result and gaps under 10 % between rows are not.

MinMaxとEntropyは、mAPが小数点以下4桁まで一致するだけではありません。2つの出力ファイルのSHA-256が同一です。entropyのヒストグラムは 実際に構築され、4倍の時間がかかりましたが、KL探索は量子化した21個のactivationすべてでレンジ全体を残しました。裾の重くない有界なactivationでは そうなります。Percentile 99.99は0.0048の低下で他の2つの5倍、そのほぼすべてがboxクラスです(0.914から0.904)。calibration画像200枚では、 percentileのclipが、box検出が依存する少なくとも1つのtensorに対して狭すぎるレンジを選びました。ここではデフォルトで問題ありませんでしたが、 比較したからこそ、それが推測ではなく計測結果になります。計測の再現性は、同じ夜のうちに同じファイルを新しいプロセスで3回計測して確認しました。 1.36、1.44、1.80 msに対しfp32は3.72、3.96、4.51 msで、比は2.5〜2.75倍です。表の各行は静かな時間帯の1サンプルなので、 結果として言えるのはこの比であり、行間の10 %未満の差は結果とは言えません。

03

Dynamic quantisation was expected to buy no speed; on one thread it bought halfdynamic quantisationは速度向上なしと予想したが、1スレッドでは時間が半分になった

3.57 ms to 1.82 ms on one thread, a 2.0× cut, but 1.54 ms on four threads against fp32's 1.52: nothing. The integer convolution kernels the dynamic path runs did the single-thread work faster and did not scale with threads, while the static QDQ graph did both. The paragraph written before the run said dynamic quantisation "saves bytes and buys no speed"; that expectation was wrong on one thread and the repository keeps both the expectation and the measurement, because a corrected prediction is part of the record.

1スレッドでは3.57 msから1.82 msへ2.0分の1になりましたが、4スレッドでは1.54 msでfp32の1.52 msと変わりませんでした。 dynamic経路が使う整数畳み込みカーネルは、シングルスレッドの処理は速くなったもののスレッド数に応じてスケールせず、static QDQグラフは両方を達成しました。 実験前に書いた段落では、dynamic quantisationは「容量は減るが速度は上がらない」としていました。この予想は1スレッドでは誤りでした。 修正された予測も記録の一部なので、リポジトリには予想と計測結果の両方を残しています。

04

Pruning is the wrong tool for this networkこのネットワークにpruningは不適切

Removing 25 % of the channels took 28 % of the multiply-accumulates and 20 % of the one-thread latency (3.57 to 2.84 ms), and cost 0.019 mAP after three fine-tune epochs. Removing 50 % took 53 % of the MACs and 32 % of the latency (2.42 ms) and cost 0.065. Latency falls slower than the arithmetic because narrow convolutions run the kernels less efficiently and the per-operator overhead does not shrink at all. Before fine-tuning the two pruned networks scored 0.220 and 0.000: the γ ranking was never trained toward sparsity, so what it removed was in use. Pruned 50 % plus INT8 reaches 1.25 ms, under 10 % faster than static INT8 alone, for seven points of mAP. The decision is static INT8 with no pruning; a smaller network is a training decision, train it narrow, not surgery on a trained one.

channelを25 %削除すると、積和演算は28 %、1スレッドlatencyは20 %減り(3.57から2.84 ms)、3 epochのfine-tune後でmAPは0.019低下しました。 50 %削除すると、MACsは53 %、latencyは32 %減り(2.42 ms)、mAPは0.065低下しました。latencyが演算量ほど減らないのは、幅の狭い畳み込みでは カーネルの効率が下がり、operatorごとのオーバーヘッドはまったく減らないからです。fine-tune前の2つのpruning済みネットワークは0.220と0.000でした。 γによる順位付けはsparsityに向けて学習されていなかったため、削除したchannelは実際に使われていたのです。50 % pruning+INT8は1.25 msに達しますが、 static INT8単体より10 %未満しか速くならず、その代わりにmAPを7ポイント失います。結論は、pruningなしのstatic INT8です。 小さいネットワークが欲しいなら、それは学習時の判断であり、学習済みモデルへの手術ではなく、最初から幅を狭くして学習すべきです。

A one-thread x86 latency under ONNX Runtime is not a Jetson latency. The deployment target is an ARM core plus TensorRT, a different INT8 path on different hardware; the ordering of the graphs is expected to carry, the ratios are not. Post-training quantisation only: if the INT8 drop had come out above about 0.01, quantisation-aware training would be the next step, not a different calibration method.

ONNX Runtimeでのx86 1スレッドのlatencyは、Jetsonのlatencyではありません。デプロイ先はARMコア+TensorRTで、異なるハードウェア上の異なるINT8経路です。 グラフ間の順位はそのまま当てはまると予想されますが、比率は当てはまりません。今回はpost-training quantisationのみです。 INT8での低下がおよそ0.01を超えていたら、次の手はcalibration手法の変更ではなくquantisation-aware trainingでした。

11

What this does not proveこの結果から言えないこと

The limits of a synthetic dataset with perfect labels, stated plainly.

完全なラベルを持つ合成データセットの限界を、率直に記します。

  • Occlusion robustness. Only 0.6 % of objects are occluded past half. The 0.910 recall in that band rests on 53 samples. Testing it properly needs a lower camera and a smaller separation, and a regenerated dataset.
  • Sim-to-real. Both regimes are rendered. The easy→hard gap is a measured stand-in for the real gap, not the real gap.
  • Single seed on the ablation. The eight-cell grid was run once. The large effects (0.147 vs 0.874) are far outside any plausible seed noise; the small ones (+0.016 for geometric augmentation) are labelled suggestive rather than proven.
  • Three classes, one camera pose, one placement distribution. Nothing here tests generalisation to new object categories or new viewpoints.
  • The classical baseline is one person's best attempt. A stronger classical pipeline exists in principle; the comparison is honest about being between these two implementations.
  • occlusionへの頑健性。半分以上隠れている物体は0.6 %だけです。その帯でのrecall 0.910は53サンプルに基づいています。 きちんと検証するには、カメラを低くし、物体間隔を狭めて、データセットを作り直す必要があります。
  • Sim-to-real。どちらのregimeもレンダリング画像です。easy→hardの差は、現実とのギャップを計測可能な形で代替したものであり、 現実のギャップそのものではありません。
  • ablationは単一seed。8条件のグリッドは1回しか実行していません。大きな効果(0.147対0.874)はseedによるノイズの範囲を大きく超えていますが、 小さな効果(geometric augmentationの+0.016)は証明ではなく示唆として扱っています。
  • 3クラス、カメラ姿勢1つ、配置分布1つ。新しい物体カテゴリや新しい視点への汎化は、ここでは一切検証していません。
  • 古典的baselineは1人の人間による最善の試み。原理的にはより強い古典的パイプラインが存在し得ます。この比較は、あくまでこの2つの実装間の比較です。
12

Running it実行方法

Setup, the commands in order, and the runtimes to expect on a CPU.

セットアップ、実行するコマンドの順序、CPUでの想定所要時間。

git clone https://github.com/AungKaung1928/mujoco-clutter-detect.git
cd mujoco-clutter-detect
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt

python gen_dataset.py --regime hard --n 200 --smoke     # smallest useful run
python gen_dataset.py --regime hard --n 12000
python gen_dataset.py --regime easy --n 12000
python view_dataset.py --regime hard --n 12             # -> out/labels.png

nice -n 10 python train_det.py --regime hard --epochs 25 \
    --save runs/det_hard_none.json --ckpt runs/det_hard_none.pt      # ~35 min
nice -n 10 python run_ablation.py --epochs 12 --fit-n 6000 \
    --out runs/ablation.json                                         # ~95 min
nice -n 10 python export_onnx.py --ckpt runs/det_hard_none.pt \
    --regime hard --save runs/onnx.json                              # idle box only
nice -n 10 python quantize.py --calib 200                            # step 6a, idle box only
nice -n 10 python prune.py --ratios 0.25 0.5 --finetune-epochs 3 --int8   # step 6b
python edge_curve.py --out out/edge_curve.png                        # step 6c, seconds

Or check the claims without regenerating anythingまたは、何も再生成せずに主張を検証する

./verify.sh

Six tiers. The first three need no dataset and no weights — the fifteen hand-computed AP cases, the encode/decode inverse check, and the step-6 checks (INT8 round trip, pruning rewiring exactness, MAC count). They take under a minute. The later tiers need data/ (2.9 GB, gitignored) and runs/*.pt, and say how to regenerate them rather than failing.

6段階あります。最初の3段階はデータセットもweightも不要で、手計算した15件のAPケース、encode/decodeの逆変換チェック、 step 6のチェック(INT8の往復変換、pruningの再配線の厳密性、MAC数)を実行します。所要時間は1分未満です。後半の段階は data/(2.9 GB、gitignore済み)とruns/*.ptを必要とし、ない場合は失敗せずに再生成方法を案内します。

The script pins MUJOCO_GL=glfw and the thread count, because both change the numbers.

スクリプトはMUJOCO_GL=glfwとスレッド数を固定しています。どちらも数値を変えてしまうからです。

13

The short versionまとめ

The whole project compressed into one paragraph.

プロジェクト全体を1段落に凝縮します。

Multi-object detection on a simulated tabletop, built metric-first. Labels come from MuJoCo's segmentation buffer, so they are exact under perspective and occlusion; the occlusion fraction needs a second pass, which an exact pruning rule cuts by half. A COCO-style AP@[.5:.95] harness is written by hand and proved against fifteen paper-derived cases plus end-to-end controls. A fully fitted classical pipeline — background estimate, seed-counted watershed, nine shape features, class-balanced logistic regression with a background class — scores mAP 0.532, and the four findings under it name exactly where hand-designed features stop. A 381 k-parameter anchor-free heatmap detector then scores 0.911, with the per-class spread collapsing from 0.304 to 0.006 and the classical occlusion cliff absent. An eight-cell ablation shows training on randomised appearance is what transfers: un-randomised collapses to 0.15 under appearance shift, photometric augmentation recovers it only to 0.52, and on top of the randomiser it adds 0.0007. Through ONNX Runtime the detector runs end to end in 1.20 ms against the classical 2.13 ms at identical mAP — and the first latency measurement was discarded for having been taken on a hot box, with both numbers kept in the repository. Static INT8 with MinMax calibration then brings the one-thread model time from 3.57 ms to 1.38 ms (2.6×), under the classical 2.13 ms, at mAP 0.9097 against fp32's 0.9107; pruning half the channels reaches 0.8454 after a three-epoch fine-tune.

シミュレーション上のテーブルを対象に、評価指標を先に作る方針で複数物体検出を構築しました。ラベルはMuJoCoのsegmentation bufferから取得するため、 遠近法やocclusionの下でも厳密です。occlusionの割合には2回目のパスが必要ですが、厳密な枝刈りルールでその量を半分にしました。 COCO形式のAP@[.5:.95]評価ハーネスを自作し、手計算で導いた15ケースとend-to-endの対照実験で正しさを証明しました。十分にfitさせた古典的パイプライン (背景推定、seed数を数えるwatershed、9個の形状特徴量、backgroundクラス付きのクラス均衡logistic regression)はmAP 0.532で、その下の4つのfindingが、 手設計の特徴量の限界がどこにあるかを具体的に示しています。続いてパラメータ数381 kのanchor-free heatmap検出器は0.911を出し、クラス間のばらつきは 0.304から0.006に縮まり、古典的手法で見られたocclusionの崖も現れませんでした。8条件のablationにより、転移を生むのはランダム化した外観での学習であることが分かりました。 ランダム化しないモデルは外観の変化で0.15に崩れ、photometric augmentationでも0.52までしか回復せず、ランダム化に上乗せしても効果は0.0007です。 ONNX Runtimeを使うと、検出器はend-to-endで1.20 ms(古典的手法は2.13 ms)、mAPは同等です。最初のlatency計測値は過熱したマシンで取ったため破棄し、 両方の数値をリポジトリに残しています。さらにMinMax calibrationによるstatic INT8で、1スレッドのモデル時間は3.57 msから1.38 ms(2.6倍)となり、 古典的手法の2.13 msを下回りました。mAPは0.9097(fp32は0.9107)です。channelの半分をpruningすると、3 epochのfine-tune後で0.8454になります。