Spec the face pipeline on SCRFD and MobileFaceNet
FR-CULL-8..12 specify the subsystem in terms of "a 512-dimension embedding from a stated model" and stop there, because D13 was open. This names the models, and grounds them in the measurements and the working C++ pipeline in ../scene-actor-extraction rather than in a literature reading. The licensing half of D13 stays open, but with a route through it: the InsightFace weights are non-commercial and cannot be committed, so the app ships the code and the user fetches the model. faces.model_id already makes that a survivable choice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -716,6 +716,9 @@ Specified by FR-CULL-8 … FR-CULL-12, NFR-SEC-5, [architecture.md §6.4](archit
|
||||
spike S14 and decision D13 — the runtime and the model licences are unresolved, so this is the shape
|
||||
of the subsystem, not a build order.
|
||||
|
||||
[faces.md](faces.md) names the models this shape is filled in with, and adds one column to §10.1's
|
||||
`faces` table (`crop_px`) that the calibration in its §8 depends on.
|
||||
|
||||
### 10.1 Schema (a v5 migration)
|
||||
|
||||
```sql
|
||||
|
||||
+769
@@ -0,0 +1,769 @@
|
||||
# Faces and identity — SCRFD and MobileFaceNet
|
||||
|
||||
Spec for **S14**, the spike that decides whether §3.9.1 is buildable, and the build that follows it.
|
||||
|
||||
FR-CULL-8 … FR-CULL-12 specify the subsystem in terms of "a 512-dimension embedding from a stated
|
||||
model" and stop there, deliberately: [catalog.md §10.4](catalog.md) records that the model was left
|
||||
abstract because D13 was open. This document names the two models, fixes the pre- and
|
||||
post-processing they need, specifies the calibration and clustering that sit on top, and states what
|
||||
S14 has to measure before any of it is trusted.
|
||||
|
||||
**It does not close the licensing half of D13.** §2 is the reason, and it comes first because
|
||||
[segmentation.md §7](segmentation.md) established the precedent that reading the grant is cheaper
|
||||
than discovering it at packaging time.
|
||||
|
||||
---
|
||||
|
||||
## 1. The two models, and why these two
|
||||
|
||||
**Detector — SCRFD.** *Sample and Computation Redistribution for Efficient Face Detection* (Guo et
|
||||
al., ICLR 2022). A single-stage anchor-based detector whose contribution is where the FLOPs go, not
|
||||
a new architecture: the shallow stages carry more capacity because that is where small faces are
|
||||
decided. It emits, per face, a box, a confidence, and **five landmarks in the same forward pass** —
|
||||
which is the property that matters here, because FR-CULL-8 requires landmarks and because §4's
|
||||
alignment step is not optional. A detector without landmarks would need a second network to supply
|
||||
them.
|
||||
|
||||
The `500M` variant is the one to ship: 500 MFLOPs at VGA, against 10 GFLOPs for `10G`. Face indexing
|
||||
is a background sweep over a whole library on a phone as well as a desktop (NFR-RES-2), so the cheap
|
||||
variant's accuracy loss on tiny faces is the right trade — a face too small for `500M` to find is
|
||||
also too small for §5 to embed usefully.
|
||||
|
||||
**Embedder — MobileFaceNet trained with ArcFace loss ("MBF").** ~1M parameters, ~0.45 GFLOPs for one
|
||||
112×112 crop, 512-d output. ArcFace's additive angular margin is what makes the output space one
|
||||
where *cosine similarity means something* — the loss explicitly optimises angular separation between
|
||||
identities, which is what §6's calibration then has to convert into a probability.
|
||||
|
||||
**Why not the larger ResNet50 embedder** (`w600k_r50`, the other half of InsightFace's `buffalo_l`):
|
||||
because it is not measurably better here, and it is 13× the file. §1.1's reference project fitted the
|
||||
same Platt calibration (§8) to all four candidates over a 2,418-identity gallery, which is a clean
|
||||
read on the embedding space alone with no tracking or matching logic in it:
|
||||
|
||||
| Model | File | Steepness `a` | Boundary at P=0.5 | Held-out macro F1 |
|
||||
|---|---|---|---|---|
|
||||
| LVFace-B (Glint360K) | 455 MB | 17.7 | sim 0.228 | 67.4% |
|
||||
| **ArcFace w600k-MBF** | **13 MB** | **16.2** | **sim 0.267** | **64.4%** |
|
||||
| ArcFace w600k-R50 | 174 MB | 15.4 | sim 0.301 | — |
|
||||
| ArcFace R18 | 48 MB | 15.3 | sim 0.309 | 63.1% |
|
||||
|
||||
**MBF's calibration curve is steeper than R50's and R18's**, separating same-identity from
|
||||
different-identity pairs more confidently and at a lower boundary, at a fraction of the size. LVFace-B
|
||||
is genuinely better and is 35× the file — a defensible choice for a desktop-only feature and not one
|
||||
for a subsystem that has to index a library on a phone (NFR-RES-2).
|
||||
|
||||
Two caveats on that table, both from the source: the R50 gallery was built with ~30% fewer reference
|
||||
images per identity, which is why it carries no F1 figure — the calibration row does not depend on the
|
||||
image count and is comparable, the benchmark row would not have been. And the F1 column measures a
|
||||
*film-cast identification* task, not this one. It ranks the models; it does not predict DarkRoom's
|
||||
accuracy.
|
||||
|
||||
**Both are pure convolutional graphs**, which matters for §3: the runtime is tract, and tract's
|
||||
operator coverage is the thing that decides whether a graph runs at all.
|
||||
|
||||
### 1.1 A working reference implementation exists
|
||||
|
||||
`../scene-actor-extraction` is a C++ pipeline by the same author that runs **exactly this model
|
||||
pair** — SCRFD-500MF then ArcFace over 5-point-aligned 112×112 crops — over feature films, with a
|
||||
fitted Platt calibration and a scored benchmark behind it. It is **MIT licensed**, so porting from it
|
||||
into GPL-3.0-or-later is straightforward and needs attribution, not permission.
|
||||
|
||||
That changes what S14 is. The open questions are no longer "does this pair work" and "what are the
|
||||
magic numbers" — they are *does tract load these graphs* (§12 M1, and §4.1 says why that is in
|
||||
genuine doubt) and *does the accuracy hold on family snapshots rather than film frames*. The
|
||||
pre-processing constants, the decode layout, and the calibration algorithm are all readable rather
|
||||
than rediscoverable, and several of them are not what the published Python would lead you to write.
|
||||
|
||||
| What | Reference | Ports to |
|
||||
|---|---|---|
|
||||
| ArcFace 5-point template and warp | `src/face_utils.hpp` | `align.rs` (§5) |
|
||||
| SCRFD pre-process, decode, NMS | `src/backends/ort_backend.cpp` `SCRFDDecoder` | `detect.rs` (§4) |
|
||||
| ArcFace pre-process and L2 normalise | same file, `ArcFaceEmbedder` | `embed.rs` (§6) |
|
||||
| Platt fit over a similarity histogram | `src/gallery/gallery_calibration.hpp` | `calibrate.rs` (§8) |
|
||||
| Model-free unit tests for both | `tests/test_face_utils.cpp`, `tests/test_calibration.cpp` | the §3 feature split |
|
||||
|
||||
That last row is worth noticing: the reference already separates the geometry and arithmetic from the
|
||||
inference well enough to unit-test them with no model on the machine. §3's `inference` feature flag is
|
||||
the same boundary, enforced by Cargo rather than by discipline.
|
||||
|
||||
**What does not port.** The reference matches faces against a *known gallery* of named identities;
|
||||
DarkRoom clusters an unlabelled library (§9). And it is a video pipeline, so it has a tracker, a
|
||||
`max_faces` cap, and frame-to-frame identity annealing that have no analogue here.
|
||||
|
||||
---
|
||||
|
||||
## 2. Licensing — the open half of D13
|
||||
|
||||
The architectures are published research. **The weights everyone actually uses are not
|
||||
redistributable by this project.**
|
||||
|
||||
InsightFace's code is MIT. Its *pretrained models* — `buffalo_l`, `buffalo_s`, `buffalo_sc`, and
|
||||
every `det_*` and `w600k_*` checkpoint inside them — carry a **non-commercial research-only** grant,
|
||||
stated in the model-zoo README and applying to both the auto-downloaded and the manually downloaded
|
||||
artefacts. `w600k_mbf` is trained on WebFace600K, whose own terms are research-only as well, so the
|
||||
restriction has two independent sources rather than one that might be renegotiated.
|
||||
|
||||
These are the exact artefacts in question — §1.1's reference project fetches them from InsightFace's
|
||||
own release page, which is where the grant attaches:
|
||||
|
||||
| File | Source |
|
||||
|---|---|
|
||||
| `det_500m.onnx` (2.5 MB) | `insightface/releases/download/v0.7/buffalo_sc.zip` |
|
||||
| `w600k_mbf.onnx` (13 MB) | `insightface/releases/download/v0.7/buffalo_s.zip` |
|
||||
|
||||
DarkRoom is GPL-3.0-or-later. A non-commercial field-of-use restriction is not a GPL-compatible
|
||||
grant, so:
|
||||
|
||||
- these weights **cannot be committed to this repository**, the way `yolo26n-seg.onnx` is (D14);
|
||||
- they cannot ship inside an APK, a Flatpak, or an F-Droid build;
|
||||
- and the restriction binds the *user*, not only the project — a professional photographer using
|
||||
DarkRoom commercially is outside the grant even if they fetched the file themselves.
|
||||
|
||||
That last point is the one that is easy to miss and dishonest to leave implicit. It is why §2.2's
|
||||
recommended route puts the licence text in front of the user rather than in a footnote.
|
||||
|
||||
### 2.1 The routes, priced
|
||||
|
||||
| Route | What ships | Cost |
|
||||
|---|---|---|
|
||||
| **A — commit the weights** | Everything works out of the box, one `git lfs pull` | **Not available.** Licence-incompatible with GPL-3.0 and with all three distribution channels (NFR-COMPAT-2). Listed only so it is visibly rejected rather than quietly assumed. |
|
||||
| **B — permissively licensed weights** | Same, legally | Requires weights that do not exist off the shelf in this architecture pair. Either found (§2.3) or trained, and training an ArcFace embedder needs a face dataset whose own terms permit redistribution — the harder half of the problem. |
|
||||
| **C — the user fetches them** | The app ships the *code*, not the weights; face indexing is off until the user obtains a model | Works today, keeps the repository clean, and is honest. Costs a first-run flow, an F-Droid anti-feature declaration, and a licence notice the user has to read. |
|
||||
|
||||
**Recommendation: C now, B when it becomes possible.** §1.1's project already works this way — a
|
||||
`scripts/download_models.sh` that fetches the weights rather than a repository that carries them —
|
||||
so route C is a practised pattern here rather than a theory. Two things DarkRoom must do that the
|
||||
reference does not: pin a checksum for **every** model (the reference verifies only one of the four),
|
||||
and put the licence in front of the user, because a photo editor's users are not all researchers.
|
||||
|
||||
The schema already forces this to be a survivable choice — `faces.model_id` (catalog.md §10.1) exists precisely so that a model change is
|
||||
detectable and re-indexable rather than silently poisoning every similarity in the library. Under C,
|
||||
swapping in a permissive model later is a new `model_id` and a re-index, not a migration.
|
||||
|
||||
### 2.2 What route C requires of the code
|
||||
|
||||
- **The weights are never a build input.** `dr-face` (§3) takes bytes; it has no `embedded-model`
|
||||
feature and no `models/` directory. This is the one structural difference from `dr-segment`, and
|
||||
it is deliberate — a feature flag that *could* embed weights is a feature flag someone eventually
|
||||
turns on in a packaging script.
|
||||
- **The fetch does not live in `dr-face`.** It lives in the app layer, so the inference crate keeps
|
||||
no network dependency at all. §11 makes that a checkable property rather than a convention.
|
||||
- **The licence is shown, not linked.** Before the first download, the app states in plain language
|
||||
that the weights are research-only, that commercial use is outside the grant, and who the grantor
|
||||
is. NFR-SEC-5's last bullet already requires the models to be named, versioned and licensed in the
|
||||
about screen; this is the same obligation moved to the moment where it can still change a
|
||||
decision.
|
||||
- **A checksum is pinned.** The app verifies the digest of what it fetched against a value compiled
|
||||
in, and refuses a mismatch. An unverified model file is an arbitrary graph executed over the
|
||||
user's family photographs.
|
||||
- **Indexing stays off until a model is present and verified**, and disabling it deletes nothing —
|
||||
that is a separate control (NFR-SEC-5, §11).
|
||||
|
||||
### 2.3 Before S14 writes any code
|
||||
|
||||
Three questions to answer by reading, in this order, and to record with the date they were checked —
|
||||
the same discipline [segmentation.md §7](segmentation.md) applied:
|
||||
|
||||
1. **Is there a permissively licensed SCRFD export?** Third-party ONNX exports of SCRFD are
|
||||
plentiful; a re-export inherits the original weights' grant, so the question is whether anyone has
|
||||
*trained* SCRFD on a redistributable dataset, not whether they have converted the InsightFace one.
|
||||
2. **Is there a permissively licensed 512-d MobileFaceNet/ArcFace checkpoint?** Candidates worth
|
||||
reading the terms on rather than assuming: EdgeFace (Idiap), the ONNX Model Zoo's ArcFace entry
|
||||
(MIT, but a ResNet100 at ~250 MB — licence-clean and the wrong size), and any MobileFaceNet
|
||||
trained on a dataset with distribution terms.
|
||||
3. **What does a permissive *detector* alternative cost in accuracy?** YuNet (OpenCV Zoo) is 232 KB,
|
||||
emits five landmarks, and comes from a permissively licensed repository. If it is close enough,
|
||||
the detector half of the licence problem disappears and only the embedder remains — a materially
|
||||
better position than either half alone.
|
||||
|
||||
Question 3 is nearly free to answer: `face_detection_yunet_2023mar.onnx` is **already sitting in
|
||||
§1.1's `models/` directory**, fetched from `opencv/opencv_zoo`. It needs its own decode path (its
|
||||
output layout is different, which is why the reference's `SCRFDDecoder` explicitly rejects it at load
|
||||
rather than silently misreading it), and then it is one more row in §12's table.
|
||||
|
||||
---
|
||||
|
||||
## 3. Crate shape — `core/dr-face`
|
||||
|
||||
A new workspace member, modelled on `dr-segment` and for the same reason: everything that reasons
|
||||
about faces is testable with no GPU adapter and no model present (ARCH §6.5a).
|
||||
|
||||
```
|
||||
core/dr-face/
|
||||
src/lib.rs FaceError, re-exports
|
||||
src/detect.rs SCRFD: preprocess, decode, NMS
|
||||
src/align.rs 5-point similarity transform → 112×112 crop
|
||||
src/embed.rs MBF: preprocess, forward, L2 normalise
|
||||
src/calibrate.rs cosine → P(same person) (FR-CULL-9)
|
||||
src/cluster.rs constrained agglomeration (FR-CULL-10)
|
||||
```
|
||||
|
||||
```toml
|
||||
[features]
|
||||
# No `embedded-model`. §2.2 — the weights are not a build input, ever.
|
||||
default = []
|
||||
# The ONNX runtime. Off by default so `calibrate` and `cluster` — which are
|
||||
# arithmetic over embeddings and have no model in them — stay testable in a
|
||||
# build that carries no inference engine at all.
|
||||
inference = ["dep:ort", "dep:ort-tract", "dep:ndarray"]
|
||||
```
|
||||
|
||||
The `ort` + `ort-tract` pairing is settled by D13's 2026-08-21 update and already in the workspace
|
||||
manifest: `ort`'s API, tract's pure-Rust engine, no C under the NDK.
|
||||
|
||||
**The split between `inference` and the rest is load-bearing.** Calibration and clustering are where
|
||||
the subsystem's accuracy actually lives, they are pure functions of embeddings and labels, and they
|
||||
must be testable against synthetic embedding sets with no weights on the machine. A test suite that
|
||||
needs a research-licensed download to run is a test suite that does not run in CI.
|
||||
|
||||
### 3.1 The API
|
||||
|
||||
```rust
|
||||
/// A loaded detector. Fixed input shape — see §4.
|
||||
pub struct Detector { /* session, input edge */ }
|
||||
|
||||
impl Detector {
|
||||
pub fn from_bytes(onnx: &[u8], id: ModelId) -> Result<Self, FaceError>;
|
||||
/// `rgb` is f32 0..=1, row-major, three per pixel.
|
||||
pub fn detect(&self, rgb: &[f32], w: usize, h: usize, opts: &DetectOptions)
|
||||
-> Result<Vec<Detection>, FaceError>;
|
||||
}
|
||||
|
||||
pub struct Detection {
|
||||
/// Normalised to the image's long edge (catalog.md §10.1).
|
||||
pub bbox: (f32, f32, f32, f32),
|
||||
/// Five points, same normalisation, in the model's own order (§5).
|
||||
pub landmarks: [(f32, f32); 5],
|
||||
pub confidence: f32,
|
||||
}
|
||||
|
||||
pub struct Embedder { /* session */ }
|
||||
|
||||
impl Embedder {
|
||||
pub fn from_bytes(onnx: &[u8], id: ModelId) -> Result<Self, FaceError>;
|
||||
/// Takes an *aligned* 112×112 crop. Alignment is `align::warp`; passing a
|
||||
/// raw bbox crop here is the silent-accuracy-loss bug §5 exists to prevent.
|
||||
pub fn embed(&self, aligned: &Aligned112) -> Result<Embedding, FaceError>;
|
||||
}
|
||||
|
||||
/// L2-normalised, 512-d. Carries its model id so a comparison across models
|
||||
/// is a type error rather than a plausible-looking number (catalog.md §10.1).
|
||||
pub struct Embedding { pub model: ModelId, pub v: [f32; 512] }
|
||||
```
|
||||
|
||||
`Aligned112` is a newtype over the pixel buffer that only `align::warp` can construct. That is the
|
||||
whole defence against §5's failure mode, and it costs nothing.
|
||||
|
||||
---
|
||||
|
||||
## 4. Detection
|
||||
|
||||
### 4.1 Fixed input, and how the image is fitted to it
|
||||
|
||||
**`det_500m.onnx` has a dynamic H/W input, and this is the largest single risk in the document.**
|
||||
§1.1's reference had to use ONNX Runtime rather than OpenCV's `dnn` module precisely because OpenCV
|
||||
could not load SCRFD's dynamic `Shape` nodes. tract is in the same family of problem: `dr-segment`
|
||||
exists in its current shape because tract failed shape inference on YOLO's dynamic export, which is
|
||||
why `tools/export-seg-model.sh` passes `dynamic=False` and why `semantic.rs` has a fixed
|
||||
`INPUT_EDGE`.
|
||||
|
||||
So the graph almost certainly needs its input dimensions frozen before tract will touch it. That is a
|
||||
known, cheap operation — `onnxruntime.tools.make_dynamic_shape_fixed` rewrites the declared dims
|
||||
without retraining or re-exporting from PyTorch — but it is a step, it produces an artefact that must
|
||||
itself be reproducible (a `tools/fix-face-model-shapes.sh`, in the spirit of the existing export
|
||||
script), and **it must be tried before anything else in this document is scheduled.** §12's M1.
|
||||
|
||||
Fixed at **640**, with 320 available as a faster, blinder option to measure.
|
||||
|
||||
**Letterbox.** Scale by `min(640/w, 640/h)` preserving aspect, then paste into a 640×640 canvas. The
|
||||
reference centres the image and fills the margin with grey `114`, inverting with
|
||||
`x_src = (x_model − pad_x) / scale`. What matters is not *where* the padding goes but that the
|
||||
forward and inverse agree and that the fill value is treated as part of the contract: a mismatch
|
||||
between them offsets every box and landmark the model returns by the padding, which yields detections
|
||||
that look plausible, landmarks that align badly, and an embedding-quality loss that only shows up as
|
||||
poor clustering three stages later. Port the reference's convention rather than inventing one, and
|
||||
assert it with a round-trip test.
|
||||
|
||||
Normalisation is `(x·255 − 127.5) / 128`, RGB, NCHW — note `/128`, not `/127.5`; see §6.
|
||||
|
||||
### 4.2 Outputs, and decoding them
|
||||
|
||||
**Nine tensors for three strides `{8, 16, 32}`, twelve for four `{8, 16, 32, 64}`.** Which one a
|
||||
given export produces is discovered at load — `strides = output_count / 3` — not assumed, because
|
||||
both variants exist and hardcoding three silently ignores the largest faces in a twelve-output model.
|
||||
With two anchors per location and `N_s = (640/s)²` locations:
|
||||
|
||||
| Tensor | Shape | Meaning |
|
||||
|---|---|---|
|
||||
| `score_s` | `[N_s·2, 1]` | sigmoid already applied inside the graph |
|
||||
| `bbox_s` | `[N_s·2, 4]` | distances left, top, right, bottom **in units of the stride** |
|
||||
| `kps_s` | `[N_s·2, 10]` | five `(dx, dy)` offsets, same units |
|
||||
|
||||
Anchor centres are `(x·s, y·s)` for each grid cell, repeated once per anchor. Decoding is therefore
|
||||
|
||||
```
|
||||
x1 = cx − l·s x2 = cx + r·s kx_i = cx + dx_i·s
|
||||
y1 = cy − t·s y2 = cy + b·s ky_i = cy + dy_i·s
|
||||
```
|
||||
|
||||
then divide by the letterbox scale to return to source pixels, then normalise by the long edge before
|
||||
storage.
|
||||
|
||||
Flat index within a stride is `(row · fw + col) · 2 + anchor`.
|
||||
|
||||
**A shape assertion at load time, not a decode-time surprise.** `dr-segment` already learned this —
|
||||
`SegmentError::OutputShape` exists because a different export of the same model produces confidently
|
||||
wrong results otherwise. The reference implements exactly this and it is worth porting verbatim:
|
||||
output count divisible by three and between 9 and 12, then the **last dimension of each group checked
|
||||
against `{1, 4, 10}`**. That single check is what catches a YuNet file passed where an SCRFD one was
|
||||
meant — YuNet also has twelve outputs, so the count alone does not distinguish them, and the failure
|
||||
without the check is not an error but a page of plausible garbage.
|
||||
|
||||
### 4.3 Thresholds
|
||||
|
||||
Score ≥ **0.5**, NMS IoU **0.4**, plain greedy NMS per image across all strides together.
|
||||
|
||||
Deliberately *not* the low threshold `dr-segment` chose. There, a false positive costs one spurious
|
||||
entry in a list the user is picking from. Here it costs an entry in the People view that the user has
|
||||
to reject, in a library with thousands of images — and worse, a garbage embedding that participates
|
||||
in clustering and can bridge two real clusters into one. False negatives are recoverable by a later
|
||||
re-index with a better model; a polluted cluster graph is not, once the user has confirmed faces
|
||||
inside it.
|
||||
|
||||
A minimum box size of **40 px on the source proxy** is applied on top — the reference's figure,
|
||||
chosen there for the same reason it holds here: below it there is not enough face left to align
|
||||
reliably, and §7's table shows what the embedder actually receives at that size. §12's M4 is what
|
||||
moves this number, if measurement says it should move.
|
||||
|
||||
**One thing not to port:** the reference caps detections at ten per frame, largest first. That is
|
||||
right for a film frame, where the extras in the background are noise. It is wrong for a photo
|
||||
library, where a group shot with thirty faces in it is precisely the picture the user wants indexed.
|
||||
No cap; the min-size floor is the only filter.
|
||||
|
||||
---
|
||||
|
||||
## 5. Alignment — the step that is silently wrong when skipped
|
||||
|
||||
ArcFace embeddings are trained on faces warped to a canonical 112×112 arrangement. Feeding the model
|
||||
a plain bounding-box crop *works* — it produces 512 numbers, they are unit-norm, and cosine
|
||||
similarities between them look entirely reasonable. They are just much worse, and nothing in the
|
||||
system reports it.
|
||||
|
||||
The transform is a **similarity transform** — rotation, uniform scale, translation, four degrees of
|
||||
freedom — from the five detected landmarks to this template, which is the arrangement the weights were
|
||||
trained against:
|
||||
|
||||
```
|
||||
(38.2946, 51.6963) subject's right eye ─┐ image-left of centre
|
||||
(73.5318, 51.5014) subject's left eye ─┘
|
||||
(56.0252, 71.7366) nose tip
|
||||
(41.5493, 92.3655) subject's right mouth corner
|
||||
(70.7299, 92.2041) subject's left mouth corner
|
||||
```
|
||||
|
||||
Similarity, not affine: a full affine fit to five noisy points will happily shear a face, and shear is
|
||||
exactly the deformation the embedder was never shown.
|
||||
|
||||
**The naming is a trap and the order is not.** Point 0 sits at x=38 on a 112-wide canvas — left of
|
||||
centre *in the image*, which is the subject's **right** eye. Both namings are in circulation and they
|
||||
are opposite. What matters is that SCRFD emits its five points in this same order, so the correct
|
||||
amount of reordering between detector and template is **none**; the reference states this explicitly
|
||||
in `types.hpp` for the benefit of whoever next reads it and doubts it. A future detector with a
|
||||
different order carries its own permutation next to its `model_id`, rather than this file growing an
|
||||
assumption.
|
||||
|
||||
**One divergence from the reference to settle by measurement.** It fits the transform with OpenCV's
|
||||
`estimateAffinePartial2D` under **RANSAC** at a 3-pixel threshold, and drops the detection when the
|
||||
fit fails. RANSAC over five points is a strange fit — the minimal sample for a similarity is two, so
|
||||
it can discard landmarks it judges outliers and solve from a subset, which on a profile face is as
|
||||
likely to be the correct geometry as the wrong one. InsightFace's own pipeline uses a plain
|
||||
least-squares (Umeyama) similarity over all five points, which cannot silently drop anything and is
|
||||
the one to write first. §12's M5 measures whether the choice matters; the degenerate-input path still
|
||||
has to exist either way, because collinear landmarks do occur.
|
||||
|
||||
**A trick worth keeping.** The reference retries detection on a frame where nothing was found, after
|
||||
replicate-padding by 25% and applying CLAHE to the L channel. Padding gives a face at the very edge a
|
||||
context the detector needs; CLAHE rescues underexposed frames. Both are plausible on scanned negatives
|
||||
and backlit photographs — worth trying at low priority, since a retry doubles the cost of exactly the
|
||||
images that yielded nothing.
|
||||
|
||||
Sampling is bilinear from the *source proxy*, in one step — never crop-then-warp, which resamples
|
||||
twice and throws away detail the warp could have used. Pixels falling outside the source are black.
|
||||
|
||||
**The landmark order must match the template order.** The template above is written in the detector's
|
||||
own output order; if a future detector emits them differently, the template is reordered with it and
|
||||
that mapping belongs next to the model id, not compiled in as an assumption.
|
||||
|
||||
*Test:* warp a synthetic image with a known rotation and scale, and assert the five points land on
|
||||
the template within a fraction of a pixel. This is testable with no model present, which is why
|
||||
`align` sits outside the `inference` feature.
|
||||
|
||||
---
|
||||
|
||||
## 6. Embedding
|
||||
|
||||
112×112 **RGB** — the crop comes out of the warp in whatever order the source was in, and the
|
||||
reference converts BGR→RGB explicitly before the blob rather than relying on a flag, which is the
|
||||
readable way to do it.
|
||||
|
||||
Normalisation is `(x·255 − 127.5) / 128`. **Note `/128`, not `/127.5`:** InsightFace's published
|
||||
Python uses `1.0/127.5` for the recognition model, the reference uses `1.0/128` for both models, and
|
||||
every measured number in §1's table was produced with `/128`. The difference is 0.4% of scale and
|
||||
almost certainly immaterial, but "almost certainly" is not a reason to pick silently — write `/128` to
|
||||
match the numbers we have, and settle it with one back-to-back run in §12.
|
||||
|
||||
Output is 512 floats; **L2-normalise before storing**, so every downstream comparison is a dot product
|
||||
and no code path has to remember to normalise. The reference clamps the norm at 1e-6 before dividing,
|
||||
which costs nothing and removes a NaN path.
|
||||
|
||||
Some exports of these graphs are fp16 in, fp16 out. The reference detects this from the graph's
|
||||
declared element type rather than from the filename; worth porting, because the alternative failure is
|
||||
a silent garbage tensor.
|
||||
|
||||
Storage is `512 × f16` (catalog.md §10.1) — 1 KB per face, 25 MB for a 25,000-face library. The f16
|
||||
round-trip perturbs a unit vector by ~1e-3 in cosine, three orders of magnitude below the separation
|
||||
between a match and a non-match, and the halving matters because these rows are the ones NFR-SEC-5
|
||||
contemplates optionally syncing.
|
||||
|
||||
Re-normalise on load after the f16 widen. It is one pass over 512 floats and it removes a class of
|
||||
drift that is otherwise invisible.
|
||||
|
||||
---
|
||||
|
||||
## 7. Which pixels the pipeline actually sees
|
||||
|
||||
FR-CULL-8 pins indexing to the FR-CULL-2 ladder: **the proxy tier, never a full decode.** The
|
||||
relevant tier is `ThumbSize::Large` — 1024 px on the long edge (`dr-thumbs`).
|
||||
|
||||
That has a consequence worth stating in numbers rather than discovering in a clustering report. On a
|
||||
1024 px proxy:
|
||||
|
||||
| Face size in frame | Pixels across | What §6 receives |
|
||||
|---|---|---|
|
||||
| A portrait, face fills a third of the frame | ~340 | Downsampled to 112. Ideal. |
|
||||
| Two people, half-length | ~120 | Roughly native. Good. |
|
||||
| A group of eight | ~50 | **Upsampled** to 112. Degraded, and usable. |
|
||||
| A figure in a landscape | ~20 | At or below §4.3's floor. Rejected. |
|
||||
|
||||
So `faces` records one column beyond catalog.md §10.1's schema:
|
||||
|
||||
```sql
|
||||
ALTER TABLE faces ADD COLUMN crop_px INTEGER NOT NULL; -- source pixels across the aligned crop
|
||||
```
|
||||
|
||||
`crop_px` earns its place three times over. It is the honest quality signal for the UI; it is a
|
||||
**feature in §8's calibration**, which FR-CULL-9 explicitly demands ("a raw cosine means something
|
||||
different for every model, every population, and *every face size*"); and it is what a future
|
||||
higher-resolution re-embedding pass would select on, so that pass becomes a query rather than a
|
||||
re-index of everything.
|
||||
|
||||
**No RAW decode is added.** Where the Large proxy is missing, the job enqueues a `Thumbnail` job at
|
||||
background priority and re-queues itself, exactly as FR-CULL-8 requires.
|
||||
|
||||
---
|
||||
|
||||
## 8. Calibration — cosine to probability
|
||||
|
||||
FR-CULL-9 makes this a hard requirement: no code path may threshold a bare cosine, every threshold
|
||||
in the subsystem is stated as a probability, and the fit is per library and reports its own validity.
|
||||
This section is how that is obtained, and the interesting part is where the training pairs come from
|
||||
when the user has labelled nothing yet.
|
||||
|
||||
### 8.1 Where the pairs come from
|
||||
|
||||
**Negatives are free and abundant.** Two faces detected in *the same photograph* are almost never the
|
||||
same person. That gives every multi-face image in the library a full set of negative pairs at no
|
||||
labelling cost — and they are *hard* negatives, drawn from the same camera, lighting, and processing,
|
||||
which is exactly the population where a threshold tuned on easy negatives fails. The known exceptions
|
||||
— mirrors, photographs of photographs, collages, a spliced panorama — are rare enough to be noise at
|
||||
this scale and worth naming so the exception is not mistaken for a bug later.
|
||||
|
||||
**Positives, in order of trustworthiness:**
|
||||
|
||||
1. **User confirmations** (FR-CULL-10). Every pair of faces confirmed to the same person. The gold
|
||||
standard, and empty on day one.
|
||||
2. **Burst siblings.** FR-CULL-5 already groups bursts. Two faces in adjacent frames of one burst,
|
||||
in nearly the same position, are near-certainly the same person. Free, requires no labelling, and
|
||||
available immediately on any library with continuous-shooting frames in it. **Their purity is an
|
||||
S14 measurement** (§12), not an assumption — if bursts turn out to be dirtier than expected, this
|
||||
source is dropped and the calibration simply stays invalid for longer.
|
||||
3. Nothing else. Bootstrapping positives from high cosine similarity is circular — it fits the
|
||||
calibration to the belief it was supposed to test.
|
||||
|
||||
### 8.2 The fit
|
||||
|
||||
§1.1's reference already implements this and its shape should be ported rather than reinvented:
|
||||
|
||||
```
|
||||
P(same | cos) = σ(a·cos + b + log_prior_odds)
|
||||
```
|
||||
|
||||
Four details in it are the difference between working and nearly working.
|
||||
|
||||
**Fit against a histogram, not against pairs.** A 25,000-face library has 3×10⁸ pairs; no gradient
|
||||
descent is running over that. The reference buckets every pair into **200 bins over cos ∈ [−1, 1]**,
|
||||
carrying a positive and a negative count per bin, and fits the two parameters against the per-bin
|
||||
counts. The cost becomes the similarity matrix plus 200 numbers, and the matrix is a single GEMM
|
||||
(§9). This is the trick that makes a per-library fit affordable at all, and it is not obvious from
|
||||
the outside.
|
||||
|
||||
**Balance the classes explicitly.** `w_pos = total/(2·n_pos)`, `w_neg = total/(2·n_neg)`. Negatives
|
||||
outnumber positives by orders of magnitude; an unweighted fit produces a well-shaped curve sitting at
|
||||
the wrong height — precisely the "plausible number all the way to the user interface" failure
|
||||
FR-CULL-9 describes.
|
||||
|
||||
**The base rate is a runtime argument, not part of the fit.** `log_prior_odds` is added at evaluation
|
||||
time, so the balanced fit is stored once and the prior varies per query — the odds that two faces
|
||||
drawn from a 40-image holiday album match are not the odds for a 40,000-image archive. Its inverse,
|
||||
`boundary_at(p)`, gives the cosine at which the probability crosses `p`, which is what turns §9's
|
||||
"merge above 0.9" into an actual comparison. Baking a prior into `a` and `b` would need a refit per
|
||||
context and would make the stored parameters mean something different depending on where they came
|
||||
from.
|
||||
|
||||
**Deduplicate before pairing.** Near-identical embeddings (cos > 1 − 1e-7) are the same photograph
|
||||
counted twice; the reference drops them per identity first. In a photo library the equivalent is
|
||||
duplicates and virtual copies (FR-CAT-11), and leaving them in stacks the positive histogram at
|
||||
cos ≈ 1 with pairs that teach the fit nothing about hard cases.
|
||||
|
||||
**A third feature this design adds.** The reference fits on cosine alone; DarkRoom should carry
|
||||
`crop_px` too:
|
||||
|
||||
```
|
||||
logit P(same) = w₀ + w₁·cos + w₂·log₂(min(crop_px_a, crop_px_b))
|
||||
```
|
||||
|
||||
FR-CULL-9 explicitly names face size as an axis along which an uncalibrated similarity misbehaves,
|
||||
§7 shows a real library spans 40 px to 340 px of face, and `crop_px` is already in hand. The
|
||||
*minimum* of the pair, because a comparison is only as good as its worse crop. It generalises the
|
||||
histogram to a small 2-D grid of bins, which changes nothing structural. If M7 says the extra feature
|
||||
buys nothing, drop it and match the reference exactly — but a film pipeline working from broadcast
|
||||
frames had far less size variation to explain than this one does.
|
||||
|
||||
### 8.3 Validity, and saying so
|
||||
|
||||
Stored per catalog.md §10.3: parameters, a validity flag, and a hash of the face set fitted from.
|
||||
|
||||
Invalid when fewer than **200 positive pairs** or **2,000 negative pairs** are available, or when the
|
||||
reliability check fails.
|
||||
|
||||
Deliberately far stricter than the reference's floor of two positives and one negative. That floor is
|
||||
reasonable there: its pairs come from a curated gallery of labelled reference portraits, where a
|
||||
positive pair is trustworthy by construction. Here the positives are bootstrapped from bursts and
|
||||
early confirmations (§8.1) and the whole risk is fitting confidently to a handful of them. The
|
||||
reference also refuses to draw positives from an identity with fewer than five distinct embeddings,
|
||||
letting it contribute negatives only — the same asymmetry applies to a thinly-confirmed person and is
|
||||
worth keeping. In that state the UI says confidence is unavailable and the People view
|
||||
still works — clustering falls back to a documented default operating point, labelled in the
|
||||
interface as an untuned default, and no probability is displayed. That is FR-CULL-9's requirement
|
||||
read literally: not presenting an untuned default *as though it were measured*.
|
||||
|
||||
Refit is triggered by the same debounce as clustering (§9), and when the confirmed-pair count grows
|
||||
materially.
|
||||
|
||||
*Acceptance (FR-CULL-9):* a reliability diagram over ten probability bins, on a held-out labelled
|
||||
split, with observed match rate within a stated tolerance of the predicted probability in each
|
||||
populated bin. A single accuracy figure is not an answer to this requirement.
|
||||
|
||||
---
|
||||
|
||||
## 9. Clustering
|
||||
|
||||
Not a job kind, for the reason catalog.md §10.2 gives: it is a whole-library operation with no
|
||||
natural `subject_id`. It runs as a debounced library pass when detection has been idle and the face
|
||||
count has moved materially.
|
||||
|
||||
**The graph.** For each face, its `k = 20` nearest neighbours by cosine, then each candidate edge
|
||||
scored through §8 to a probability.
|
||||
|
||||
Brute force is honest arithmetic here, and §1.1's project is the evidence rather than the estimate: it
|
||||
computes the full similarity matrix as **one GEMM** (`E · Eᵀ` over L2-normalised rows) at gallery
|
||||
scale, on CPU, and instruments it well enough to have compared CPU against OpenCL and found the
|
||||
question worth asking rather than urgent. For DarkRoom that is a 25,000 × 512 matrix at 25 MB in and
|
||||
~2.5 GB out — so the matrix is computed **in row blocks**, with each block reduced to its top-k and
|
||||
its histogram contribution before the next is started, and never materialised whole. Once, in the
|
||||
background, after an indexing sweep that took an hour. An approximate index is an optimisation to
|
||||
reach for when §12 says it is needed, not before.
|
||||
|
||||
**Constraints, not just thresholds:**
|
||||
|
||||
- **Cannot-link on co-occurrence.** Two faces in the same image are never merged. This is the same
|
||||
observation §8.1 mines for negatives, used here as a hard constraint, and it is the single
|
||||
cheapest defence against the over-merging FR-CULL-10 warns about.
|
||||
- **Confirmed faces are anchors.** A confirmation is user data (FR-CULL-12) and clustering never
|
||||
moves it. Two clusters each containing confirmations of *different* people cannot merge; a cluster
|
||||
containing confirmations of one person absorbs suggestions but never reassigns the confirmed.
|
||||
|
||||
**The algorithm.** Constrained average-link agglomeration over the probability graph, merging while
|
||||
the average pairwise probability exceeds **0.9** and no cannot-link is violated. Average-link rather
|
||||
than single-link because single-link chains — one bad edge welds two identities together, which is
|
||||
the documented way face clustering fails on families.
|
||||
|
||||
**Incremental by default.** A new face joins the existing cluster whose average probability against
|
||||
it is highest, if that clears the threshold; otherwise it starts an unnamed group. A full
|
||||
re-agglomeration happens on model change, on recalibration, and on explicit user request. Suggestions
|
||||
are recomputed freely; confirmations survive all of it (FR-CULL-10).
|
||||
|
||||
**Splitting** re-agglomerates within one person at a raised threshold and offers the resulting groups
|
||||
as the split. FR-CULL-10 requires splitting to be as easy as merging, and a split that hands the user
|
||||
a pile of loose faces to re-sort is not that.
|
||||
|
||||
---
|
||||
|
||||
## 10. Catalog and jobs
|
||||
|
||||
**Schema** is catalog.md §10.1's v5 migration, plus `faces.crop_px` (§7) and the calibration table
|
||||
(§8.3). Nothing else changes.
|
||||
|
||||
**One new job kind:**
|
||||
|
||||
```rust
|
||||
/// Detect and embed faces in one image, from its proxy (FR-CULL-8).
|
||||
DetectFaces = 8,
|
||||
```
|
||||
|
||||
Background priority, coalesced per `image_id`, interruptible, resumable — it inherits FR-CAT-3's
|
||||
properties with no exceptions, which is what FR-CULL-8's acceptance criterion is about. It is not a
|
||||
network job.
|
||||
|
||||
One job does detection *and* embedding for every face in the image, rather than splitting them.
|
||||
Splitting would double the queue's row count for no benefit: the proxy is already decoded and in
|
||||
memory, and the natural unit of resumable work is one photograph.
|
||||
|
||||
**Selector term** (FR-CULL-11):
|
||||
|
||||
```rust
|
||||
Person { id: PersonId, include_suggested: bool }, // defaults to false
|
||||
```
|
||||
|
||||
Confirmed-only by default, so a saved smart collection does not silently change membership when a
|
||||
later indexing pass revises a guess.
|
||||
|
||||
---
|
||||
|
||||
## 11. Privacy obligations that constrain the code's shape
|
||||
|
||||
NFR-SEC-5 is not a policy to remember; it is a set of properties the code either has structurally or
|
||||
does not have at all. The checkable ones:
|
||||
|
||||
- **`dr-face` has no network dependency.** No `reqwest`, no `ureq`, transitively. Worth a CI check
|
||||
over `cargo tree`, alongside the existing lints — the crate that holds the embeddings should be
|
||||
provably unable to send them anywhere.
|
||||
- **The model fetch (§2.2) is in the app layer**, which is why the API in §3.1 takes bytes.
|
||||
- **The diagnostics bundle is an allowlist** (NFR-OPS-1), so `faces`, `face_person` and the
|
||||
calibration table are excluded by not being named, and a future table cannot become uploadable by
|
||||
existing.
|
||||
- **Delete-all is one transaction and one control**: faces, links, people, calibration, and the
|
||||
cached crops if any. Separately, a switch that stops indexing so no such data is produced. The two
|
||||
are different actions and are not collapsed into one.
|
||||
- **The about screen reads the model metadata from the loader** — id, version, licence — rather than
|
||||
from a hardcoded string that will drift from what is actually running.
|
||||
|
||||
---
|
||||
|
||||
## 12. What S14 measures
|
||||
|
||||
The corpus is a real personal library — the ~2,000-image sample S14 already specifies — with a
|
||||
hand-labelled identity subset. Fixed in advance, so the result is a measurement rather than an
|
||||
argument:
|
||||
|
||||
| # | Measure | Why it decides something |
|
||||
|---|---|---|
|
||||
| **M1** | **Does tract load both graphs?** As shipped, then with the input dims frozen | Go/no-go, and it is *first*. `det_500m.onnx` has a **dynamic H/W input** (§4.1), which is the exact thing tract failed on for YOLO and which OpenCV's `dnn` could not load either — so the as-shipped answer is expected to be no, and the real question is whether `make_dynamic_shape_fixed` is enough or whether a PyTorch re-export is needed. Costs an afternoon; everything below is void without it. |
|
||||
| **M2** | Detection latency per image at 640, and at 320, on the reference desktop and one Android device | Whether a 17k library indexes in a background sweep or a weekend. The one measured datapoint we have — YOLO26n-seg, ~470 ms at 640×640 in tract — suggests SCRFD-500M lands well under it, but tract is not ORT and an extrapolation is not a measurement. |
|
||||
| **M3** | Embedding latency per face, and faces per image in a real library | The multiplier on M2. A library averaging 1.5 faces per image at 100 ms per face is an hour for 25k faces; at 400 ms it is four. |
|
||||
| **M4** | Detection recall against hand-labelled faces, bucketed by `crop_px` | Where §4.3's floor should actually sit, and what proportion of a real library's faces are in the degraded bucket §7 predicts. |
|
||||
| **M5** | **Aligned versus unaligned embeddings**, same corpus, same clustering. Then two A/Bs that cost one run each: least-squares versus RANSAC alignment (§5), and `/128` versus `/127.5` normalisation (§6) | Quantifies §5. If the gap is small the alignment code is still correct; if it is large, this is the measurement that stops someone "simplifying" it later. The two A/Bs close divergences between the reference and the published Python that are currently settled by assertion. |
|
||||
| **M6** | Cluster purity and completeness against the labelled subset | Whether the subsystem is worth building at all. §1's F1 figures rank the models on *film frames against a known cast*; nothing yet says how MBF behaves on family snapshots it must cluster blind. |
|
||||
| **M7** | Calibration reliability, and **the sample size at which the fit becomes valid** | FR-CULL-9's acceptance criterion, plus the practical question of whether a 2,000-image library ever gets a valid fit or whether §8.3's "unavailable" is the normal state. |
|
||||
| **M8** | Burst-derived positive-pair purity (§8.1) | Whether the free positives are usable or the fit waits for user confirmations. |
|
||||
| **M9** | YuNet as a drop-in detector: M4 and M6, re-run | §2.3's question 3, and the model file is already on disk. If the answer is "close enough", half the licence problem disappears. Needs its own decode path — its outputs are not SCRFD's. |
|
||||
| **M10** | Peak RSS during an indexing sweep | NFR-RES-2. Two loaded graphs plus a proxy plus a batch of crops, on a phone. |
|
||||
|
||||
---
|
||||
|
||||
## 13. Order
|
||||
|
||||
1. **M1** — load both graphs in tract, freezing the input dims if needed (§4.1). Go/no-go, and it
|
||||
now comes first: it is an afternoon, and §2.3's reading is wasted effort if the graphs will not
|
||||
run at all. This is a change from the S14 brief's ordering, made because §1.1 removed the
|
||||
uncertainty that justified reading first — the models are known to work, so the open question is
|
||||
the runtime, not the choice.
|
||||
2. **Licence reading** (§2.3), in parallel with 3. Before anything *ships*, per D13.
|
||||
3. `dr-face` skeleton, `detect` + `align` + `embed`, ported from §1.1's table, with an example binary
|
||||
that draws boxes and landmarks on a JPEG — the same shape as `dr-segment`'s `examples/detect.rs`,
|
||||
and for the same reason: the thing worth looking at is whether the landmarks land on a real
|
||||
photograph. Port `tests/test_face_utils.cpp`'s cases first; they are model-free and they fail
|
||||
loudly on exactly the mistakes §5 describes.
|
||||
4. M2–M5 on the corpus. Alignment is validated here, before anything depends on embedding quality.
|
||||
5. `calibrate` + `cluster` against the labelled subset. M6–M8. The Platt fit ports from
|
||||
`gallery_calibration.hpp`; the pair *sourcing* (§8.1) is new and is the part to get wrong.
|
||||
6. The v5 migration, the `DetectFaces` job, the debounced clustering pass.
|
||||
7. UI: People view, confirm and reject, merge and split, the `Person` selector term.
|
||||
8. The route-C first-run flow, the about-screen entry, and the delete-all control — with the feature
|
||||
shipped disabled until they exist.
|
||||
|
||||
Steps 1–5 are the spike. Steps 6–8 are the build, and they are only justified if §12 says so.
|
||||
|
||||
---
|
||||
|
||||
## 14. What this does not settle
|
||||
|
||||
- **Whether a permissive model pair exists.** §2.3. If it does, route B replaces route C and step 8's
|
||||
first-run flow shrinks to nothing.
|
||||
- **Whether tract runs these graphs at all.** §12 M1, and §4.1 says why the answer is in real doubt.
|
||||
Every other line of this document is conditional on it.
|
||||
- **Faces in trashed images.** catalog.md §10.4's open question, unchanged: probably excluded from
|
||||
suggestions but not deleted, so a restore does not re-index.
|
||||
- **Whether embeddings sync.** NFR-SEC-5 permits it, opt-in, separately consented. Nothing here
|
||||
depends on the answer.
|
||||
- **Re-embedding at higher resolution.** §7's `crop_px` makes it a query rather than a full re-index,
|
||||
but whether it is worth doing is an M4 question.
|
||||
- **Approximate nearest neighbours.** §9 says brute force until measured otherwise. A 100k-face
|
||||
library is where this stops being true.
|
||||
|
||||
---
|
||||
|
||||
## 15. Register entries
|
||||
|
||||
**D13** — *face inference runtime and model licensing* · the runtime half stays answered (`ort` +
|
||||
`ort-tract`, unchanged since 2026-08-21). **The licensing half is answered conditionally by §2:**
|
||||
route C — ship the code, not the weights — unblocks the build without breaking GPL-3.0 or any
|
||||
distribution channel, and §2.3's reading may yet convert it to route B. Closing D13 outright waits on
|
||||
that reading.
|
||||
|
||||
**D17** — *face model pair and distribution route* · **PROPOSED**. SCRFD-500M for detection,
|
||||
MobileFaceNet/ArcFace for embedding, weights obtained by the user rather than redistributed, with a
|
||||
pinned checksum and a licence notice shown before the first fetch. The alternative considered and
|
||||
rejected is committing the InsightFace weights (§2.1 route A), which is not available at any price.
|
||||
The model *pair* is better evidenced than a proposal usually is — §1's table is a measured comparison
|
||||
over a 2,418-identity gallery, not a literature reading — so the open half of this decision is the
|
||||
route, not the pair. Relates to: D13, D14, NFR-COMPAT-2, FR-CULL-8.
|
||||
|
||||
**D18** — *porting from `scene-actor-extraction`* · **PROPOSED, and the easy half of a decision**.
|
||||
That project is MIT and by this project's author, so MIT-into-GPL-3.0-or-later is a compatible
|
||||
one-way combination requiring attribution, not permission. Ported files carry a header naming the
|
||||
origin. Worth recording because "we already have this working in another language" is exactly the
|
||||
provenance that goes undocumented and then cannot be answered three years later.
|
||||
|
||||
**S14** — *face pipeline in Rust* · scope sharpened, and **substantially de-risked**, by this
|
||||
document. §12 replaces "measure per-image latency, cluster purity and calibration convergence" with
|
||||
ten specific measures. Two changes to the brief itself: its instruction to resolve the licence
|
||||
question *before writing any of it* is relaxed to "before shipping any of it", because M1 is an
|
||||
afternoon and is worth knowing first (§13); and its central question — whether the pipeline works at
|
||||
all — is largely answered in advance by §1.1, leaving the runtime and the domain shift from film
|
||||
frames to family snapshots as the real unknowns.
|
||||
|
||||
---
|
||||
|
||||
## 16. Requirements touched
|
||||
|
||||
| ID | How this document addresses it |
|
||||
|---|---|
|
||||
| FR-CULL-8 | §4 detection, §6 embedding, §7 the proxy tier and its consequences, §10 the `DetectFaces` job |
|
||||
| FR-CULL-9 | §8 — pairs, fit, validity, and the reliability-diagram acceptance test |
|
||||
| FR-CULL-10 | §9 constrained agglomeration, confirmations as anchors, split by re-agglomeration |
|
||||
| FR-CULL-11 | §10 the `Person` selector term, confirmed-only by default |
|
||||
| FR-CULL-12 | §10 schema unchanged from catalog.md §10.1: embeddings derived, names to the sidecar |
|
||||
| NFR-SEC-5 | §11 — the obligations restated as structural properties, one of them CI-checkable |
|
||||
| NFR-COMPAT-2 | §2 — why the obvious weights cannot ship, and what does instead |
|
||||
| NFR-RES-2 | §1 the cheap model pair, §12 M2/M3/M10 on a phone |
|
||||
| NFR-ARCH-2 | §10 background priority, preempted by visible work |
|
||||
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 227 |
|
||||
| TRACES tags found | 614 |
|
||||
| TRACES tags found | 621 |
|
||||
| Requirements defined | 177 |
|
||||
| Requirements covered | 91 |
|
||||
| **Coverage** | **51.4%** (91/177) |
|
||||
@@ -46,27 +46,27 @@ _None._
|
||||
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:840`](../core/dr-catalog/src/schema.rs#L840), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3055`](../ui/dr-ui/src/library.rs#L3055), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1212`](../ui/dr-ui/ui/library.slint#L1212), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) |
|
||||
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3301`](../ui/dr-ui/src/library.rs#L3301), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:461`](../ui/dr-ui/ui/app.slint#L461), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/app.slint:826`](../ui/dr-ui/ui/app.slint#L826), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1278`](../ui/dr-ui/ui/library.slint#L1278), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) |
|
||||
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2516`](../ui/dr-ui/ui/library.slint#L2516), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) |
|
||||
| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2757`](../ui/dr-ui/src/develop.rs#L2757), [`ui/dr-ui/src/develop.rs:2785`](../ui/dr-ui/src/develop.rs#L2785), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/lib.rs:1579`](../ui/dr-ui/src/lib.rs#L1579), [`ui/dr-ui/src/lib.rs:1684`](../ui/dr-ui/src/lib.rs#L1684), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:849`](../ui/dr-ui/src/lib.rs#L849), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:2790`](../ui/dr-ui/src/develop.rs#L2790), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/lib.rs:1579`](../ui/dr-ui/src/lib.rs#L1579), [`ui/dr-ui/src/lib.rs:1684`](../ui/dr-ui/src/lib.rs#L1684), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:849`](../ui/dr-ui/src/lib.rs#L849), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:394`](../core/dr-catalog/src/schema.rs#L394), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2325`](../ui/dr-ui/src/develop.rs#L2325), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3408`](../ui/dr-ui/src/library.rs#L3408), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3329`](../ui/dr-ui/src/library_ui.rs#L3329), [`ui/dr-ui/src/library_ui.rs:3517`](../ui/dr-ui/src/library_ui.rs#L3517), [`ui/dr-ui/src/library_ui.rs:3635`](../ui/dr-ui/src/library_ui.rs#L3635), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5222`](../ui/dr-ui/src/library_ui.rs#L5222), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) |
|
||||
| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
|
||||
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) |
|
||||
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) |
|
||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:154`](../core/dr-pipeline/src/graph.rs#L154), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:1399`](../core/dr-pipeline/src/sidecar.rs#L1399), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2757`](../ui/dr-ui/src/develop.rs#L2757), [`ui/dr-ui/src/develop.rs:3189`](../ui/dr-ui/src/develop.rs#L3189), [`ui/dr-ui/src/develop.rs:3243`](../ui/dr-ui/src/develop.rs#L3243), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:3337`](../ui/dr-ui/src/develop.rs#L3337), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1275`](../ui/dr-ui/src/lib.rs#L1275), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/app.slint:913`](../ui/dr-ui/ui/app.slint#L913) |
|
||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:154`](../core/dr-pipeline/src/graph.rs#L154), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:1399`](../core/dr-pipeline/src/sidecar.rs#L1399), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1275`](../ui/dr-ui/src/lib.rs#L1275), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/app.slint:913`](../ui/dr-ui/ui/app.slint#L913) |
|
||||
| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:18`](../core/dr-pipeline/src/graph.rs#L18), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:579`](../ui/dr-ui/src/lib.rs#L579) |
|
||||
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) |
|
||||
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3916`](../ui/dr-ui/src/develop.rs#L3916) |
|
||||
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3921`](../ui/dr-ui/src/develop.rs#L3921) |
|
||||
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:433`](../core/dr-gpu/tests/capture_sharpen.rs#L433), [`core/dr-gpu/tests/detail_stage.rs:241`](../core/dr-gpu/tests/detail_stage.rs#L241), [`core/dr-gpu/tests/local_contrast.rs:475`](../core/dr-gpu/tests/local_contrast.rs#L475), [`core/dr-gpu/tests/noise_reduction.rs:555`](../core/dr-gpu/tests/noise_reduction.rs#L555), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:486`](../core/dr-pipeline/src/graph.rs#L486), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) |
|
||||
| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:1491`](../core/dr-pipeline/src/operation.rs#L1491), [`core/dr-pipeline/src/operation.rs:1516`](../core/dr-pipeline/src/operation.rs#L1516), [`core/dr-pipeline/src/operation.rs:1531`](../core/dr-pipeline/src/operation.rs#L1531), [`core/dr-pipeline/src/operation.rs:1555`](../core/dr-pipeline/src/operation.rs#L1555), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:549`](../core/dr-pipeline/src/operation.rs#L549) |
|
||||
| FR-DEV-3f | [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:167`](../core/dr-film/src/profile.rs#L167), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:110`](../core/dr-pipeline/src/graph.rs#L110), [`core/dr-pipeline/src/graph.rs:292`](../core/dr-pipeline/src/graph.rs#L292), [`core/dr-pipeline/src/graph.rs:96`](../core/dr-pipeline/src/graph.rs#L96), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:120`](../core/dr-pipeline/src/ops/film_sim.rs#L120), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:291`](../core/dr-pipeline/src/ops/film_sim.rs#L291), [`core/dr-pipeline/src/ops/film_sim.rs:96`](../core/dr-pipeline/src/ops/film_sim.rs#L96), [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:1647`](../core/dr-pipeline/src/sidecar.rs#L1647), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:415`](../core/dr-pipeline/src/sidecar.rs#L415), [`core/dr-pipeline/src/sidecar.rs:555`](../core/dr-pipeline/src/sidecar.rs#L555), [`core/dr-pipeline/src/sidecar.rs:678`](../core/dr-pipeline/src/sidecar.rs#L678), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2480`](../ui/dr-ui/src/develop.rs#L2480), [`ui/dr-ui/src/develop.rs:2794`](../ui/dr-ui/src/develop.rs#L2794), [`ui/dr-ui/src/lib.rs:1908`](../ui/dr-ui/src/lib.rs#L1908), [`ui/dr-ui/src/lib.rs:512`](../ui/dr-ui/src/lib.rs#L512), [`ui/dr-ui/src/lib.rs:570`](../ui/dr-ui/src/lib.rs#L570), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707) |
|
||||
| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:110`](../core/dr-pipeline/src/graph.rs#L110), [`core/dr-pipeline/src/graph.rs:292`](../core/dr-pipeline/src/graph.rs#L292), [`core/dr-pipeline/src/graph.rs:96`](../core/dr-pipeline/src/graph.rs#L96), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:1647`](../core/dr-pipeline/src/sidecar.rs#L1647), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:415`](../core/dr-pipeline/src/sidecar.rs#L415), [`core/dr-pipeline/src/sidecar.rs:555`](../core/dr-pipeline/src/sidecar.rs#L555), [`core/dr-pipeline/src/sidecar.rs:678`](../core/dr-pipeline/src/sidecar.rs#L678), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2485`](../ui/dr-ui/src/develop.rs#L2485), [`ui/dr-ui/src/develop.rs:2799`](../ui/dr-ui/src/develop.rs#L2799), [`ui/dr-ui/src/lib.rs:1908`](../ui/dr-ui/src/lib.rs#L1908), [`ui/dr-ui/src/lib.rs:512`](../ui/dr-ui/src/lib.rs#L512), [`ui/dr-ui/src/lib.rs:570`](../ui/dr-ui/src/lib.rs#L570), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707) |
|
||||
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336) |
|
||||
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
||||
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2812`](../ui/dr-ui/src/develop.rs#L2812), [`ui/dr-ui/src/develop.rs:2822`](../ui/dr-ui/src/develop.rs#L2822), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1267`](../ui/dr-ui/src/lib.rs#L1267) |
|
||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2751`](../ui/dr-ui/src/develop.rs#L2751), [`ui/dr-ui/src/develop.rs:2772`](../ui/dr-ui/src/develop.rs#L2772), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
|
||||
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2817`](../ui/dr-ui/src/develop.rs#L2817), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1267`](../ui/dr-ui/src/lib.rs#L1267) |
|
||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
|
||||
| FR-DEV-8 | [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299) |
|
||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:427`](../core/dr-pipeline/src/graph.rs#L427), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2978`](../ui/dr-ui/src/develop.rs#L2978), [`ui/dr-ui/src/develop.rs:3461`](../ui/dr-ui/src/develop.rs#L3461), [`ui/dr-ui/src/develop.rs:3495`](../ui/dr-ui/src/develop.rs#L3495), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) |
|
||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:427`](../core/dr-pipeline/src/graph.rs#L427), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) |
|
||||
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) |
|
||||
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4563`](../ui/dr-ui/src/develop.rs#L4563), [`ui/dr-ui/src/develop.rs:4595`](../ui/dr-ui/src/develop.rs#L4595), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1326`](../ui/dr-ui/src/lib.rs#L1326), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:276`](../ui/dr-ui/ui/app.slint#L276), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
|
||||
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4568`](../ui/dr-ui/src/develop.rs#L4568), [`ui/dr-ui/src/develop.rs:4600`](../ui/dr-ui/src/develop.rs#L4600), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1326`](../ui/dr-ui/src/lib.rs#L1326), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:276`](../ui/dr-ui/ui/app.slint#L276), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
|
||||
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2400`](../core/dr-gpu/src/adjust.rs#L2400), [`core/dr-pipeline/src/graph.rs:417`](../core/dr-pipeline/src/graph.rs#L417), [`core/dr-pipeline/src/graph.rs:470`](../core/dr-pipeline/src/graph.rs#L470), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
| FR-EXP-3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
|
||||
Reference in New Issue
Block a user