fix: eliminate false-positive duplicate mega-groups
The union-find algorithm caused transitivity chains: A≈B and B≈C merged A, B, C into one group even if A and C were completely different photos, producing the 5000+ "duplicate" groups that were wrong. - getGroups now returns individual pairs (2 photos each) — what was actually flagged by the pHash comparison, no chaining - scan() clears all existing pairs on each run so the result exactly matches the current threshold (no stale loose pairs from prior scans) - Default dedupe_threshold lowered from 10 → 5 (fewer false positives; distance ≤5 means the images are genuinely near-identical) Run "Scan library" once to replace the old results with clean pairs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
64cca095ea
commit
e58e6de125
2 files changed
+15
-27
No files matched your search
+14
-26
@@ -5,9 +5,13 @@ import { hammingDistance } from '../worker/hash.js';
|
|||||||
const SCAN_CHUNK = 50;
|
const SCAN_CHUNK = 50;
|
||||||
|
|
||||||
export async function scan(db, threshold) {
|
export async function scan(db, threshold) {
|
||||||
const THRESHOLD = threshold ?? parseInt(process.env.DEDUPE_THRESHOLD ?? '10', 10);
|
const THRESHOLD = threshold ?? parseInt(process.env.DEDUPE_THRESHOLD ?? '5', 10);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
|
// Clear all existing pairs so the result exactly matches the current threshold.
|
||||||
|
// Dismissed pairs are also cleared — a fresh scan is a clean slate.
|
||||||
|
db.raw.prepare('DELETE FROM duplicate_pairs').run();
|
||||||
|
|
||||||
const photos = db.raw.prepare(
|
const photos = db.raw.prepare(
|
||||||
"SELECT id, phash FROM photos WHERE phash IS NOT NULL AND phash != '0000000000000000'"
|
"SELECT id, phash FROM photos WHERE phash IS NOT NULL AND phash != '0000000000000000'"
|
||||||
).all();
|
).all();
|
||||||
@@ -57,34 +61,18 @@ export function getGroups(db) {
|
|||||||
const pairs = db.getDupePairs();
|
const pairs = db.getDupePairs();
|
||||||
if (!pairs.length) return [];
|
if (!pairs.length) return [];
|
||||||
|
|
||||||
const parent = new Map();
|
const allIds = [...new Set(pairs.flatMap(p => [p.photo_id_a, p.photo_id_b]))];
|
||||||
|
|
||||||
function find(x) {
|
|
||||||
if (!parent.has(x)) parent.set(x, x);
|
|
||||||
if (parent.get(x) !== x) parent.set(x, find(parent.get(x)));
|
|
||||||
return parent.get(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
function union(a, b) { parent.set(find(a), find(b)); }
|
|
||||||
|
|
||||||
for (const { photo_id_a, photo_id_b } of pairs) union(photo_id_a, photo_id_b);
|
|
||||||
for (const id of [...parent.keys()]) find(id);
|
|
||||||
|
|
||||||
const groups = new Map();
|
|
||||||
for (const id of parent.keys()) {
|
|
||||||
const root = find(id);
|
|
||||||
if (!groups.has(root)) groups.set(root, []);
|
|
||||||
groups.get(root).push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allIds = [...parent.keys()];
|
|
||||||
const rows = fetchPhotosByIds(db, allIds);
|
const rows = fetchPhotosByIds(db, allIds);
|
||||||
const photoMap = new Map(rows.map(r => [r.id, r]));
|
const photoMap = new Map(rows.map(r => [r.id, r]));
|
||||||
|
|
||||||
return [...groups.values()]
|
// Return each pair as its own 2-photo group.
|
||||||
.filter(ids => ids.length >= 2)
|
// Union-find chaining caused false "5000-photo" mega-groups through
|
||||||
.map(ids => ids.map(id => photoMap.get(id)).filter(Boolean))
|
// transitivity (A≈B and B≈C ≠ A≈C). Individual pairs are always accurate.
|
||||||
.sort((a, b) => b.length - a.length);
|
return pairs
|
||||||
|
.map(({ photo_id_a, photo_id_b }) =>
|
||||||
|
[photoMap.get(photo_id_a), photoMap.get(photo_id_b)].filter(Boolean)
|
||||||
|
)
|
||||||
|
.filter(g => g.length === 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scoreDuplicate(photo) {
|
export function scoreDuplicate(photo) {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ const SETTING_DEFAULTS = {
|
|||||||
thumb_quality: '80',
|
thumb_quality: '80',
|
||||||
preview_size: '1600',
|
preview_size: '1600',
|
||||||
preview_quality: '85',
|
preview_quality: '85',
|
||||||
dedupe_threshold: '10',
|
dedupe_threshold: '5',
|
||||||
// iCloud Photos sync
|
// iCloud Photos sync
|
||||||
icloud_enabled: '0',
|
icloud_enabled: '0',
|
||||||
icloud_apple_id: '',
|
icloud_apple_id: '',
|
||||||
|
|||||||
Reference in new issue
Block a user