Metadata-Version: 2.4
Name: lucid-yolo
Version: 0.5.0.dev0
Summary: Independent, from-scratch PyTorch Lightning implementation of the real-time detection, instance-segmentation, and oriented-detection methods described in the YOLO26 paper (arXiv:2606.03748)
Author: Jirka Borovec
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/Borda/lucid-YOLO
Keywords: object-detection,instance-segmentation,oriented-detection,pytorch-lightning,reproduction
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: torch>=2.4
Requires-Dist: pytorch-lightning[extra]>=2.4
Requires-Dist: jsonargparse[signatures]>=4.28
Requires-Dist: torchvision>=0.19
Requires-Dist: torchmetrics>=1.9
Requires-Dist: faster-coco-eval>=1.7
Requires-Dist: fvcore>=0.1.5
Requires-Dist: pyyaml>=6.0
Requires-Dist: tqdm>=4.66
Requires-Dist: numpy>=1.26
Dynamic: license-file

# 🔦 lucid-yolo

> lucid-yolo is an independent, from-scratch PyTorch Lightning implementation of the real-time detection, instance segmentation, and oriented detection methods described in the Ultralytics YOLO26 paper (arXiv:2606.03748). "YOLO" refers to the family of real-time detectors originated by Redmon et al. (2016). This project is not affiliated with, endorsed by, or derived from Ultralytics or its codebase. No Ultralytics source code, configurations, or model weights were consulted or used. See docs/PROVENANCE.md.

**A modern YOLO, written out in full, with its homework shown.** Every mechanism in the paper — the NMS-free end-to-end head, the removal of distribution focal loss, the MuSGD optimizer, the progressive loss schedule, the rotated-box formulation — is implemented here from the published equations, trained, measured, and written up with the numbers it actually produced. Where the papers leave a choice open, the choice is recorded as a numbered assumption instead of buried in the code.

It is a teaching artifact, a research baseline, and a reproduction record at the same time. It is **not** a production detector: every trained tier is the smallest scale for 50 epochs, and no weights ship.

## 🧭 Why this exists

A paper describes a method; a repository ships an implementation; and the gap between the two is where reproduction usually fails. Published detectors are read by thousands of people through a single codebase, so "the method" and "that codebase's choices" become impossible to tell apart — and the parts the paper never specified are exactly the parts nobody can see.

This project rebuilds the methods from the papers alone, deliberately without reading any existing implementation of them, and writes down every point where the papers ran out of instructions. What comes out is three things at once:

- **A test of the claims.** If the NMS-free head really costs 0.6–0.8 AP, an independent implementation should measure something close to that. [It measures 1.11.](#detection--coco-2017-val-5000-images)
- **A map of the underdetermined.** 63 numbered assumptions — the mask crop frame, the angle convention, the head's initialization bias — each one a place where two faithful implementations could legitimately diverge. See [`docs/ASSUMPTIONS.md`](docs/ASSUMPTIONS.md).
- **A codebase you can read.** No config DSL between you and the architecture, one mechanism per module, and every module citing the equation it implements.

**Non-goals**, stated so nothing here is mistaken for them: matching the paper's headline accuracy (that needs a training budget this project does not have), shipping weights for production use, or being faster than the reference implementation. What is claimed is fidelity of *method*, with the evidence attached.

## 👥 Who this is for

| You are | Start here | What you get |
| -- | -- | -- |
| **A student entering computer vision** | [How a modern YOLO works](#how-a-modern-yolo-works) | One readable path from image to detections, with each block, loss and decode step in its own typed module — no config DSL to decode first |
| **An applied ML engineer** | [Quickstart](#quickstart) | Four commands: train, evaluate, predict, draw. Lightning underneath, so your callbacks, loggers and accelerators work as they already do |
| **A researcher reproducing results** | [What was reproduced](#what-was-reproduced) | Exact recipes, the run behind every number, the criteria each tier had to clear, and the deviations from the paper stated as deviations |
| **A researcher building on top** | [Customizing it](#customizing-it) | Five scales over one topology, losses and assigners as separate modules, and a frozen-golden gate that tells you when a change moved something it should not have |

<a id="how-a-modern-yolo-works"></a>

## 🧠 How a modern YOLO works

One image, one forward pass, and **two** ways of reading the same output:

```mermaid
flowchart LR
    IMG[image<br/>letterboxed 640x640] --> BB[backbone<br/>ConvBNAct, C3k2, SPPF, C2PSA]
    BB --> NK[neck<br/>top-down + bottom-up fusion]
    NK --> P3[P3 stride 8]
    NK --> P4[P4 stride 16]
    NK --> P5[P5 stride 32]
    P3 & P4 & P5 --> HEAD[dual head<br/>one-to-many + one-to-one]
    HEAD --> O2M[one-to-many branch<br/>training signal, NMS at inference]
    HEAD --> O2O[one-to-one branch<br/>top-k, no suppression]
    O2O --> OUT[300 detections<br/>x1 y1 x2 y2 score class]
```

The interesting part is the **one-to-one branch**. A classical detector predicts many boxes per object and deletes the duplicates afterwards with non-maximum suppression — an operation that is not a neural network, does not export cleanly, and costs latency that grows with the number of objects. Train a second branch to assign exactly one prediction per object, and suppression has nothing left to do: ranking by score and taking the top 300 *is* the decode. The exported graph carries a `TopK` node and no `NonMaxSuppression` node, and a test in this repository asserts exactly that on all three tasks.

The cost is accuracy, and this project measures it rather than assuming it (see below).

Three tasks, one trunk:

| Task | What the head adds | Output row |
| -- | -- | -- |
| **Detection** | nothing — the base case | `[x1, y1, x2, y2, score, class]` |
| **Instance segmentation** | mask coefficients per detection, plus a shared prototype bank the coefficients combine | the detection row, plus a mask cropped to its own box |
| **Oriented detection** | one angle per detection, and boxes that turn | `[cx, cy, w, h, theta, score, class]` — a rotated rectangle, not an upright one |

Every module names the equation it implements and the assumption it rests on. `docs/ASSUMPTIONS.md` is the list of every place where the papers did not determine an answer and this project had to choose one — 63 of them, each with what was chosen, why, and what would falsify it.

<a id="what-was-reproduced"></a>

## 📊 What was reproduced

Three tiers have been trained and accepted, each at the **n scale for 50 epochs on one GPU**. That is a smoke tier: enough to show a mechanism works end to end, far short of the paper's own training budget. Read every number below as "this implementation, trained small" — not as a claim about the paper's headline table.

### Detection — COCO 2017 val, 5000 images

| weights | decode path | mAP50-95 | mAP50 | mAP75 |
| -- | -- | -- | -- | -- |
| EMA | NMS | **25.30** | 38.12 | 26.94 |
| EMA | end-to-end (no NMS) | 23.84 | 36.00 | 25.15 |
| raw | NMS | 25.33 | 38.45 | 27.05 |
| raw | end-to-end (no NMS) | 24.23 | 36.72 | 25.82 |

**The claim this tier exists to test.** The paper reports the NMS-free path costing 0.6–0.8 AP against the NMS path. This run measures **1.11 AP** on raw weights and 1.46 on EMA — the right sign, the right order of magnitude, wider than published. Whether that gap closes at longer schedules is an open question the report records rather than explains away.

![Detection training curves](docs/figures/det_smoke_training.svg)

### Instance segmentation — COCO 2017 val

| criterion | required | observed |
| -- | -- | -- |
| box mAP50-95 | > 25 (the detection floor, unchanged) | **26.12** EMA-NMS |
| mask-to-box ratio | segm mAP50-95 ≥ 0.65 × box mAP50-95 | **0.728** NMS, 0.732 end-to-end |
| wiring gate | train mask IoU ≥ 0.7 | **0.8146** |

The ratio is bounded above by construction — every mask starts from a box this same model predicted and is cropped to it — so the question is only how far below, and where the loss comes from. It is **0.879** at IoU 0.50 and 0.667 at IoU 0.75, and across object sizes 0.30 small / 0.70 medium / 0.91 large. The model finds and labels instances about as well as its boxes do; it pays at the boundary, and most where the prototype grid is coarsest relative to the object.

![Segmentation training curves](docs/figures/seg_smoke_training.svg)

### Oriented detection — DOTA-v1.0 val, tiled

| weights | rotated mAP50-95 | rotated mAP50 |
| -- | -- | -- |
| EMA | **0.2914** | 0.5242 |
| raw | 0.2867 | 0.5140 |

**Read this one carefully: it is per tile.** Aerial images are cut into overlapping 1024 px tiles, and an object crossing a seam is counted in each tile that saw it, so this figure is comparable to nothing published. The whole-image merge exists in the codebase; no run has been re-scored through it yet, and that is roadmap 111 rather than a footnote that quietly ages.

![Oriented training curves](docs/figures/obb_smoke_training.svg)

### What is gated rather than claimed

- **Architecture fidelity**: parameter and FLOP counts for all five scales are held within ±2% params and ±5% FLOPs of the paper's table (n: 2.4M/5.4G through x: 55.7M/193.9G), as a test, on every commit.
- **Regression**: 20 frozen golden metric files, one set per released minor, re-verified beside the 7 live ones on every run of `make gate` — a later release may never silently regress an earlier one's numbers.
- **The suite**: 1826 tests, all offline, no network, no dataset, no GPU required.

Full write-ups with the exact commands as run, the failures along the way, and what each tier does *not* claim: **docs/REPRODUCTION_REPORT.md**. Per-task model cards, including intended use and limitations: **docs/model_cards/**.

<a id="quickstart"></a>

## 🚀 Quickstart

```bash
pip install lucid-yolo             # the released wheel
git clone … && make setup          # or a dev checkout, with the gates
```

Every distribution published before `0.4.0` is a `.devN` pre-release, which `pip` skips by default; add `--pre` if you specifically want one of those.

Four commands cover a whole tier. Datasets are never committed and never downloaded by tests — docs/DATASETS.md says where COCO 2017 and DOTA-v1.0 come from.

```bash
# 1. get data, and check the layout before spending GPU hours on it
lucid-data download --data_root /data/coco --splits '[train,val]' --verify true
lucid-data check    --data_root /data/coco

# 2. train — one config per tier, packaged inside the wheel
lucid-yolo fit --config det_smoke.yaml --data.data_root /data/coco

# 3. score a checkpoint on the protocol its own task names
lucid-eval --checkpoint runs/det.ckpt --data_root /data/coco --output report.json

# 4. predict on one image
lucid-predict --checkpoint runs/det.ckpt --image street.jpg
```

Swap `det_smoke.yaml` for `seg_smoke.yaml` or `obb_smoke.yaml` and the same four commands train and score the other two tasks. docs/TRAINING.md carries the full recipe per tier, including the batch-size scaling and the one setting that matters most on a hosted runtime.

**Releases publish no trained weights** (a deliberate policy — see docs/DECISIONS.md, D14), so every command above takes a checkpoint you trained.

### Seeing what the model saw

```bash
python scripts/draw_predictions.py runs/det.ckpt street.jpg --output street_det.png
python scripts/draw_predictions.py runs/seg.ckpt street.jpg --output street_seg.png --conf-threshold 0.4
python scripts/draw_predictions.py runs/obb.ckpt aerial.png --output aerial_obb.png --img-size 1024
```

A detection checkpoint draws boxes, a segmentation one adds each instance's own mask overlay, and an oriented one draws rotated quadrilaterals — never their upright envelopes, which are a different rectangle from the one the model reported. Colour is a function of the class index alone, so two figures of the same scene compare by eye. `matplotlib` is a dev dependency and stays one: the drawing lives in `scripts/`, and installing the library pulls no plotting stack.

<a id="customizing-it"></a>

## 🛠️ Customizing it

The architecture is **typed Python, not a config file** (docs/DECISIONS.md, ADR-001). Configs carry the schedule, the optimizer settings and the loss gains; topology is named by `variant` and lives in the registry. That means a change to the model is a change to code a type checker reads, and a change to a run is a flag.

| You want to change | Touch | Notes |
| -- | -- | -- |
| Model size | `--variant n\|s\|m\|l\|x` | Five rows of depth / width / max-channels over one topology; only `n` has been trained here |
| A building block | `models/blocks.py` | `ConvBNAct`, `Bottleneck`, `C3k`, `C3k2`, `SPPF`, `SpatialAttention`, `PSABlock`, `C2PSA` — each with its own tests |
| The head, or a new task | `models/heads/` | The dual head, the prototype bank and the semantic auxiliary are separate modules |
| A loss term or its weight | `losses/`, or a `--model.*` gain | CIoU, ProbIoU, angle, mask, semantic, and the progressive branch-weight schedule are each their own file |
| Label assignment | `assign/` | Task-aligned assignment, its small-target variant, and the one-to-one assigner |
| The optimizer | `optim/musgd.py` | MuSGD, with Newton–Schulz orthogonalization split out and tested on its own |
| How detections are decoded | `decode/` | The end-to-end top-k path, the NMS path, and the rotated variants |
| Your own dataset | `--data.data_root` | COCO-JSON and YOLO-txt layouts are both read, and the layout is probed rather than declared |

Then run `make gate`. It runs the linters, the type checker, the full offline suite and the frozen goldens — so a change that quietly moved a number you were not editing fails in about two minutes rather than in a training run tomorrow.

## ⚖️ The decisions that shape it

Six choices explain most of what this repository looks like. Each one is argued in `docs/DECISIONS.md`, and each has evidence behind it rather than only a preference.

**No reference implementation is ever read.** A reproduction that consults the original tests nothing — it inherits the answers, including the ones the paper never gave. So the source list is an allowlist: the papers, and a handful of permissively licensed implementations admitted for diagnostics only, each named with the decision that admitted it ([`PROVENANCE.md`](docs/PROVENANCE.md), D13 / ADR-004). Commit messages cite sources by id, and a hook rejects a commit whose citation does not resolve.

**The architecture is typed Python, never a config file** ([ADR-001](docs/DECISIONS.md)). A YAML topology moves every structural error from the type checker to the first forward pass, and makes "what changed" a diff of strings. Here, five scale rows multiply depth, width and max-channels over one topology, and the run configs carry only schedule, optimizer and loss gains. Evidence that it holds: parameter and FLOP counts for all five scales are gated against the paper's table on every commit.

**Every underdetermined choice becomes a numbered assumption.** When the papers stop short — the mask crop frame, the long-edge angle range, the classifier's prior bias — the choice is written down with what it costs if wrong, and cited from the code that depends on it. That register is what makes a disagreement between this implementation and another *locatable* instead of mysterious ([`ASSUMPTIONS.md`](docs/ASSUMPTIONS.md)).

**The reproduction report is append-only** (D10). A later release corrects an earlier claim by adding to it, never by editing the record away — including the claims that did not survive. The 0.4 consolidation reads all three tiers against each other and its headline result is an *absence*: one quantity all three could have reported in the same units, and only two of them did.

**Releases ship no trained weights** (D14). For the oriented model that is a licence conclusion as well as a policy one — DOTA permits academic use only, so weights trained on it could not ship under this repository's Apache-2.0 terms even if the policy allowed it. What ships instead is the recipe, the report, and the frozen goldens that let you tell whether your run matches.

**Dependencies are permissive-only, and audited by what they ship rather than what they say.** The licence gate reads a distribution's declared fields, the licence documents it bundles, *and* the binaries listed in its own `RECORD` — because a wheel can declare BSD-3-Clause, ship a licence file naming no copyleft, and vendor a GPL library anyway. That is not hypothetical: it is why the example in this README is drawn with matplotlib and not the library originally chosen for it (D15, D16).

## 📓 How this project works

Reproducibility here is a process, not a claim, and the process is visible in the repository:

- **`AGENTS.md`** — the execution contract. One work package, one commit, gate green before it lands.
- **`docs/ROADMAP.md`** — every work package, what it does, its definition of done, its status.
- **`docs/PROVENANCE.md`** — every source this implementation is allowed to read, by id. Commit messages cite them.
- **`docs/ASSUMPTIONS.md`** — every choice the papers left open, numbered and cited from the code that depends on it.
- **`docs/DECISIONS.md`** — architecture and policy decisions, with the four ADRs behind them.
- **`docs/RESEARCH_LOG.md`** — fidelity measurements, negative results, and the modeling mistakes worth remembering.
- **`docs/ENGINEERING_LOG.md`** — the same, for repo tooling: CI, packaging, licensing, doc generation. Split from the research log by claim, not by work package.
- **`docs/REPRODUCTION_REPORT.md`** — append-only: a later release corrects an earlier claim by adding to it, never by editing the record away.

The clean-room rule is absolute and it is what makes the reproduction mean anything: no reference implementation of this architecture was read, at any point, by anyone or anything working on this repository. What is permitted — papers, and permissively licensed implementations admitted for diagnostics — is listed in `PROVENANCE.md` with the decision that admitted it.

## 🏷️ Versioning

Perpetual 0.x release train — each 0.MINOR is a gated capability milestone (0.1 detection, 0.2 instance segmentation, 0.3 oriented detection, 0.4+ rolling). No 1.0 is planned: the project tracks a living specification (the paper plus this project's assumption register), and each release freezes its golden metrics; later releases must never regress them. This is a deliberate policy, not an abandonment signal — see docs/DECISIONS.md.

Current release: **0.4.0** — the three tiers of 0.3 plus the inference path that reads their checkpoints, a second dataset layout, whole-image tile merging and an export gate. It is the first release that trains no new tier and publishes no new accuracy figure. No `v0.1.0` was ever tagged; the detector's history ships inside the 0.2.0 changelog section, and `goldens/frozen/0.2/` is the first frozen set. Releases publish no trained weights (D14), which for the oriented model is a licence matter as well as a policy one: DOTA permits academic use only, so weights trained on it could not ship under this repository's Apache-2.0 terms.

## 🧰 Development

```bash
make setup   # venv + editable install + pre-commit hooks
make gate    # lint + types + tests + golden regression: the merge gate
```

Contributions follow the same contract the rest of the work does: `AGENTS.md` first, one logical change per commit, `make gate` green, and the source of every design claim cited by id.

## 📜 License

Apache-2.0 (see LICENSE and NOTICE). Dependencies are audited on every commit for copyleft licences — including the binaries a wheel ships without declaring them.
