Carry every identity across a re-detection, by box and by embedding
record_detections replaces an image's faces and carried only the user's confirmations onto the new ones, by box overlap above 0.5 IoU. Everything else on the old faces was dropped: the suggestions the last grouping pass made, and the people the user had said a face was not. On the reference library that is 13,011 suggestions and 77 rejections beside 3,778 confirmations — a re-detection of it would have been correct by FR-CULL-12's letter, since suggestions are derived data, and would have handed back a People screen of strangers. Now every old face is read before the delete — box, vector, assignment, rejections — and matched to the new faces one-to-one, best pair first. A pair qualifies when the boxes overlap at all and either the overlap alone says so (IoU above 0.5, the old rule) or the embeddings do (cosine above SAME_FACE_COSINE, 0.45, the reference library's P≈0.95 line). The embedding route claims the box a low-resolution pass drew badly enough that overlap alone would not; the vector is also what breaks the tie in a group photograph, where two neighbouring faces overlap both new boxes. Overlap is required on both routes, because the same vector elsewhere in the frame — a mirror, a print on the wall — is not the same face and must not take its name. Onto the matched face go the assignment as it was, confirmed or suggested with its probability, and every rejection. The merge's match_faces still matches by overlap alone across devices; it is the same question and is not changed here.
This commit is contained in:
@@ -762,8 +762,8 @@ pub fn export_to_shards_reporting(
|
||||
/// computed is the one it can vouch for. A peer's *stronger* pass is another
|
||||
/// matter: it is the re-detection this device's own sweep would queue
|
||||
/// (`FaceDetector::supersedes`), already done, and taking it is what spares
|
||||
/// a tablet the fetch. Names survive the replacement by box overlap, as they
|
||||
/// do a local re-detection.
|
||||
/// a tablet the fetch. Names survive the replacement by box overlap and
|
||||
/// embedding, as they do a local re-detection (`faces::record_detections`).
|
||||
///
|
||||
/// A peer's faces are taken under whichever compatible detector found them:
|
||||
/// a tablet set to the fast detector adopts the desktop's thorough pass
|
||||
|
||||
+383
-47
@@ -207,16 +207,60 @@ pub use dr_face::Calibration;
|
||||
|
||||
// ── detection ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// The cosine above which two vectors from one embedder are taken to be
|
||||
/// the same face in the same photograph, for carrying an identity across a
|
||||
/// re-detection.
|
||||
///
|
||||
/// Set at the reference library's P≈0.95 line (docs/faces.md §9's table:
|
||||
/// 0.449), which is far above anything two different people in one frame
|
||||
/// reach and below what one face re-embedded from a better crop of itself
|
||||
/// does. The number is only ever asked about *overlapping* boxes on *one*
|
||||
/// image, which is what makes such a loose figure safe: the question is not
|
||||
/// "is this the same person" but "is this the same face, now that the box
|
||||
/// has moved".
|
||||
pub const SAME_FACE_COSINE: f32 = 0.45;
|
||||
|
||||
/// What one stored face knew about itself, read before a re-detection
|
||||
/// replaces it.
|
||||
struct Prior {
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
/// Decoded and unit length, under the *embedder* half of its id so a
|
||||
/// cosine against a new face from the same embedder is defined.
|
||||
embedding: Option<dr_face::Embedding>,
|
||||
/// `(person, probability, confirmed)`, where the face had one.
|
||||
assignment: Option<(i64, f64, bool)>,
|
||||
rejected: Vec<i64>,
|
||||
}
|
||||
|
||||
/// Replace every face on an image with a fresh detection pass.
|
||||
///
|
||||
/// Replace rather than append, because `DetectFaces` is coalesced per image
|
||||
/// (FR-CAT-3) and re-running it must be idempotent — appending would double the
|
||||
/// faces on every re-index and silently inflate every cluster.
|
||||
///
|
||||
/// **Confirmed assignments survive** where the new detection covers the same
|
||||
/// part of the frame: a re-index with a better model must not discard the
|
||||
/// user's labelling (FR-CULL-10). Matching is by box overlap, since the face is
|
||||
/// in the same place even when the box moves a little.
|
||||
/// # What survives the replacement
|
||||
///
|
||||
/// **The identity of every face the new detection finds again** — the user's
|
||||
/// confirmations first of all (FR-CULL-10), and with them the suggestions
|
||||
/// the last grouping pass made and the people the user has said a face is
|
||||
/// *not*. A re-index with a better model must not discard the user's
|
||||
/// labelling, and it should not empty the People screen either: a library
|
||||
/// of thirteen thousand suggestions re-detected into thirteen thousand
|
||||
/// unassigned faces is correct by the letter of FR-CULL-12 and looks, to
|
||||
/// the person who spent an afternoon on it, like the work was thrown away.
|
||||
///
|
||||
/// A new face is the same face as an old one when the two **overlap in the
|
||||
/// frame and either the boxes agree or the embeddings do**: an IoU above 0.5,
|
||||
/// which is loose on purpose because a better detector is entitled to move
|
||||
/// the box; or a cosine above [`SAME_FACE_COSINE`] between the vectors, for
|
||||
/// the box a low-resolution pass drew badly enough that overlap alone would
|
||||
/// not claim it. The embedding is also what breaks a tie — two faces side by
|
||||
/// side in a group photograph both overlap both new boxes, and the vector
|
||||
/// says which is which where the rectangles cannot. Each old face is
|
||||
/// carried onto at most one new one, best pair first.
|
||||
///
|
||||
/// **Every model's faces are replaced, and every other model's marker goes
|
||||
/// with them.** An image holds the faces of whichever pipeline looked at it
|
||||
@@ -236,29 +280,15 @@ pub fn record_detections(
|
||||
) -> Result<Vec<FaceId>, CatalogError> {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
|
||||
// What the user had asserted, so it can be carried across the replacement.
|
||||
let mut prior: Vec<(f32, f32, f32, f32, i64, f64)> = Vec::new();
|
||||
{
|
||||
let mut q = tx.prepare(
|
||||
"SELECT f.x, f.y, f.w, f.h, fp.person_id, fp.probability
|
||||
FROM faces f
|
||||
JOIN face_person fp ON fp.face_id = f.id
|
||||
WHERE f.image_id = ?1 AND fp.confirmed = 1",
|
||||
)?;
|
||||
let rows = q.query_map([image_id.0 as i64], |r| {
|
||||
Ok((
|
||||
r.get::<_, f64>(0)? as f32,
|
||||
r.get::<_, f64>(1)? as f32,
|
||||
r.get::<_, f64>(2)? as f32,
|
||||
r.get::<_, f64>(3)? as f32,
|
||||
r.get::<_, i64>(4)?,
|
||||
r.get::<_, f64>(5)?,
|
||||
))
|
||||
})?;
|
||||
for row in rows {
|
||||
prior.push(row?);
|
||||
}
|
||||
}
|
||||
// Everything the old faces knew, so it can be carried across the
|
||||
// replacement. Read only when there is something to carry it onto: a
|
||||
// pass that found nothing has nothing to match, and decoding a vector
|
||||
// per face for no reader would be the wasted work.
|
||||
let prior = if faces.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
read_priors(&tx, image_id)?
|
||||
};
|
||||
|
||||
tx.execute("DELETE FROM faces WHERE image_id = ?1", [image_id.0 as i64])?;
|
||||
tx.execute(
|
||||
@@ -266,9 +296,11 @@ pub fn record_detections(
|
||||
rusqlite::params![image_id.0 as i64, model_id],
|
||||
)?;
|
||||
|
||||
let carried = match_priors(&prior, faces);
|
||||
|
||||
let now = now_secs();
|
||||
let mut ids = Vec::with_capacity(faces.len());
|
||||
for f in faces {
|
||||
for (i, f) in faces.iter().enumerate() {
|
||||
tx.execute(
|
||||
"INSERT INTO faces
|
||||
(image_id, x, y, w, h, landmarks, detector_confidence,
|
||||
@@ -306,28 +338,38 @@ pub fn record_detections(
|
||||
)?;
|
||||
let id = FaceId(tx.last_insert_rowid() as u64);
|
||||
|
||||
// Re-attach a confirmation whose box this detection clearly replaces.
|
||||
// 0.5 IoU is loose on purpose: the question is "is this the same face
|
||||
// in the frame", not "is this the same box", and a better detector is
|
||||
// entitled to move the box.
|
||||
if let Some((_, _, _, _, person, prob)) = prior
|
||||
.iter()
|
||||
.filter(|p| iou((p.0, p.1, p.2, p.3), (f.x, f.y, f.w, f.h)) > 0.5)
|
||||
.max_by(|a, b| {
|
||||
iou((a.0, a.1, a.2, a.3), (f.x, f.y, f.w, f.h))
|
||||
.total_cmp(&iou((b.0, b.1, b.2, b.3), (f.x, f.y, f.w, f.h)))
|
||||
})
|
||||
{
|
||||
tx.execute(
|
||||
"INSERT INTO face_person (face_id, person_id, probability, confirmed)
|
||||
VALUES (?1, ?2, ?3, 1)",
|
||||
rusqlite::params![id.0 as i64, person, prob],
|
||||
)?;
|
||||
if let Some(p) = carried[i].map(|at| &prior[at]) {
|
||||
if let Some((person, prob, confirmed)) = p.assignment {
|
||||
tx.execute(
|
||||
"INSERT INTO face_person (face_id, person_id, probability, confirmed)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![id.0 as i64, person, prob, confirmed],
|
||||
)?;
|
||||
}
|
||||
for person in &p.rejected {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO face_person_rejected (face_id, person_id)
|
||||
VALUES (?1, ?2)",
|
||||
rusqlite::params![id.0 as i64, person],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
let lost = prior
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(at, p)| p.assignment.is_some() && !carried.contains(&Some(*at)))
|
||||
.count();
|
||||
if lost > 0 {
|
||||
log::debug!(
|
||||
"image {}: {lost} assigned face(s) not found again by the re-detection",
|
||||
image_id.0
|
||||
);
|
||||
}
|
||||
|
||||
// The run marker, written whether or not anything was found. Zero faces is
|
||||
// a real answer and recording it is what stops the next pass looking at
|
||||
// this photograph again -- see the V9 migration.
|
||||
@@ -351,6 +393,115 @@ pub fn record_detections(
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// The faces on an image as they stand, with everything a re-detection
|
||||
/// carries forward.
|
||||
fn read_priors(tx: &Connection, image_id: ImageId) -> Result<Vec<Prior>, CatalogError> {
|
||||
let mut out: Vec<(i64, Prior)> = Vec::new();
|
||||
{
|
||||
let mut q = tx.prepare(
|
||||
"SELECT f.id, f.x, f.y, f.w, f.h, f.model_id, f.embedding,
|
||||
fp.person_id, fp.probability, fp.confirmed
|
||||
FROM faces f
|
||||
LEFT JOIN face_person fp ON fp.face_id = f.id
|
||||
WHERE f.image_id = ?1
|
||||
ORDER BY f.id",
|
||||
)?;
|
||||
let rows = q.query_map([image_id.0 as i64], |r| {
|
||||
let model: String = r.get(5)?;
|
||||
let blob: Vec<u8> = r.get(6)?;
|
||||
let person: Option<i64> = r.get(7)?;
|
||||
Ok((
|
||||
r.get::<_, i64>(0)?,
|
||||
Prior {
|
||||
x: r.get::<_, f64>(1)? as f32,
|
||||
y: r.get::<_, f64>(2)? as f32,
|
||||
w: r.get::<_, f64>(3)? as f32,
|
||||
h: r.get::<_, f64>(4)? as f32,
|
||||
embedding: dr_face::Embedding::from_f16_bytes(
|
||||
dr_face::ModelId::new(embedder_of(&model).to_string()),
|
||||
&blob,
|
||||
),
|
||||
assignment: match person {
|
||||
Some(p) => Some((p, r.get::<_, f64>(8)?, r.get::<_, i64>(9)? != 0)),
|
||||
None => None,
|
||||
},
|
||||
rejected: Vec::new(),
|
||||
},
|
||||
))
|
||||
})?;
|
||||
for row in rows {
|
||||
out.push(row?);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut q = tx.prepare(
|
||||
"SELECT fr.face_id, fr.person_id
|
||||
FROM face_person_rejected fr
|
||||
JOIN faces f ON f.id = fr.face_id
|
||||
WHERE f.image_id = ?1",
|
||||
)?;
|
||||
let rows = q.query_map([image_id.0 as i64], |r| {
|
||||
Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))
|
||||
})?;
|
||||
for row in rows {
|
||||
let (face, person) = row?;
|
||||
if let Some((_, p)) = out.iter_mut().find(|(id, _)| *id == face) {
|
||||
p.rejected.push(person);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out.into_iter().map(|(_, p)| p).collect())
|
||||
}
|
||||
|
||||
/// Which old face, if any, each new detection replaces — indexed like
|
||||
/// `faces`, holding an index into `prior`.
|
||||
///
|
||||
/// One-to-one, best pair first: every qualifying pair is scored by how much
|
||||
/// the boxes and the vectors agree, and pairs are taken in that order until
|
||||
/// each side is spoken for. A pair qualifies when the boxes overlap at all
|
||||
/// and either the overlap alone says so (IoU above 0.5) or the embeddings do
|
||||
/// ([`SAME_FACE_COSINE`]). Overlap is required on both routes, because two
|
||||
/// faces of one person in one frame — a mirror, a photograph within the
|
||||
/// photograph — have the same vector and must not swap names.
|
||||
fn match_priors(prior: &[Prior], faces: &[DetectedFace]) -> Vec<Option<usize>> {
|
||||
let mut pairs: Vec<(f32, usize, usize)> = Vec::new();
|
||||
for (pi, p) in prior.iter().enumerate() {
|
||||
let old = (p.x, p.y, p.w, p.h);
|
||||
for (fi, f) in faces.iter().enumerate() {
|
||||
let overlap = iou(old, (f.x, f.y, f.w, f.h));
|
||||
if overlap <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let cosine = p
|
||||
.embedding
|
||||
.as_ref()
|
||||
.and_then(|old| {
|
||||
let new = dr_face::Embedding::from_f16_bytes(
|
||||
dr_face::ModelId::new(embedder_of(&f.model_id).to_string()),
|
||||
&f.embedding,
|
||||
)?;
|
||||
old.cosine(&new)
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
if overlap > 0.5 || cosine >= SAME_FACE_COSINE {
|
||||
pairs.push((overlap + cosine.max(0.0), pi, fi));
|
||||
}
|
||||
}
|
||||
}
|
||||
pairs.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
|
||||
let mut carried = vec![None; faces.len()];
|
||||
let mut taken = vec![false; prior.len()];
|
||||
for (_, pi, fi) in pairs {
|
||||
if taken[pi] || carried[fi].is_some() {
|
||||
continue;
|
||||
}
|
||||
taken[pi] = true;
|
||||
carried[fi] = Some(pi);
|
||||
}
|
||||
carried
|
||||
}
|
||||
|
||||
/// A face embedded again from its stored landmarks: the new vector and its
|
||||
/// length. What the measuring pass hands back per face.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -373,8 +524,8 @@ pub struct Measurement {
|
||||
/// created: the boxes and landmarks are right, the identities are the user's
|
||||
/// work, and only the vector needs doing again. Updating in place is what
|
||||
/// keeps `face_person` and the face ids exactly as they were -- a
|
||||
/// re-detection would carry confirmations across by box overlap and lose
|
||||
/// every suggestion, for no gain.
|
||||
/// re-detection carries identities across by matching old faces to new, and
|
||||
/// a match is a judgement where an update in place is a fact.
|
||||
///
|
||||
/// `dropped` are faces whose landmarks turned out to be degenerate -- the warp
|
||||
/// could not be built from them. Deleted here, as detection would have refused
|
||||
@@ -1678,6 +1829,191 @@ mod tests {
|
||||
assert!(got[0].confirmed);
|
||||
}
|
||||
|
||||
/// A stored vector pointing along one axis of the embedding space, so a
|
||||
/// test can say "these two faces are the same" or "orthogonal" exactly.
|
||||
fn along(axis: usize) -> Vec<u8> {
|
||||
let mut v = Box::new([0.0_f32; dr_face::EMBEDDING_DIM]);
|
||||
v[axis] = 1.0;
|
||||
dr_face::Embedding {
|
||||
model: dr_face::ModelId::new("w600k_mbf"),
|
||||
v,
|
||||
}
|
||||
.to_f16_bytes()
|
||||
}
|
||||
|
||||
fn face_at(x: f32, y: f32, w: f32, h: f32, axis: usize) -> DetectedFace {
|
||||
DetectedFace {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
embedding: along(axis),
|
||||
..face(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// The other half of FR-CULL-10's promise: the suggestions and the
|
||||
/// rejections are the state of the People screen, and a re-index that
|
||||
/// dropped them would hand back a library of strangers.
|
||||
#[test]
|
||||
fn re_detection_carries_a_suggestion_and_a_rejection_across() {
|
||||
let c = db();
|
||||
let img = image(&c, 1);
|
||||
let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
|
||||
let anna = create_person(&c, "Anna").unwrap();
|
||||
let bob = create_person(&c, "Bob").unwrap();
|
||||
suggest(&c, ids[0], anna, 0.83).unwrap();
|
||||
reject(&c, ids[0], bob).unwrap();
|
||||
|
||||
let mut moved = face(9);
|
||||
moved.x = 0.11;
|
||||
record_detections(&c, img, "scrfd_10g+w600k_mbf", 4000, &[moved]).unwrap();
|
||||
|
||||
let got = for_image(&c, img).unwrap();
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].person, Some(anna), "suggestion was lost on re-index");
|
||||
assert!(
|
||||
!got[0].confirmed,
|
||||
"a suggestion came back as a confirmation"
|
||||
);
|
||||
assert!((got[0].probability - 0.83).abs() < 1e-6);
|
||||
assert!(
|
||||
!suggest(&c, got[0].id, bob, 0.99).unwrap(),
|
||||
"the rejection of Bob was lost on re-index"
|
||||
);
|
||||
}
|
||||
|
||||
/// A box the low-resolution pass drew badly enough that overlap alone
|
||||
/// would not claim it: the vector does.
|
||||
#[test]
|
||||
fn a_moved_box_is_claimed_by_its_embedding() {
|
||||
let c = db();
|
||||
let img = image(&c, 1);
|
||||
let ids = record_detections(
|
||||
&c,
|
||||
img,
|
||||
"w600k_mbf",
|
||||
1024,
|
||||
&[face_at(0.10, 0.10, 0.20, 0.20, 3)],
|
||||
)
|
||||
.unwrap();
|
||||
let anna = create_person(&c, "Anna").unwrap();
|
||||
confirm(&c, ids[0], anna).unwrap();
|
||||
|
||||
// Shifted by two thirds of its width: IoU ≈ 0.2, well under the 0.5
|
||||
// the box route needs, but the boxes still touch.
|
||||
let again = face_at(0.23, 0.10, 0.20, 0.20, 3);
|
||||
record_detections(&c, img, "scrfd_10g+w600k_mbf", 4000, &[again]).unwrap();
|
||||
let got = for_image(&c, img).unwrap();
|
||||
assert_eq!(got[0].person, Some(anna));
|
||||
assert!(got[0].confirmed);
|
||||
|
||||
// The same shift with a stranger's vector is a different face, and
|
||||
// the confirmation is not handed to it.
|
||||
let other = image(&c, 2);
|
||||
let ids = record_detections(
|
||||
&c,
|
||||
other,
|
||||
"w600k_mbf",
|
||||
1024,
|
||||
&[face_at(0.10, 0.10, 0.20, 0.20, 3)],
|
||||
)
|
||||
.unwrap();
|
||||
confirm(&c, ids[0], anna).unwrap();
|
||||
let stranger = face_at(0.23, 0.10, 0.20, 0.20, 4);
|
||||
record_detections(&c, other, "scrfd_10g+w600k_mbf", 4000, &[stranger]).unwrap();
|
||||
assert_eq!(for_image(&c, other).unwrap()[0].person, None);
|
||||
}
|
||||
|
||||
/// Two people side by side, boxes overlapping both ways, and the new
|
||||
/// boxes shifted so that overlap alone would swap them.
|
||||
#[test]
|
||||
fn the_embedding_breaks_a_tie_between_neighbouring_faces() {
|
||||
let c = db();
|
||||
let img = image(&c, 1);
|
||||
let ids = record_detections(
|
||||
&c,
|
||||
img,
|
||||
"w600k_mbf",
|
||||
1024,
|
||||
&[
|
||||
face_at(0.10, 0.10, 0.20, 0.20, 1),
|
||||
face_at(0.25, 0.10, 0.20, 0.20, 2),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let anna = create_person(&c, "Anna").unwrap();
|
||||
let bob = create_person(&c, "Bob").unwrap();
|
||||
confirm(&c, ids[0], anna).unwrap();
|
||||
confirm(&c, ids[1], bob).unwrap();
|
||||
|
||||
// Anna's new box overlaps Bob's old one more than her own.
|
||||
let anna_again = face_at(0.20, 0.10, 0.20, 0.20, 1);
|
||||
let bob_again = face_at(0.35, 0.10, 0.20, 0.20, 2);
|
||||
let new = record_detections(
|
||||
&c,
|
||||
img,
|
||||
"scrfd_10g+w600k_mbf",
|
||||
4000,
|
||||
&[anna_again, bob_again],
|
||||
)
|
||||
.unwrap();
|
||||
let got = for_image(&c, img).unwrap();
|
||||
let person_of = |id: FaceId| got.iter().find(|f| f.id == id).unwrap().person;
|
||||
assert_eq!(person_of(new[0]), Some(anna));
|
||||
assert_eq!(person_of(new[1]), Some(bob));
|
||||
}
|
||||
|
||||
/// The same vector somewhere else in the frame — a mirror, a print on
|
||||
/// the wall — is not the same face, and overlap is what says so.
|
||||
#[test]
|
||||
fn the_same_person_elsewhere_in_the_frame_is_not_claimed() {
|
||||
let c = db();
|
||||
let img = image(&c, 1);
|
||||
let ids = record_detections(
|
||||
&c,
|
||||
img,
|
||||
"w600k_mbf",
|
||||
1024,
|
||||
&[face_at(0.10, 0.10, 0.20, 0.20, 5)],
|
||||
)
|
||||
.unwrap();
|
||||
let anna = create_person(&c, "Anna").unwrap();
|
||||
confirm(&c, ids[0], anna).unwrap();
|
||||
|
||||
record_detections(
|
||||
&c,
|
||||
img,
|
||||
"w600k_mbf",
|
||||
1024,
|
||||
&[face_at(0.60, 0.60, 0.20, 0.20, 5)],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(for_image(&c, img).unwrap()[0].person, None);
|
||||
}
|
||||
|
||||
/// Each old face is carried onto at most one new one: a second box over
|
||||
/// the same face — a detector that fires twice — does not become a
|
||||
/// second Anna.
|
||||
#[test]
|
||||
fn an_identity_is_carried_onto_one_face_only() {
|
||||
let c = db();
|
||||
let img = image(&c, 1);
|
||||
let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
|
||||
let anna = create_person(&c, "Anna").unwrap();
|
||||
confirm(&c, ids[0], anna).unwrap();
|
||||
|
||||
let mut twin = face(1);
|
||||
twin.x += 0.02;
|
||||
record_detections(&c, img, "w600k_mbf", 1024, &[face(1), twin]).unwrap();
|
||||
let named = for_image(&c, img)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|f| f.person == Some(anna))
|
||||
.count();
|
||||
assert_eq!(named, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_suggestion_never_overwrites_a_confirmation() {
|
||||
let c = db();
|
||||
|
||||
@@ -199,7 +199,8 @@ pub struct FaceSettings {
|
||||
/// the clustering and the sync all go on over every face already found.
|
||||
/// What a stronger choice does is queue the images a weaker one indexed for
|
||||
/// re-detection, after the ones nothing has indexed ([`Self::supersedes`]),
|
||||
/// with confirmed names carried across by `record_detections`' box overlap.
|
||||
/// with names carried across by `record_detections`' box and embedding
|
||||
/// matching.
|
||||
///
|
||||
/// The first version of this setting keyed everything on the full id, and
|
||||
/// choosing Thorough emptied both devices' People screens until a whole-
|
||||
|
||||
Reference in New Issue
Block a user