You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
87 lines
2.8 KiB
87 lines
2.8 KiB
#!/usr/bin/env python3 |
|
"""Suggest duplicate Mangueiral ads from visually similar local photos. |
|
|
|
Uses a simple difference hash as a screening signal. Results are candidates for |
|
manual review, never automatic proof that two ads describe the same property. |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
import sqlite3 |
|
from collections import defaultdict |
|
from pathlib import Path |
|
|
|
from PIL import Image, UnidentifiedImageError |
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2] |
|
DB_PATH = ROOT / "dfimoveis_data" / "dfimoveis.sqlite3" |
|
DATA_ROOT = DB_PATH.parent |
|
|
|
|
|
def difference_hash(path: Path) -> int | None: |
|
try: |
|
with Image.open(path) as image: |
|
pixels = list(image.convert("L").resize((9, 8)).getdata()) |
|
except (OSError, UnidentifiedImageError): |
|
return None |
|
value = 0 |
|
for row in range(8): |
|
offset = row * 9 |
|
for column in range(8): |
|
value = (value << 1) | (pixels[offset + column] > pixels[offset + column + 1]) |
|
return value |
|
|
|
|
|
def main() -> None: |
|
connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True) |
|
connection.execute("PRAGMA query_only = ON") |
|
connection.execute("BEGIN") |
|
rows = connection.execute( |
|
""" |
|
WITH mangueiral AS ( |
|
SELECT s.listing_id |
|
FROM listing_searches AS s |
|
JOIN listings AS l USING (listing_id) |
|
WHERE instr(search_url, '/jardins-mangueiral/') > 0 |
|
AND l.inactive_at IS NULL |
|
), uncommon AS ( |
|
SELECT sha256 |
|
FROM photos |
|
WHERE sha256 IS NOT NULL |
|
GROUP BY sha256 |
|
HAVING COUNT(DISTINCT listing_id) <= 5 |
|
) |
|
SELECT p.listing_id, p.local_path |
|
FROM photos AS p |
|
JOIN mangueiral AS m USING (listing_id) |
|
JOIN uncommon AS u USING (sha256) |
|
WHERE p.local_path IS NOT NULL |
|
ORDER BY p.listing_id, p.ordinal |
|
""" |
|
).fetchall() |
|
connection.close() |
|
|
|
hashes: dict[str, list[int]] = defaultdict(list) |
|
for listing_id, relative_path in rows: |
|
value = difference_hash(DATA_ROOT / relative_path) |
|
if value is not None: |
|
hashes[listing_id].append(value) |
|
|
|
ids = sorted(hashes) |
|
print("listing_a\tlisting_b\tvisually_similar_photos") |
|
for index, first_id in enumerate(ids): |
|
for second_id in ids[index + 1 :]: |
|
available = list(hashes[second_id]) |
|
matches = 0 |
|
for first_hash in hashes[first_id]: |
|
distances = [(first_hash ^ second_hash).bit_count() for second_hash in available] |
|
if distances and min(distances) <= 6: |
|
available.pop(distances.index(min(distances))) |
|
matches += 1 |
|
if matches >= 3: |
|
print(f"{first_id}\t{second_id}\t{matches}") |
|
|
|
|
|
if __name__ == "__main__": |
|
main()
|
|
|