# 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 - **`pgvector`** extension 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:** ```javascript 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:** ```sql 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 1. **JOINy** s tabulkami tagů a kamer — v Mongo by to vyžadovalo `$lookup` (pomalé) 2. **Transakce** napříč JSONB a relačními tabulkami 3. **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" ```python 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`](https://pypi.org/project/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 `pgvector` extension ### 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 1. **Perceptual hash funguje** — fotky [4] a [5] (pořízené 4 sekundy po sobě) mají Hamming distance pouze **4** = klasický burst snapshot. 2. **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. 3. **Screenshot rozpoznán** — foto [3] má v XMP `description: "Screenshot"` → automatická kategorizace. 4. **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á.** 5. **GPS jen u 1/7 fotek** — u většiny iPhone fotek máte vypnuté lokační služby. 6. **Time zone** — fotky mají `OffsetTime: +02:00`. **Datum pořízení ukládat jako `TIMESTAMPTZ`** (s timezone). 7. **ExifRead > Pillow** — Pillow má GPS bug (`'int' object has no attribute 'items'`). **Primární parser bude ExifRead.** 8. **MakerNote (Apple binary blob)** — obsahuje burst ID, HDR příznak, focus distance. Pro rozluštění potřeba [`exiftool`](https://exiftool.org/) (volat přes `pyexiftool`). --- ## 7. Návrh databázového schématu (draft) ```sql CREATE TABLE photos ( id BIGSERIAL PRIMARY KEY, -- identita (3 úrovně) sha256_file CHAR(64) UNIQUE NOT NULL, -- byte identita sha256_pixels CHAR(64), -- pixel identita phash BIGINT, -- vizuální podobnost -- soubor file_path VARCHAR(1000) NOT NULL, file_name VARCHAR(255) NOT NULL, file_size BIGINT, mime_type VARCHAR(50), format VARCHAR(20), -- JPEG, PNG, HEIC, ... width INT, height INT, -- pořízení taken_at TIMESTAMPTZ, -- s timezone (máme OffsetTime!) taken_at_source VARCHAR(20), -- 'exif' / 'mtime' / 'iptc' / 'unknown' -- technika (z EXIF) camera_make VARCHAR(100), camera_model VARCHAR(255), lens_model VARCHAR(255), iso INT, aperture NUMERIC(4,2), exposure_time VARCHAR(20), -- "1/500" focal_length_mm NUMERIC(5,2), -- GPS (NULL pokud chybí) gps_lat NUMERIC(10,7), gps_lon NUMERIC(10,7), gps_altitude NUMERIC(7,2), -- klasifikace is_screenshot BOOLEAN DEFAULT FALSE, face_count INT, -- z XMP, rozšířit AI -- flexibilní JSONB pro celý dump exif_raw JSONB, iptc_raw JSONB, xmp_raw JSONB, -- import / zpracování imported_at TIMESTAMPTZ DEFAULT NOW(), processed_at TIMESTAMPTZ, processing_status VARCHAR(50) DEFAULT 'pending' ); CREATE INDEX idx_photos_sha256_pixels ON photos(sha256_pixels); CREATE INDEX idx_photos_phash ON photos(phash); CREATE INDEX idx_photos_taken_at ON photos(taken_at); CREATE INDEX idx_photos_camera_model ON photos(camera_model); CREATE INDEX idx_photos_exif_gin ON photos USING GIN (exif_raw); CREATE TABLE tags ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, parent_tag_id INT REFERENCES tags(id), -- hierarchie "místo > Praha > Karlův most" UNIQUE(name, parent_tag_id) ); CREATE TABLE photo_tags ( photo_id BIGINT REFERENCES photos(id) ON DELETE CASCADE, tag_id INT REFERENCES tags(id) ON DELETE CASCADE, source VARCHAR(20), -- 'manual' / 'iptc' / 'xmp' / 'auto' created_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (photo_id, tag_id) ); ``` --- ## 8. Otevřené otázky pro příště ### Foto bez EXIF (sirotek typu [7]) - **a)** Importovat (s `taken_at` z `mtime`) - **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:Regions` jdou čí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 projektu (k 2026-05-21) ### Soubory v projektu ``` FotkyBuzalkovi/ ├── demo_fotky/ # 7 ukázkových fotek ├── explore_photos.py # Explorační skript (hashe, EXIF, IPTC, XMP) ├── photo_exploration.json # Výstup exploreru ├── create_schema.py # ZASTARALÉ - obsahuje MySQL syntaxi a hardcoded hesla ├── test_db_connection.py # Test PG + Mongo + Redis (Mongo/Redis nebudou potřeba) ├── test_mongo.py # ZASTARALÉ - Mongo nepoužijeme ├── README.md └── NAVRH.md # Tento dokument ``` ### Co bude potřeba udělat - [ ] Smazat nebo přepsat `create_schema.py` (MySQL syntaxe `INDEX` uvnitř `CREATE TABLE` v PG nefunguje) - [ ] Migrovat hesla do `.env` + `python-dotenv` - [ ] Smazat / archivovat `test_mongo.py` - [ ] Vytvořit migraci podle schématu v sekci 7 - [ ] Skript pro import fotek s deduplikací (workflow v sekci 4) - [ ] Rozhodnout otevřené otázky ze sekce 8 ### Nainstalované Python balíčky ``` psycopg2-binary 2.9.12 # PostgreSQL driver pymongo 4.17.0 # (nepotřebujeme) redis 7.4.0 # (zatím nepotřebujeme) pillow 12.2.0 # Základní práce s obrázky ExifRead 3.5.1 # Primární EXIF parser (lepší než Pillow) imagehash 4.3.2 # Perceptuální hashe (pHash, dHash, wHash) + numpy, scipy, PyWavelets (závislosti imagehash) ``` --- ## 11. Užitečné odkazy - [Pillow](https://pillow.readthedocs.io/) — Python Imaging Library - [ExifRead](https://github.com/ianare/exif-py) — EXIF parser - [imagehash](https://github.com/JohannesBuchner/imagehash) — perceptuální hashe - [exiftool](https://exiftool.org/) — gold standard pro metadata (Perl, ale `pyexiftool` wrapper) - [pgvector](https://github.com/pgvector/pgvector) — vektory v PostgreSQL pro sémantické hledání - [PostgreSQL JSONB docs](https://www.postgresql.org/docs/current/datatype-json.html) - [IPTC Photo Metadata Standard](https://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata) - [XMP Specification (Adobe)](https://www.adobe.com/devnet/xmp.html)