Charge a colour mode for its rarity, and keep the verdict
The refinement worked and could not be controlled. Pruning modes below a share threshold made the flag's removal a *discrete* event: below the line its Mahalanobis distance was enormous and nothing rescued it, above the line it sat at zero and nothing removed it. A control over that would appear dead through most of its travel and then start eating sky. So the prune is gone. A mode is charged `−ln(share × k)` nats, floored at zero, and that cost enters both tests — doubled in the chi-square, which is a squared distance, and directly in the log density. Rarity becomes a distance rather than a threshold, and the things a photographer wants to remove separate along it. Measured on the synthetic frame the tests build: a flag holding 1.6% of the sky is more than half gone by **2.95 nats** and a cloud bank holding a third of it survives to **5.75**. The whole interval between them is somewhere a control can sit. `the_flag_goes_before_the_cloud_does` pins the ordering, which is the property that makes one slider worth offering at all. Measured against an even split rather than against one, so raising `clusters` describes a category more finely without making every colour in it look rarer. Floored at zero so a dominant mode earns no *discount* — a bonus there would let the commonest colour outvote a bad chi-square, which is the one direction this must not bend. `Refinement` holds the per-pixel verdict, quantised to a byte over ±16 nats — an eighth of a nat per step, far finer than the narrowest transition the gate can be asked for, and the same size as the coverage buffer it sits beside. `apply` is then a smoothstep, and the model is never consulted again. That is `distance.rs`'s arrangement deliberately: there a signed distance field is computed once and feather, grow and shrink become arithmetic on it, "which is what makes those live controls rather than ones that stall on every drag". Same shape, different field. The blur moved with it, from the gate to the verdict. Smoothing the evidence rather than the decision means it is paid for once in `compute` instead of on every frame of a drag, and it is the better thing to smooth in any case. `apply` at `STRICTNESS_OFF` returns the weights untouched without reading the verdict at all. A control whose off position is *very nearly* the unrefined mask cannot answer "is this helping"; one whose off position is the unrefined mask can. `strictness_zero_changes_nothing` holds it to that, and `strictness_is_monotonic` holds the rest of the travel to only ever removing more — a slider that gave weight back partway up would be one whose direction nobody could predict. The synthetic sky is smooth enough to sit on `VARIANCE_FLOOR`, where a real one has noise and therefore a real spread, which moves every crossing down together. The ordering survives that; the placement is a calibration. Which is the honest argument for a control rather than a constant, and why the default sits at half scale instead of at the flag's measured crossing. The example sweeps the whole range and writes a frame per nat, because the question a photographer asks of a slider is where to put it, and that needs the travel rather than a point on it. Verified: fmt clean, clippy -D warnings clean, 60 dr-segment tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,13 +24,21 @@
|
||||
//! came from* rather than as an abstract grey field. PPM for the same reason
|
||||
//! the other examples use it: no encoder dependency, and every viewer reads it.
|
||||
//!
|
||||
//! And `<prefix>-<category>-refined.ppm` beside it, which is the same category
|
||||
//! after [`dr_segment::refine_category`] has cut it back to the pixels whose
|
||||
//! colour agrees with it. Both, never one: whether that refinement is an
|
||||
//! improvement is a comparative judgement — did the flag come out of the sky,
|
||||
//! and is the sky still there afterwards — and a single image cannot answer
|
||||
//! it. The percentage printed beside each is how much weight came off, which
|
||||
//! is the number to be suspicious of when it is large.
|
||||
//! And `<prefix>-<category>-refined-<strictness>.ppm` beside it — the same
|
||||
//! category after [`dr_segment::Refinement`] has cut it back to the pixels
|
||||
//! whose colour agrees with it, at each point of a sweep across the whole
|
||||
//! strictness range the application's slider offers.
|
||||
//!
|
||||
//! A sweep rather than one image, for two reasons. Whether the refinement is
|
||||
//! an improvement is a comparative judgement — did the flag come out of the
|
||||
//! sky, and is the sky still there afterwards — which a single image cannot
|
||||
//! answer. And the control is a slider, so the question actually being asked
|
||||
//! is *where to put it*, which needs the travel rather than a point on it.
|
||||
//!
|
||||
//! The percentage printed beside each is how much weight came off, which is
|
||||
//! the number to be suspicious of when it grows quickly. The timing beside
|
||||
//! that is the fit against one `apply`, and it is the measurement that decides
|
||||
//! whether the slider can be a live drag.
|
||||
//!
|
||||
//! Timings are reported as a median over the requested run count, with the
|
||||
//! first run excluded. That first pass pays for tract's lazy allocation and is
|
||||
@@ -122,8 +130,13 @@ fn main() {
|
||||
// judgement this example exists to support is comparative — is the
|
||||
// flag out, and is the sky still there — and it cannot be made from
|
||||
// one image.
|
||||
//
|
||||
// Swept rather than shown once, because the application offers this as
|
||||
// a slider and the question a photographer will actually ask is "where
|
||||
// do I put it": the whole travel of the control, at the resolution
|
||||
// their own eye will judge it at.
|
||||
let start = Instant::now();
|
||||
let (refined, what) = dr_segment::refine_category(
|
||||
let refinement = dr_segment::Refinement::compute(
|
||||
&mask,
|
||||
&rgb,
|
||||
width,
|
||||
@@ -131,28 +144,54 @@ fn main() {
|
||||
scene.cell_pixels(),
|
||||
&dr_segment::RefineOptions::default(),
|
||||
);
|
||||
let took = start.elapsed();
|
||||
match what {
|
||||
dr_segment::Refined::Applied { removed } => {
|
||||
println!(
|
||||
" refined in {took:?}: {:.1}% of the weight cut",
|
||||
removed * 100.0
|
||||
);
|
||||
write_overlay(
|
||||
&format!("{prefix}-{name}-refined.ppm"),
|
||||
&rgb,
|
||||
&refined,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
dr_segment::Refined::Skipped(why) => {
|
||||
let fit = start.elapsed();
|
||||
|
||||
let refinement = match refinement {
|
||||
Ok(r) => r,
|
||||
Err(why) => {
|
||||
println!(" left coarse: {why:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
println!(" fitted in {fit:?}");
|
||||
|
||||
let total: f32 = mask.iter().sum();
|
||||
for step in 0..=STRICTNESS_STEPS {
|
||||
let strictness = step as f32 * dr_segment::STRICTNESS_MAX / STRICTNESS_STEPS as f32;
|
||||
|
||||
// Timed separately, and this is the number that decides whether
|
||||
// the control can be a live drag or has to wait for the release.
|
||||
let start = Instant::now();
|
||||
let refined = refinement.apply(&mask, strictness);
|
||||
let took = start.elapsed();
|
||||
|
||||
let cut = if total > 0.0 {
|
||||
(total - refined.iter().sum::<f32>()) / total
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
println!(
|
||||
" strictness {strictness:>4.1}: {:5.1}% cut ({took:?})",
|
||||
cut * 100.0
|
||||
);
|
||||
write_overlay(
|
||||
&format!("{prefix}-{name}-refined-{strictness:.1}.ppm"),
|
||||
&rgb,
|
||||
&refined,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Points on the strictness sweep, over `0..=STRICTNESS_MAX`.
|
||||
///
|
||||
/// Eight, so the files come out at whole nats from `0` to `8` — few enough to
|
||||
/// look at every one of them, which is the point of writing them at all, and
|
||||
/// spaced at the unit the control is actually denominated in.
|
||||
const STRICTNESS_STEPS: usize = 8;
|
||||
|
||||
#[cfg(feature = "embedded-scene-model")]
|
||||
fn embedded() -> SceneModel {
|
||||
SceneModel::embedded().expect("could not load the embedded scene model")
|
||||
|
||||
@@ -64,7 +64,10 @@ pub mod semantic;
|
||||
pub use distance::{signed_distance, Falloff, Morphology, Shaped};
|
||||
pub use hierarchy::{Edge, Merge, MergeTree, RegionField};
|
||||
pub use prior::{Membership, PriorOptions};
|
||||
pub use refine::{refine_category, RefineOptions, Refined, SkipReason};
|
||||
pub use refine::{
|
||||
refine_category, RefineOptions, Refined, Refinement, SkipReason, STRICTNESS_DEFAULT,
|
||||
STRICTNESS_MAX, STRICTNESS_OFF,
|
||||
};
|
||||
#[cfg(feature = "semantic")]
|
||||
pub use scene::{Category, Scene, SceneModel};
|
||||
#[cfg(feature = "semantic")]
|
||||
|
||||
+520
-281
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user