Files
fotkyBuzalkovi/NAVRH.md
T
administrator b6aba06baa notebookVb
2026-05-21 07:11:54 +02:00

378 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 5121024 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)