10 KiB
FotkyBuzalkovi — návrh systému a poznámky z analýzy
Pracovní dokument shrnující rozhodnutí a poznatky z prvotní analýzy projektu. Datum diskuze: 2026-05-21
1. Cíl projektu
- Organizovat a tagovat cca 200 000 rodinných fotografií
- Lokální nasazení (žádný cloud, žádní externí uživatelé)
- Současný stav: prázdný projekt s ukázkovými fotkami v
demo_fotky/
2. Architektonické rozhodnutí
Finální stack: jen PostgreSQL + filesystem
┌─────────────────────┐
│ Python aplikace │
└──────────┬──────────┘
│
┌──────┴──────┐
▼ ▼
┌─────────┐ ┌──────────┐
│PostgreSQL│ │Filesystem│
│ metadata │ │ .jpg/.png│
└─────────┘ └──────────┘
Co bylo zvažováno a zavrženo
| Technologie | Verdikt | Důvod |
|---|---|---|
| MongoDB | ❌ vynecháno | JSONB v PostgreSQL nahradí veškerou potřebnou funkcionalitu |
| Redis | ❌ vynecháno (zatím) | Pro lokální 200k fotek bez webových uživatelů zbytečný |
| MySQL | ❌ vynecháno | Projekt používá PostgreSQL (jiná databáze byla v původním plánu) |
Kdy přidat Redis (pozdější fáze)
- Web UI s rychlým vyhledáváním (cache výsledků)
- Paralelní workery pro import / generování thumbnailů
- Background fronty (zpracování AI tagů)
Kdy přidat něco dalšího
pgvectorextension pro PostgreSQL — až budeme chtít sémantické hledání (CLIP embeddings)- Elasticsearch — kdyby PostgreSQL fulltext nestačil (u 200k řádků nestane)
3. JSONB v PostgreSQL vs MongoDB BSON
Terminologie
- BSON = binární JSON od MongoDB (s typy jako
ObjectId,Date,Decimal128) - JSONB = binární JSON v PostgreSQL (vlastní formát, ne BSON, ale funkčně podobný)
Srovnání vyhledávání
MongoDB:
db.photos.find({ "exif.camera": "Canon EOS 5D" })
db.photos.find({ "exif.iso": { $gte: 800 } })
db.photos.find({ "tags": { $in: ["dovolená", "moře"] } })
PostgreSQL JSONB:
SELECT * FROM photos WHERE exif_data->>'camera' = 'Canon EOS 5D';
SELECT * FROM photos WHERE (exif_data->>'iso')::int >= 800;
SELECT * FROM photos WHERE exif_data @> '{"camera": "Canon EOS 5D"}';
Co umí stejně
| Funkce | MongoDB | PostgreSQL JSONB |
|---|---|---|
| Filtr podle pole v JSON | ano | ano (->>, @>) |
| Vnořené cesty | "a.b.c" |
data#>>'{a,b,c}' |
| Existence klíče | $exists |
? operátor |
| Index na JSON cestu | ano | ano (GIN) |
| Fulltext | ano | ano |
Výhody PostgreSQL JSONB pro náš případ
- JOINy s tabulkami tagů a kamer — v Mongo by to vyžadovalo
$lookup(pomalé) - Transakce napříč JSONB a relačními tabulkami
- Jeden backup nástroj (
pg_dump)
4. Identita fotky — 4 úrovně hashů
Klíčový problém: Když změníte byť jediný keyword v IPTC, SHA256 souboru se kompletně změní. Jak detekovat, že je to stále ta samá fotka?
┌─────────────────────────────────────────────────────────────┐
│ Stejná identita Stejný obsah │
│ ←─────────────────────────────────────────────────────→ │
│ │
│ SHA256 Pixel hash Perceptual Embedding │
│ souboru (jen pixely) hash (pHash) (CLIP/DINO) │
│ │
│ byte-by-byte metadata +recomprese +sémantika │
│ identické ignorováno +resize podobnost │
│ +crop │
└─────────────────────────────────────────────────────────────┘
1. SHA256 souboru (sha256_file)
- Detekuje přesnou kopii souboru
- Změna IPTC/EXIF, otočení, znovuuložení → jiný hash
2. Pixel hash (sha256_pixels)
- SHA256 dekódovaných pixelů (po aplikaci EXIF orientation)
- Stejná fotka po změně metadat → identický hash
- Toto je odpověď na otázku "jak detekovat, že je to ta samá fotka po změně keywords"
with Image.open(path) as img:
img = ImageOps.exif_transpose(img)
if img.mode != "RGB":
img = img.convert("RGB")
pixel_hash = hashlib.sha256(img.tobytes()).hexdigest()
3. Perceptual hash (pHash, dHash, wHash)
- 64-bit "otisk obsahu" — vizuálně podobné fotky → blízké hashe
- Porovnává se přes Hamming distance (
bin(h1 ^ h2).count("1")) - < 10 = velmi podobné, > 20 = odlišné
- Detekuje: recompresi, resize, drobné úpravy, watermark, crop
- Knihovna:
imagehash
4. Deep learning embedding (budoucnost)
- Vektor 512–1024 dimenzí z CLIP / DINOv2
- Sémantická podobnost ("dvě fotky stejné scény z různých úhlů")
- Uložení v PostgreSQL přes
pgvectorextension
Workflow při importu nové fotky
1. Spočítej sha256_file
↓
2. Najdi v DB stejný sha256_file?
ANO → identická kopie, přeskočit
NE → ↓
3. Spočítej sha256_pixels
↓
4. Najdi v DB stejný sha256_pixels?
ANO → stejná fotka, jen jiná metadata → updatuj, nepřidávej duplikát
NE → ↓
5. Spočítej phash, ulož do DB
↓
6. (Volitelně) najdi podobné: WHERE hamming(phash, ?) < 10
Poznámka k JPEG
- Před hashingem vždy aplikovat EXIF orientation (
ImageOps.exif_transpose) - Konvertovat do RGB pro konzistenci
- Jinak by se rotovaná fotka vs nerotovaná lišila
5. Tři vrstvy metadat ve fotce
| Standard | Co tam je | Kdo to vyplňuje |
|---|---|---|
| EXIF | Technická data (clona, ISO, čas, GPS, model kamery) | Kamera automaticky |
| IPTC | Popisná data (titulek, popis, klíčová slova, autor, copyright) | Člověk / software |
| XMP | Modernější nástupce IPTC od Adobe, často duplikuje + edits, ratings, regions | Software (Lightroom, Apple Photos) |
Pro náš případ
- iPhone vyplňuje hlavně EXIF a XMP (ne IPTC)
- Apple Photos do XMP ukládá rozpoznané obličeje (
mwg-rs:Regions) — i se jmény, pokud je pojmenuješ! - iOS screenshoty mají v XMP
description: "Screenshot"→ automatická detekce - Pokud budeme tagovat, je dobré tagy ukládat i do IPTC
Keywords— přežijí export do jiné aplikace
6. Výsledky exploration na 7 demo fotkách
Souhrn dat z explore_photos.py
| # | Soubor | Mpx | EXIF tagů | IPTC | XMP | GPS | Kamera |
|---|---|---|---|---|---|---|---|
| 1 | 2026-04-22 09.05.08.jpg | 24.47 | 121 | 0 | 4 | ano | iPhone 16 Pro Max |
| 2 | 2026-04-24 15.15.47.jpg | 12.19 | 107 | 0 | 0 | ne | iPhone 13 Pro Max |
| 3 | 2026-05-02 10.04.44.png | 3.57 | 12 | 0 | 1 | ne | Screenshot |
| 4 | 2026-05-18 06.22.37.jpg | 12.19 | 105 | 0 | 4 | ne | iPhone 13 Pro Max |
| 5 | 2026-05-18 06.22.41.jpg | 12.19 | 105 | 0 | 4 | ne | iPhone 13 Pro Max |
| 6 | 2026-05-18 13.54.47.jpg | 12.19 | 98 | 6 | 0 | ne | iPhone 13 Pro Max |
| 7 | 2026-05-18 14.10.59.jpg | 2.36 | 0 | 0 | 0 | ne | (sirotek) |
Klíčové objevy
-
Perceptual hash funguje — fotky [4] a [5] (pořízené 4 sekundy po sobě) mají Hamming distance pouze 4 = klasický burst snapshot.
-
Apple face detection v XMP — fotky 1, 4, 5 mají
face_regions_count: 1. iPhone už detekoval obličej a uložil to do XMP. Můžeme číst přímo, bez vlastní AI. -
Screenshot rozpoznán — foto [3] má v XMP
description: "Screenshot"→ automatická kategorizace. -
Foto [7] = sirotek — žádné metadata. Pravděpodobně přeposlané přes WhatsApp/Messenger, kde se EXIF maže. Pro 200k fotek bude tato kategorie významná.
-
GPS jen u 1/7 fotek — u většiny iPhone fotek máte vypnuté lokační služby.
-
Time zone — fotky mají
OffsetTime: +02:00. Datum pořízení ukládat jakoTIMESTAMPTZ(s timezone). -
ExifRead > Pillow — Pillow má GPS bug (
'int' object has no attribute 'items'). Primární parser bude ExifRead. -
MakerNote (Apple binary blob) — obsahuje burst ID, HDR příznak, focus distance. Pro rozluštění potřeba
exiftool(volat přespyexiftool).
7. Databázové schéma
Kompletní aktuální schéma (tabulky, sloupce, indexy) viz SCHEMA.md.
8. Otevřené otázky
Foto bez EXIF (sirotek typu [7])
- a) Importovat (s
taken_atzmtime) - b) Odmítnout jako "nezpracovatelnou"
- c) Importovat, ale označit
processing_status = 'no_metadata'
Detekce duplikátů (shoda sha256_pixels)
- a) Přeskočit (nepřidávat duplicitu)
- b) Sloučit (aktualizovat metadata u existujícího záznamu)
- c) Uložit oba, jen označit jako související
Storage layout fotek
- a) Necháme v aktuálním umístění, do DB jen cestu
- b) Kopie do
archiv/YYYY/MM/původní_název.jpg - c) Kopie do
archiv/{sha[:2]}/{sha}.jpg(content-addressable = auto-dedup na FS)
9. Co dál — nápady k prozkoumání
exiftool— rozluští Apple MakerNote (burst ID by potvrdilo, že [4] a [5] patří k sobě)- Embedded thumbnail z EXIF — telefon ukládá malou náhledovku přímo v souboru → rychlejší galerie bez generování
- CLIP embeddings + pgvector — sémantické vyhledávání ("ukaž fotky pejsků na pláži")
- Reverse geocoding — z GPS souřadnic na čitelné místo (Nominatim / Photon, lokálně)
- Apple Photos jména osob — z XMP
mwg-rs:Regionsjdou číst i jména, pokud jsou v Photos pojmenovaná - Datum z názvu souboru — fallback když chybí EXIF i sensible
mtime(regex z "2026-05-18 13.54.47.jpg")
10. Aktuální stav a struktura projektu
Viz README.md — hlavní rozcestník projektu.