Index the library's faces, and group them into people
Wires dr-face to dr-catalog: a background sweep that reads the proxy the grid already built, detects, aligns, embeds and stores, then a clustering pass that turns those embeddings into suggested people. Detection runs on the Large thumbnail tier and nowhere else. That is what makes the feature affordable -- a browsed library has already paid for its proxies, so face indexing adds no RAW decode that was not already happening -- and it is why an image whose proxy is missing is skipped rather than fetched: requesting one here would put face indexing on the network path FR-CULL-8 keeps it off. The sweep keeps no cursor. It asks the catalog what is missing, so it resumes after process death with no repeated work beyond the in-flight image, and cancelling is dropping the receiver. recluster writes only the suggested half. Confirmed faces go in as anchors and come back untouched, and a cluster of one stays nameless -- naming every stray face would fill the People view with noise the user then has to dismiss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,16 @@ pub enum JobKind {
|
||||
FetchPreview = 6,
|
||||
/// Fetch a full original: pinned by rule, or explicitly asked for.
|
||||
FetchOriginal = 7,
|
||||
/// Detect and embed the faces in one image (FR-CULL-8).
|
||||
///
|
||||
/// One job does both, rather than splitting them: the proxy is already
|
||||
/// decoded and in memory, and the natural unit of resumable work is one
|
||||
/// photograph. Splitting would double the queue's row count for nothing.
|
||||
///
|
||||
/// Runs against the proxy tier, never a full decode — a library that has
|
||||
/// been browsed has already paid for its proxies, so face indexing adds no
|
||||
/// RAW decodes that were not already happening.
|
||||
DetectFaces = 8,
|
||||
}
|
||||
|
||||
impl JobKind {
|
||||
@@ -48,6 +58,7 @@ impl JobKind {
|
||||
5 => JobKind::ContentHash,
|
||||
6 => JobKind::FetchPreview,
|
||||
7 => JobKind::FetchOriginal,
|
||||
8 => JobKind::DetectFaces,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -209,13 +209,13 @@ impl Detector {
|
||||
// left/top/right/bottom in units of the stride.
|
||||
let (cx, cy) = ((c * stride) as f32, (r * stride) as f32);
|
||||
let b = &boxes[idx * 4..idx * 4 + 4];
|
||||
let (x0, y0) = lb.to_source(cx - b[0] * s, cy - b[1] * s);
|
||||
let (x1, y1) = lb.to_source(cx + b[2] * s, cy + b[3] * s);
|
||||
let (x0, y0) = lb.into_source(cx - b[0] * s, cy - b[1] * s);
|
||||
let (x1, y1) = lb.into_source(cx + b[2] * s, cy + b[3] * s);
|
||||
|
||||
let k = &kps[idx * 10..idx * 10 + 10];
|
||||
let mut landmarks = [(0.0_f32, 0.0_f32); 5];
|
||||
for (p, lm) in landmarks.iter_mut().enumerate() {
|
||||
*lm = lb.to_source(cx + k[p * 2] * s, cy + k[p * 2 + 1] * s);
|
||||
*lm = lb.into_source(cx + k[p * 2] * s, cy + k[p * 2 + 1] * s);
|
||||
}
|
||||
|
||||
raw.push(Detection {
|
||||
@@ -340,7 +340,7 @@ impl Letterbox {
|
||||
}
|
||||
|
||||
/// Input-space point back to source pixels.
|
||||
fn to_source(&self, x: f32, y: f32) -> (f32, f32) {
|
||||
fn into_source(self, x: f32, y: f32) -> (f32, f32) {
|
||||
((x - self.pad_x) / self.scale, (y - self.pad_y) / self.scale)
|
||||
}
|
||||
}
|
||||
@@ -353,7 +353,7 @@ mod tests {
|
||||
fn letterbox_round_trips_a_point() {
|
||||
let lb = Letterbox::fit(1024.0, 683.0);
|
||||
for &(x, y) in &[(0.0_f32, 0.0_f32), (512.0, 341.0), (1023.0, 682.0)] {
|
||||
let (bx, by) = lb.to_source(x * lb.scale + lb.pad_x, y * lb.scale + lb.pad_y);
|
||||
let (bx, by) = lb.into_source(x * lb.scale + lb.pad_x, y * lb.scale + lb.pad_y);
|
||||
assert!((bx - x).abs() < 1e-2, "{bx} vs {x}");
|
||||
assert!((by - y).abs() < 1e-2, "{by} vs {y}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user