Measure the faces already found rather than finding them again
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m13s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 54s
Build and test / Layer separation (push) Failing after 1s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 2s
Traceability / Requirement traces (push) Successful in 52s
Build and test / Android (aarch64) (push) Successful in 30m6s
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m13s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 54s
Build and test / Layer separation (push) Failing after 1s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 2s
Traceability / Requirement traces (push) Successful in 52s
Build and test / Android (aarch64) (push) Successful in 30m6s
Every face stored before its quality was kept holds a unit vector, and V14 forgot the run marker of each image holding one so that the next sweep would look again. Looking again meant detecting again: a whole re-detection per image, with every suggestion on it thrown away and the confirmations carried across by box overlap, to recover one number. The sweep now has a measuring pass between the proxy repair and the un-indexed images. It lists every image holding an unmeasured face, fetches the original once, warps each stored face from the landmarks it already has, embeds it, and writes the raw vector and its length over the old row. Ids, boxes and identities are untouched; the marker is re-written fresh so the sync exports the measured vectors. A face whose landmarks no longer make a warp is dropped, as detection would have refused to store it. `faces_unindexed` leaves those images to the measuring pass, so the V14 deletion no longer costs a second detection.
This commit is contained in:
+225
-10
@@ -3297,6 +3297,14 @@ fn flush_sweep(catalog: &Catalog, found: &mut Vec<MetadataFound>) {
|
||||
/// so an image with no proxy is work to be done rather than work to be skipped
|
||||
/// — which is the whole difference between indexing a library and indexing the
|
||||
/// fraction of it that has been browsed.
|
||||
///
|
||||
/// **An image whose faces are merely unmeasured is not here.** Schema V14
|
||||
/// forgot the run marker of every such image so that *something* would look
|
||||
/// at it again; [`faces_unmeasured`] is that something, and it does the cheap
|
||||
/// thing — embed the faces already found — where this list would have the
|
||||
/// whole detection run again. Both lists are drawn from the same catalog in
|
||||
/// the same sweep, so the exclusion is in the query rather than left to the
|
||||
/// caller to remember.
|
||||
fn faces_unindexed(
|
||||
catalog: &Catalog,
|
||||
model_id: &str,
|
||||
@@ -3310,6 +3318,10 @@ fn faces_unindexed(
|
||||
SELECT 1 FROM face_index fi
|
||||
WHERE fi.image_id = i.id AND fi.model_id = ?1
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM faces f
|
||||
WHERE f.image_id = i.id AND f.model_id = ?1 AND f.quality IS NULL
|
||||
)
|
||||
ORDER BY i.id"
|
||||
))?;
|
||||
let rows = stmt
|
||||
@@ -3382,6 +3394,60 @@ fn faces_without_proxy(
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// TRACES: FR-CULL-9
|
||||
/// Images holding a face this model found before its quality was kept.
|
||||
///
|
||||
/// The work list of the sweep's measuring pass: every stored face whose vector
|
||||
/// is a unit one (schema V14) is embedded again from the native render, with
|
||||
/// the landmarks it already has, and the raw vector and its length written
|
||||
/// over it. Nothing is re-detected and no face changes identity — see
|
||||
/// `dr_catalog::faces::record_measurements`.
|
||||
///
|
||||
/// Costs what the indexing pass costs per image, an original fetched and
|
||||
/// rendered, because the length exists only at the moment of embedding and
|
||||
/// there is no embedding without the pixels. What it saves is the detector,
|
||||
/// and — the part that matters — every suggestion and confirmation on those
|
||||
/// faces, which a re-detection would rebuild from box overlap.
|
||||
fn faces_unmeasured(
|
||||
catalog: &Catalog,
|
||||
model_id: &str,
|
||||
) -> Result<Vec<ThumbnailRequest>, dr_catalog::CatalogError> {
|
||||
let mut stmt = catalog.connection().prepare(&format!(
|
||||
"SELECT DISTINCT i.id, i.source_ref, r.file_id, i.file_size
|
||||
FROM images i
|
||||
JOIN remote r ON r.image_id = i.id
|
||||
JOIN faces f ON f.image_id = i.id
|
||||
WHERE r.file_id IS NOT NULL AND {VISIBLE}
|
||||
AND f.model_id = ?1 AND f.quality IS NULL
|
||||
ORDER BY i.id"
|
||||
))?;
|
||||
let rows = stmt
|
||||
.query_map([model_id], |r| {
|
||||
Ok(ThumbnailRequest {
|
||||
full_resolution: true,
|
||||
thumb_size: dr_thumbs::ThumbSize::Large,
|
||||
row: 0,
|
||||
image_id: r.get(0)?,
|
||||
path: r.get(1)?,
|
||||
file_id: r.get::<_, Option<i64>>(2)?.map(|v| v as u64),
|
||||
size: r.get::<_, Option<i64>>(3)?.unwrap_or(0) as u64,
|
||||
needs_metadata: false,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// What the sweep does with one fetched original.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SweepWork {
|
||||
/// Detect and embed from scratch: an image never indexed, or one whose
|
||||
/// faces have nothing left to be cut from.
|
||||
Detect,
|
||||
/// Embed the faces already found, again — see [`faces_unmeasured`].
|
||||
Measure,
|
||||
}
|
||||
|
||||
/// TRACES: FR-CULL-8 | FR-EXP-9 | NFR-ARCH-2 | NFR-RES-2
|
||||
/// Index faces across the **whole** library, at native resolution.
|
||||
///
|
||||
@@ -3502,7 +3568,9 @@ pub fn spawn_face_sweep(
|
||||
// un-indexed ones. Appended instead, they sit two hours of fetching
|
||||
// down the queue and the screen stays empty for the whole session —
|
||||
// which is indistinguishable from the repair not existing.
|
||||
let mut wanted = match faces_without_proxy(&catalog, &store, &model_id) {
|
||||
let mut wanted: Vec<(ThumbnailRequest, SweepWork)> = match faces_without_proxy(
|
||||
&catalog, &store, &model_id,
|
||||
) {
|
||||
Ok(repair) => {
|
||||
if !repair.is_empty() {
|
||||
log::info!(
|
||||
@@ -3510,7 +3578,7 @@ pub fn spawn_face_sweep(
|
||||
repair.len()
|
||||
);
|
||||
}
|
||||
repair
|
||||
repair.into_iter().map(|r| (r, SweepWork::Detect)).collect()
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("face sweep: looking for orphaned faces: {e}");
|
||||
@@ -3518,10 +3586,35 @@ pub fn spawn_face_sweep(
|
||||
}
|
||||
};
|
||||
|
||||
// Disjoint from the above by construction: an image with faces recorded
|
||||
// is not an image with no `face_index` row.
|
||||
// Then the faces to measure again. Behind the proxy repair and ahead
|
||||
// of the rest for the same reason that one leads: these are faces the
|
||||
// People screen is showing and grouping *now*, on vectors the gallery
|
||||
// rule cannot act on. An image already queued for a full re-detection
|
||||
// gets its quality from that, so it is not queued twice.
|
||||
let mut queued: std::collections::HashSet<i64> =
|
||||
wanted.iter().map(|(r, _)| r.image_id).collect();
|
||||
match faces_unmeasured(&catalog, &model_id) {
|
||||
Ok(measure) => {
|
||||
let fresh: Vec<_> = measure
|
||||
.into_iter()
|
||||
.filter(|r| queued.insert(r.image_id))
|
||||
.collect();
|
||||
if !fresh.is_empty() {
|
||||
log::info!(
|
||||
"face sweep: measuring the faces on {} image(s) stored before their quality was kept",
|
||||
fresh.len()
|
||||
);
|
||||
}
|
||||
wanted.extend(fresh.into_iter().map(|r| (r, SweepWork::Measure)));
|
||||
}
|
||||
Err(e) => log::warn!("face sweep: looking for unmeasured faces: {e}"),
|
||||
}
|
||||
|
||||
// Disjoint from both of the above by construction: an image with faces
|
||||
// recorded is not an image with no `face_index` row, and one whose
|
||||
// faces are unmeasured is excluded by the query.
|
||||
match faces_unindexed(&catalog, &model_id) {
|
||||
Ok(fresh) => wanted.extend(fresh),
|
||||
Ok(fresh) => wanted.extend(fresh.into_iter().map(|r| (r, SweepWork::Detect))),
|
||||
Err(e) => {
|
||||
log::warn!("face sweep: {e}");
|
||||
if wanted.is_empty() {
|
||||
@@ -3587,7 +3680,7 @@ pub fn spawn_face_sweep(
|
||||
// multiply the one allocation that actually threatens the budget
|
||||
// while buying no parallelism that exists.
|
||||
for chunk in wanted.chunks(SWEEP_LANES) {
|
||||
let fetched = futures_join_all(chunk.iter().map(|req| {
|
||||
let fetched = futures_join_all(chunk.iter().map(|(req, work)| {
|
||||
let backend = &*backend;
|
||||
let pool = &pool;
|
||||
async move {
|
||||
@@ -3595,11 +3688,11 @@ pub fn spawn_face_sweep(
|
||||
Ok(h) => h,
|
||||
Err(e) if e.indicates_offline() => {
|
||||
log::info!("face sweep: {e}");
|
||||
return (req, Err(FetchOutcome::Offline));
|
||||
return (req, *work, Err(FetchOutcome::Offline));
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("face sweep: {}: {e}", req.path);
|
||||
return (req, Err(FetchOutcome::Failed));
|
||||
return (req, *work, Err(FetchOutcome::Failed));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3622,13 +3715,13 @@ pub fn spawn_face_sweep(
|
||||
}
|
||||
};
|
||||
drop(held);
|
||||
(req, got)
|
||||
(req, *work, got)
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
|
||||
let mut lane_failed = 0usize;
|
||||
for (req, got) in fetched {
|
||||
for (req, work, got) in fetched {
|
||||
let bytes = match got {
|
||||
Ok(b) => b,
|
||||
Err(FetchOutcome::Offline) => {
|
||||
@@ -3641,6 +3734,36 @@ pub fn spawn_face_sweep(
|
||||
}
|
||||
};
|
||||
|
||||
if work == SweepWork::Measure {
|
||||
let image = dr_types::ImageId(req.image_id as u64);
|
||||
match measure_one_native(
|
||||
&gpu,
|
||||
&mut embedder,
|
||||
&catalog,
|
||||
image,
|
||||
&model_id,
|
||||
&bytes,
|
||||
) {
|
||||
Ok(n) => {
|
||||
images += 1;
|
||||
found += n;
|
||||
if tx
|
||||
.send(FaceSweepMessage::Indexed { image, faces: n })
|
||||
.is_err()
|
||||
{
|
||||
log::info!("face sweep: cancelled after {images} image(s)");
|
||||
pool.release_all(&*backend).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("face sweep: measuring {}: {e}", req.path);
|
||||
lane_failed += 1;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
match index_one_native(&gpu, &mut detector, &mut embedder, &bytes, &options) {
|
||||
Ok((faces, edge, proxy)) => {
|
||||
// Before the detections, so a kill between the two
|
||||
@@ -3855,6 +3978,51 @@ fn index_one_native(
|
||||
Ok((faces, edge, proxy))
|
||||
}
|
||||
|
||||
/// TRACES: FR-CULL-8 | FR-CULL-9
|
||||
/// Render one original at native resolution and embed its stored faces again.
|
||||
///
|
||||
/// [`index_one_native`]'s sibling for the measuring pass: the same render, no
|
||||
/// detection, and the result written *over* the faces rather than in place of
|
||||
/// them. Returns how many faces were measured.
|
||||
///
|
||||
/// The proxy is left alone. The faces here were found on a pass that stored
|
||||
/// one, or [`faces_without_proxy`] would have claimed the image first.
|
||||
fn measure_one_native(
|
||||
gpu: &dr_gpu::GpuContext,
|
||||
embedder: &mut dr_face::Embedder,
|
||||
catalog: &Catalog,
|
||||
image: dr_types::ImageId,
|
||||
model_id: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<usize, String> {
|
||||
let faces = dr_catalog::faces::unmeasured_on_image(catalog.connection(), image, model_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if faces.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let frame = render_native(gpu, bytes)?;
|
||||
let edge = frame.width.max(frame.height);
|
||||
let measured = crate::faces::measure_native(
|
||||
embedder,
|
||||
&frame.rgba,
|
||||
frame.width as usize,
|
||||
frame.height as usize,
|
||||
&faces,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let n = measured.measured.len();
|
||||
dr_catalog::faces::record_measurements(
|
||||
catalog.connection(),
|
||||
image,
|
||||
model_id,
|
||||
edge,
|
||||
&measured.measured,
|
||||
&measured.dropped,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// The class the whole-library pass fills.
|
||||
///
|
||||
/// Grid only, deliberately. The large class is four times the transfer for a
|
||||
@@ -6055,6 +6223,53 @@ mod tests {
|
||||
assert_eq!(faces_unindexed(&catalog, "other").unwrap().len(), 3);
|
||||
}
|
||||
|
||||
/// The state schema V14 leaves: a face with no quality and an image with
|
||||
/// no marker. It is the measuring pass's work, and *only* that pass's — a
|
||||
/// full re-detection of the same image would throw away every suggestion
|
||||
/// on it for nothing.
|
||||
#[test]
|
||||
fn an_unmeasured_face_is_measured_rather_than_re_detected() {
|
||||
let catalog = with_images(3);
|
||||
let ids = image_ids(&catalog);
|
||||
let stored = |quality: Option<f32>| dr_catalog::faces::DetectedFace {
|
||||
x: 0.1,
|
||||
y: 0.1,
|
||||
w: 0.2,
|
||||
h: 0.2,
|
||||
landmarks: [(0.0, 0.0); 5],
|
||||
confidence: 0.9,
|
||||
embedding: vec![0u8; 1024],
|
||||
crop_px: 120.0,
|
||||
quality,
|
||||
crop: Vec::new(),
|
||||
model_id: "w600k_mbf".into(),
|
||||
};
|
||||
let conn = catalog.connection();
|
||||
dr_catalog::faces::record_detections(conn, ids[0], "w600k_mbf", 4000, &[stored(None)])
|
||||
.unwrap();
|
||||
dr_catalog::faces::record_detections(
|
||||
conn,
|
||||
ids[1],
|
||||
"w600k_mbf",
|
||||
4000,
|
||||
&[stored(Some(18.0))],
|
||||
)
|
||||
.unwrap();
|
||||
// What V14 does to the first: the marker goes, the face stays.
|
||||
dr_catalog::faces::clear_index_marker(conn, ids[0], "w600k_mbf").unwrap();
|
||||
|
||||
let measure = faces_unmeasured(&catalog, "w600k_mbf").unwrap();
|
||||
assert_eq!(measure.len(), 1);
|
||||
assert_eq!(measure[0].image_id, ids[0].0 as i64);
|
||||
assert!(measure[0].full_resolution);
|
||||
|
||||
// Not re-detected, marker or no marker; the third image, never seen,
|
||||
// still is.
|
||||
let detect = faces_unindexed(&catalog, "w600k_mbf").unwrap();
|
||||
assert_eq!(detect.len(), 1);
|
||||
assert_eq!(detect[0].image_id, ids[2].0 as i64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scoped_grid_shows_only_that_collections_images() {
|
||||
use dr_catalog::collections::{self as coll, CollectionKind};
|
||||
|
||||
Reference in New Issue
Block a user