fix(photos): preserve tapered tips and dark live edge (#73 #74)

Lower the axial body threshold while retaining fringe rejection, minimize length-only tip inset, and make sheet sensitivity curves testable and conservative at zero.
This commit is contained in:
2026-09-16 15:18:06 +00:00
parent 037a4b870a
commit 03d3fcf460
5 changed files with 281 additions and 36 deletions
+59
View File
@@ -0,0 +1,59 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { blackKeep, greenKeep } from './sheetKeep.ts';
// --- Black sheet sensitivity (#74) ---
test('blackKeep: s=0 keeps dark live-edge wood (lum ~25–50), removes near-black sheet (#74)', () => {
// Near-black sheet (lum ≈ 5)
assert.equal(blackKeep(5, 5, 5, 0), false, 'near-black sheet removed at s=0');
assert.equal(blackKeep(0, 0, 0, 0), false, 'pure black sheet removed at s=0');
// Dark wood / bark / live edge
assert.equal(blackKeep(25, 22, 18, 0), true, 'lum~22 dark wood kept at s=0');
assert.equal(blackKeep(40, 35, 30, 0), true, 'lum~35 dark wood kept at s=0');
assert.equal(blackKeep(50, 45, 40, 0), true, 'lum~46 wood kept at s=0');
// Mid wood always kept
assert.equal(blackKeep(120, 90, 60, 0), true, 'mid wood kept at s=0');
});
test('blackKeep: s=100 removes more than s=0; curve monotonic (#74)', () => {
// Dark-ish pixel that sits between floors: sheet at high s, wood at low s
const darkish = { r: 80, g: 70, b: 60 }; // lum ≈ 72
assert.equal(blackKeep(darkish.r, darkish.g, darkish.b, 0), true, 'lum~72 kept at s=0');
assert.equal(blackKeep(darkish.r, darkish.g, darkish.b, 100), false, 'lum~72 sheet at s=100');
// Near-black always sheet
assert.equal(blackKeep(2, 2, 2, 0), false);
assert.equal(blackKeep(2, 2, 2, 100), false);
// Bright wood always kept
assert.equal(blackKeep(200, 160, 100, 0), true);
assert.equal(blackKeep(200, 160, 100, 100), true);
// Monotonic: once removed, stays removed as s increases
const probe = { r: 60, g: 55, b: 50 }; // lum ≈ 56
let prevKeep = true;
for (const s of [0, 25, 50, 75, 100]) {
const keep = blackKeep(probe.r, probe.g, probe.b, s);
if (!prevKeep) assert.equal(keep, false, `once sheet at lower s, stay sheet at s=${s}`);
prevKeep = keep;
}
// s=100 must remove more pixels than s=0 for this mid-dark band
assert.equal(blackKeep(probe.r, probe.g, probe.b, 0), true);
assert.equal(blackKeep(probe.r, probe.g, probe.b, 100), false);
});
test('greenKeep: s=0 does not eat mild wood greens; s=100 widens sheet match (#74)', () => {
// Neutral wood
assert.equal(greenKeep(140, 110, 80, 0), true);
assert.equal(greenKeep(140, 110, 80, 100), true);
// Mild green cast on wood (g - max(r,b) ≈ 5) — must keep at s=0
assert.equal(greenKeep(100, 105, 90, 0), true, 'mild green cast kept at s=0');
// Strong sheet green
assert.equal(greenKeep(40, 180, 50, 0), false, 'strong green sheet removed at s=0');
assert.equal(greenKeep(40, 180, 50, 100), false);
// Borderline: g-max ≈ 20 — may become sheet at high s
const border = { r: 80, g: 100, b: 70 }; // delta = 20
assert.equal(greenKeep(border.r, border.g, border.b, 0), true, 'delta20 kept at s=0');
assert.equal(greenKeep(border.r, border.g, border.b, 100), false, 'delta20 sheet at s=100');
});
+36
View File
@@ -0,0 +1,36 @@
/**
* Sheet-vs-slab keep predicates used by the mask worker.
* Extracted so unit tests can cover sensitivity curves without the worker.
*/
/** Keep slab pixels on a green sheet. Higher s widens the green-sheet match. */
export function greenKeep(r: number, g: number, b: number, s: number): boolean {
const t = Math.max(0, Math.min(100, s)) / 100;
// s=0 removes only strongly green sheet; increasing sensitivity widens the match.
const isSheet = g - Math.max(r, b) > 45 - 35 * t;
return !isSheet;
}
/**
* Keep slab pixels on a black sheet.
* At s=0 only near-black sheet (very low lum) is removed so dark live-edge /
* bark (lum ~25–50) stays. The match widens monotonically toward s=100 (#74).
*/
export function blackKeep(r: number, g: number, b: number, s: number): boolean {
const t = Math.max(0, Math.min(100, s)) / 100;
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
// Old floor 40 ate dark wood at minimum sensitivity. Floor 10 isolates near-black.
const isSheet = lum < 10 + 140 * t;
return !isSheet;
}
export function autoPickSheet(
samples: { r: number; g: number; b: number }[],
): 'green' | 'black' {
let gr = 0, gl = 0;
for (const p of samples) {
if (p.g - Math.max(p.r, p.b) > 20) gr++;
if (0.2126 * p.r + 0.7152 * p.g + 0.0722 * p.b < 70) gl++;
}
return gl >= gr ? 'black' : 'green';
}