notebookVb
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user