Detect duplicate images during migration
Browse files
app.py
CHANGED
|
@@ -108,6 +108,9 @@ PREVIEW_CACHE_SIZE = 256
|
|
| 108 |
PREVIEW_QUALITY = 70
|
| 109 |
PREVIEW_SIZE = 320
|
| 110 |
IMAGE_KEY_CACHE_SIZE = 512
|
|
|
|
|
|
|
|
|
|
| 111 |
DEFAULT_RETURN_SCALE = 1
|
| 112 |
DEFAULT_BATCH_SIZE = 1
|
| 113 |
DEFAULT_UI_BATCH_SIZE = 1
|
|
@@ -333,21 +336,30 @@ def cleanup_mount():
|
|
| 333 |
path.unlink()
|
| 334 |
log(f"Deleted img/{path.relative_to(IMAGE_DIR)}")
|
| 335 |
if HAS_DATA_MOUNT:
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
for path in pngs:
|
| 342 |
-
if path in duplicates:
|
| 343 |
-
path.unlink()
|
| 344 |
-
log(f"Deleted img/{path.relative_to(IMAGE_DIR)}")
|
| 345 |
-
for path in pngs:
|
| 346 |
-
if path in duplicates:
|
| 347 |
-
continue
|
| 348 |
with Image.open(path) as image:
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
path.unlink()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
|
| 353 |
def download(url, target):
|
|
@@ -1988,6 +2000,20 @@ def archive_image(image, from_api):
|
|
| 1988 |
log(f"Archive failed: {error}")
|
| 1989 |
|
| 1990 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1991 |
def image_png_bytes(image):
|
| 1992 |
pnginfo = PngInfo()
|
| 1993 |
for key, value in image.info.items():
|
|
|
|
| 108 |
PREVIEW_QUALITY = 70
|
| 109 |
PREVIEW_SIZE = 320
|
| 110 |
IMAGE_KEY_CACHE_SIZE = 512
|
| 111 |
+
DUPLICATE_HASH_SIZE = 16
|
| 112 |
+
DUPLICATE_HASH_DISTANCE = 24
|
| 113 |
+
DUPLICATE_COLOR_DISTANCE = 24
|
| 114 |
DEFAULT_RETURN_SCALE = 1
|
| 115 |
DEFAULT_BATCH_SIZE = 1
|
| 116 |
DEFAULT_UI_BATCH_SIZE = 1
|
|
|
|
| 336 |
path.unlink()
|
| 337 |
log(f"Deleted img/{path.relative_to(IMAGE_DIR)}")
|
| 338 |
if HAS_DATA_MOUNT:
|
| 339 |
+
fingerprints = []
|
| 340 |
+
for path in IMAGE_DIR.rglob("*.epng"):
|
| 341 |
+
with Image.open(BytesIO(stored_bytes(path))) as image:
|
| 342 |
+
fingerprints.append(image_fingerprint(image))
|
| 343 |
+
for path in IMAGE_DIR.glob("????-??-??/*.png"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
with Image.open(path) as image:
|
| 345 |
+
fingerprint = image_fingerprint(image)
|
| 346 |
+
duplicate = any(
|
| 347 |
+
(fingerprint[0] ^ known[0]).bit_count()
|
| 348 |
+
<= DUPLICATE_HASH_DISTANCE
|
| 349 |
+
and sum(
|
| 350 |
+
abs(left - right)
|
| 351 |
+
for left, right in zip(fingerprint[1], known[1])
|
| 352 |
+
)
|
| 353 |
+
<= DUPLICATE_COLOR_DISTANCE
|
| 354 |
+
for known in fingerprints
|
| 355 |
+
)
|
| 356 |
+
if not duplicate:
|
| 357 |
+
save_named_image(image, path.with_suffix(".epng"))
|
| 358 |
path.unlink()
|
| 359 |
+
if duplicate:
|
| 360 |
+
log(f"Deleted duplicate img/{path.relative_to(IMAGE_DIR)}")
|
| 361 |
+
else:
|
| 362 |
+
fingerprints.append(fingerprint)
|
| 363 |
|
| 364 |
|
| 365 |
def download(url, target):
|
|
|
|
| 2000 |
log(f"Archive failed: {error}")
|
| 2001 |
|
| 2002 |
|
| 2003 |
+
def image_fingerprint(image):
|
| 2004 |
+
pixels = np.asarray(
|
| 2005 |
+
ImageOps.exif_transpose(image).convert("RGB").resize(
|
| 2006 |
+
(DUPLICATE_HASH_SIZE + 1, DUPLICATE_HASH_SIZE),
|
| 2007 |
+
Image.Resampling.LANCZOS,
|
| 2008 |
+
)
|
| 2009 |
+
)
|
| 2010 |
+
differences = pixels[:, 1:] > pixels[:, :-1]
|
| 2011 |
+
return (
|
| 2012 |
+
int.from_bytes(np.packbits(differences).tobytes()),
|
| 2013 |
+
tuple(int(value) for value in pixels.mean(axis=(0, 1))),
|
| 2014 |
+
)
|
| 2015 |
+
|
| 2016 |
+
|
| 2017 |
def image_png_bytes(image):
|
| 2018 |
pnginfo = PngInfo()
|
| 2019 |
for key, value in image.info.items():
|