chore: initial project scaffold from architecture design
- Add CLAUDE.md with project overview, tech stack, build commands, architecture description, coding standards, and sample images section - Add full directory structure: src/, docs/, tests/, import/ - Add CMakeLists.txt with C++20, OpenCV/LibRaw/Qt6 dependencies, converter_core static lib, optional GUI, and GTest tests - Add architecture documentation: ARCHITECTURE.md, PIPELINE.md, MODULES.md - Add source skeletons for all pipeline stages: RawLoader, Preprocessor, NegativeDetector, Inverter, ColorCorrector, CropProcessor, OutputWriter, Pipeline, MainWindow, CliRunner, main.cpp - Add initial test stubs for pipeline and rawloader - Add sample ARW files in import/ for integration testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
199
docs/ARCHITECTURE.md
Normal file
199
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Photo-converter is a C++20 cross-platform desktop application that converts digitized analog film negatives (35mm, 120mm) into digital positives. It supports RAW camera formats (CR2, NEF, ARW, DNG, etc.) and standard image formats (JPG, PNG, TIFF), producing high-quality 16-bit output with automatic film type detection, color correction, and cropping.
|
||||
|
||||
The architecture follows **Clean Architecture** principles: the core processing logic has zero GUI dependencies and can run in both interactive (Qt GUI) and batch (CLI) modes.
|
||||
|
||||
## Component Diagram
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| Application Layer |
|
||||
| |
|
||||
| +------------------+ +------------------+ |
|
||||
| | MainWindow | | CliRunner | |
|
||||
| | (Qt GUI) | | (Batch CLI) | |
|
||||
| +--------+---------+ +--------+---------+ |
|
||||
| | | |
|
||||
+------------|------------------------------|----------------------+
|
||||
| |
|
||||
v v
|
||||
+------------------------------------------------------------------+
|
||||
| Pipeline Orchestration |
|
||||
| |
|
||||
| +----------------------------------------------------------+ |
|
||||
| | Pipeline | |
|
||||
| | Owns: vector<unique_ptr<PipelineStage>> | |
|
||||
| | Executes stages in order, propagates errors | |
|
||||
| +----------------------------------------------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------------------+
|
||||
| Pipeline Stages |
|
||||
| |
|
||||
| +----------+ +---------+ +--------+ +--------+ +---------+ |
|
||||
| |RawLoader |->|Preproc. |->|Detect |->|Invert |->|Color | |
|
||||
| |(Loader) | | | | | | | |Corrector| |
|
||||
| +----------+ +---------+ +--------+ +--------+ +---------+ |
|
||||
| | |
|
||||
| +-----------v-----+ |
|
||||
| | CropProcessor | |
|
||||
| | (Post-Process) | |
|
||||
| +-----------+-----+ |
|
||||
| | |
|
||||
| +-----------v-----+ |
|
||||
| | OutputWriter | |
|
||||
| +-----------------+ |
|
||||
+------------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------------------+
|
||||
| Core Data Types |
|
||||
| |
|
||||
| ImageData RawMetadata FilmType Error ErrorCode |
|
||||
| |
|
||||
+------------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------------------+
|
||||
| External Libraries |
|
||||
| |
|
||||
| OpenCV 4.10+ LibRaw 0.21+ Qt 6.8 LTS |
|
||||
| (imgproc, imgcodecs) (RAW demosaicing) (Widgets, GUI) |
|
||||
| |
|
||||
+------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
### Application Layer
|
||||
|
||||
Contains the two entry points (GUI and CLI). This layer is thin -- it constructs the pipeline, feeds it input, and presents results. No image processing logic lives here.
|
||||
|
||||
- **MainWindow** (Qt): File dialogs, preview, progress bar. Depends on Qt 6.8 Widgets.
|
||||
- **CliRunner**: Command-line argument parsing and batch loop. Zero Qt dependency.
|
||||
|
||||
### Pipeline Orchestration
|
||||
|
||||
The `Pipeline` class owns an ordered sequence of `PipelineStage` objects and executes them sequentially. It implements the **Chain of Responsibility** pattern: each stage either transforms the data and passes it forward, or returns an error that stops the chain.
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
Each stage implements the `PipelineStage` interface (`process(ImageData) -> StageResult`). Stages are stateless or hold only configuration. This enables the **Strategy** pattern -- stages can be swapped, reordered, or extended without modifying the Pipeline.
|
||||
|
||||
### Core Data Types
|
||||
|
||||
- `ImageData`: The carrier struct flowing through all stages. Always contains a `cv::Mat` in CV_16UC3 format.
|
||||
- `Error` / `ErrorCode`: Structured error with source location for diagnostics.
|
||||
- `StageResult = std::expected<ImageData, Error>`: The universal return type.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### 1. std::expected over Exceptions
|
||||
|
||||
**Decision:** All error handling uses `std::expected<T, Error>`, never exceptions for control flow.
|
||||
|
||||
**Rationale:**
|
||||
- Explicit error paths in function signatures (self-documenting)
|
||||
- No hidden control flow jumps
|
||||
- Zero-cost when no error occurs
|
||||
- Forces callers to handle errors (compiler warnings with `[[nodiscard]]`)
|
||||
- Consistent with modern C++ idioms (C++23)
|
||||
|
||||
### 2. 16-bit Pipeline Throughout
|
||||
|
||||
**Decision:** All internal processing uses CV_16UC3 (16-bit per channel, 3-channel BGR).
|
||||
|
||||
**Rationale:**
|
||||
- RAW files contain 12-14 bit data; 8-bit would lose dynamic range
|
||||
- Inversion, color correction, and levels adjustments need headroom
|
||||
- Conversion to 8-bit happens only at the final output stage
|
||||
- No information loss in intermediate stages
|
||||
|
||||
### 3. LibRaw RAII Guard
|
||||
|
||||
**Decision:** LibRaw::recycle() is guaranteed via an RAII guard class (`LibRawGuard`).
|
||||
|
||||
**Rationale:**
|
||||
- LibRaw requires explicit cleanup; forgetting recycle() leaks memory
|
||||
- RAII ensures cleanup on all paths (success, error, exception)
|
||||
- The guard is a private implementation detail of RawLoader
|
||||
|
||||
### 4. Core Library Separation
|
||||
|
||||
**Decision:** All processing code is in the `converter_core` static library, which has no Qt dependency.
|
||||
|
||||
**Rationale:**
|
||||
- Enables CLI-only builds without Qt
|
||||
- Makes the core testable without GUI framework
|
||||
- Clean dependency graph: GUI depends on core, never vice versa
|
||||
|
||||
### 5. Value Semantics for ImageData
|
||||
|
||||
**Decision:** `ImageData` is passed by value (moved) through the pipeline.
|
||||
|
||||
**Rationale:**
|
||||
- `cv::Mat` uses reference counting internally, so copies are shallow
|
||||
- Moving `ImageData` is cheap (Mat header + a few scalars)
|
||||
- Avoids shared mutable state between stages
|
||||
- Each stage owns its input; previous stages cannot interfere
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
photo-converter/
|
||||
+-- CMakeLists.txt Root build configuration
|
||||
+-- CLAUDE.md AI agent instructions
|
||||
+-- docs/
|
||||
| +-- ARCHITECTURE.md This file
|
||||
| +-- PIPELINE.md Pipeline stage documentation
|
||||
| +-- MODULES.md Module catalog
|
||||
+-- import/ Sample test images (ARW)
|
||||
+-- output/ Default output directory
|
||||
+-- src/
|
||||
| +-- main.cpp Entry point (GUI/CLI dispatch)
|
||||
| +-- converter/
|
||||
| | +-- pipeline/
|
||||
| | | +-- Pipeline.h/.cpp Pipeline orchestrator
|
||||
| | | +-- PipelineStage.h Stage interface (abstract)
|
||||
| | | +-- ImageData.h Core data structure
|
||||
| | | +-- Error.h Error types
|
||||
| | +-- rawloader/
|
||||
| | | +-- RawLoader.h/.cpp RAW + standard image loading
|
||||
| | +-- preprocess/
|
||||
| | | +-- Preprocessor.h/.cpp Bit depth validation, deskew
|
||||
| | +-- negative/
|
||||
| | | +-- NegativeDetector.h/.cpp Film type classification
|
||||
| | +-- invert/
|
||||
| | | +-- Inverter.h/.cpp Negative-to-positive inversion
|
||||
| | +-- color/
|
||||
| | | +-- ColorCorrector.h/.cpp WB, C-41 correction
|
||||
| | +-- crop/
|
||||
| | | +-- CropProcessor.h/.cpp Auto-crop, levels, sharpen
|
||||
| | +-- output/
|
||||
| | +-- OutputWriter.h/.cpp File output (PNG/TIFF/JPEG)
|
||||
| +-- gui/
|
||||
| | +-- MainWindow.h/.cpp Qt GUI main window
|
||||
| +-- cli/
|
||||
| +-- CliRunner.h/.cpp CLI batch runner
|
||||
+-- tests/
|
||||
+-- CMakeLists.txt Test build configuration
|
||||
+-- test_pipeline.cpp Pipeline and stage unit tests
|
||||
+-- test_rawloader.cpp RawLoader integration tests
|
||||
```
|
||||
|
||||
## Cross-Platform Build
|
||||
|
||||
The project builds on Linux, Windows, and macOS. Platform-specific dependency management:
|
||||
|
||||
| Platform | Package Manager | Command |
|
||||
|----------|----------------|---------|
|
||||
| Linux | apt | `sudo apt install libopencv-dev libraw-dev libqt6widgets6` |
|
||||
| Windows | vcpkg | `vcpkg install opencv libraw qt6-base` |
|
||||
| macOS | Homebrew | `brew install opencv libraw qt@6` |
|
||||
|
||||
The CMake build system handles all three platforms with appropriate find_package/pkg_check_modules calls. The `BUILD_GUI` option allows CLI-only builds when Qt is unavailable.
|
||||
265
docs/MODULES.md
Normal file
265
docs/MODULES.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Module Catalog
|
||||
|
||||
## Module Overview
|
||||
|
||||
| Module | Location | Responsibility | Dependencies |
|
||||
|--------|----------|---------------|--------------|
|
||||
| Pipeline | `src/converter/pipeline/` | Stage orchestration, data types | OpenCV (core) |
|
||||
| RawLoader | `src/converter/rawloader/` | File loading (RAW + standard) | OpenCV, LibRaw |
|
||||
| Preprocessor | `src/converter/preprocess/` | Format normalization, deskew | OpenCV |
|
||||
| NegativeDetector | `src/converter/negative/` | Film type classification | OpenCV |
|
||||
| Inverter | `src/converter/invert/` | Negative-to-positive inversion | OpenCV |
|
||||
| ColorCorrector | `src/converter/color/` | White balance, C-41 correction | OpenCV |
|
||||
| CropProcessor | `src/converter/crop/` | Auto-crop, levels, sharpening | OpenCV |
|
||||
| OutputWriter | `src/converter/output/` | File output | OpenCV |
|
||||
| MainWindow | `src/gui/` | Qt GUI | Qt 6.8, converter_core |
|
||||
| CliRunner | `src/cli/` | CLI batch processing | converter_core |
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
MainWindow ──> Pipeline ──> PipelineStage (interface)
|
||||
| | ^
|
||||
| | |
|
||||
v | +--------+--------+--------+--------+--------+--------+
|
||||
Qt 6.8 | | | | | | | |
|
||||
| Preproc Detect Invert Color Crop Output (future)
|
||||
| | | | | | |
|
||||
+-----+--------+--------+--------+--------+--------+
|
||||
|
|
||||
v
|
||||
RawLoader
|
||||
|
|
||||
+----+----+
|
||||
| |
|
||||
LibRaw OpenCV
|
||||
|
||||
CliRunner ──> Pipeline ──> (same stages as above)
|
||||
|
|
||||
+──> RawLoader
|
||||
```
|
||||
|
||||
## Module Details
|
||||
|
||||
---
|
||||
|
||||
### Pipeline Infrastructure (`src/converter/pipeline/`)
|
||||
|
||||
#### Error.h
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `enum class ErrorCode` | Error classification (30+ codes across all stages) |
|
||||
| `struct Error` | Error with code, message, and source location |
|
||||
| `make_error()` | Factory function with automatic source_location capture |
|
||||
|
||||
#### ImageData.h
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `enum class FilmType` | Film classification: Unknown, ColorNegative, BWNegative, ColorPositive, BWPositive |
|
||||
| `struct RawMetadata` | Camera make/model, ISO, shutter, aperture, WB multipliers, dimensions |
|
||||
| `struct ImageData` | Core carrier: cv::Mat rgb (CV_16UC3), source_path, metadata, film_type, crop_region |
|
||||
|
||||
#### PipelineStage.h
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `using StageResult` | `std::expected<ImageData, Error>` |
|
||||
| `class PipelineStage` | Abstract interface: `process(ImageData) -> StageResult`, `name() -> string` |
|
||||
| `using ProgressCallback` | `function<void(string, float)>` for GUI progress reporting |
|
||||
|
||||
#### Pipeline.h / Pipeline.cpp
|
||||
|
||||
| Class | `Pipeline` |
|
||||
|-------|-----------|
|
||||
| **Pattern** | Chain of Responsibility + Strategy |
|
||||
| **Owns** | `vector<unique_ptr<PipelineStage>>` |
|
||||
| **Methods** | |
|
||||
| `add_stage(unique_ptr<PipelineStage>)` | Append a stage |
|
||||
| `execute(ImageData, ProgressCallback) -> StageResult` | Run all stages sequentially |
|
||||
| `stage_count() -> size_t` | Number of registered stages |
|
||||
| **Thread safety** | Not thread-safe (single-threaded execution) |
|
||||
| **Copyable** | No (deleted copy ctor/assignment) |
|
||||
| **Movable** | Yes |
|
||||
|
||||
---
|
||||
|
||||
### RawLoader (`src/converter/rawloader/`)
|
||||
|
||||
| Class | `RawLoader` |
|
||||
|-------|------------|
|
||||
| **Pattern** | Factory Method (routes to load_raw or load_standard) |
|
||||
| **Methods** | |
|
||||
| `load(path) -> expected<ImageData, Error>` | Main entry point |
|
||||
| `load_raw(path)` | LibRaw loader (private) |
|
||||
| `load_standard(path)` | OpenCV loader (private) |
|
||||
| `is_raw_format(path) -> bool` | Extension check (static) |
|
||||
| `is_standard_format(path) -> bool` | Extension check (static) |
|
||||
| `log_metadata(RawMetadata)` | Print metadata to stdout (static) |
|
||||
| **Constants** | |
|
||||
| `kMaxRawFileSize` | 4 GB (4,294,967,296 bytes) |
|
||||
| `kRawExtensions[]` | cr2, cr3, nef, arw, dng, orf, rw2, raf, pef |
|
||||
| `kStandardExtensions[]` | jpg, jpeg, png, tif, tiff |
|
||||
| **Internal** | |
|
||||
| `LibRawGuard` | RAII class ensuring `LibRaw::recycle()` |
|
||||
|
||||
**LibRaw configuration:**
|
||||
```cpp
|
||||
params.use_camera_wb = 1; // Use camera white balance
|
||||
params.output_bps = 16; // 16-bit output
|
||||
params.no_auto_bright = 1; // No auto-brightness
|
||||
params.half_size = 0; // Full resolution
|
||||
params.output_color = 1; // sRGB color space
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Preprocessor (`src/converter/preprocess/`)
|
||||
|
||||
| Class | `Preprocessor` : `PipelineStage` |
|
||||
|-------|----------------------------------|
|
||||
| **Stage name** | "Preprocess" |
|
||||
| **Methods** | |
|
||||
| `process(ImageData) -> StageResult` | Validate + deskew |
|
||||
| `validate_and_convert(ImageData)` | Ensure CV_16UC3 (static, private) |
|
||||
| `deskew(ImageData)` | Geometric correction (static, private, TODO) |
|
||||
|
||||
---
|
||||
|
||||
### NegativeDetector (`src/converter/negative/`)
|
||||
|
||||
| Class | `NegativeDetector` : `PipelineStage` |
|
||||
|-------|--------------------------------------|
|
||||
| **Stage name** | "Detect" |
|
||||
| **Methods** | |
|
||||
| `process(ImageData) -> StageResult` | Classify film type |
|
||||
| `is_negative_histogram(Mat) -> bool` | Mean intensity analysis (static, private) |
|
||||
| `has_orange_mask(Mat) -> bool` | R/B ratio check (static, private) |
|
||||
| `is_monochrome(Mat) -> bool` | HSV saturation check (static, private) |
|
||||
| **Constants** | |
|
||||
| `kOrangeMaskThreshold` | 1.4f |
|
||||
| `kColorSaturationThreshold` | 15.0f |
|
||||
|
||||
---
|
||||
|
||||
### Inverter (`src/converter/invert/`)
|
||||
|
||||
| Class | `Inverter` : `PipelineStage` |
|
||||
|-------|------------------------------|
|
||||
| **Stage name** | "Invert" |
|
||||
| **Methods** | |
|
||||
| `process(ImageData) -> StageResult` | Route by film type |
|
||||
| `invert_color_negative(ImageData)` | Orange mask removal + bitwise_not (static, private) |
|
||||
| `invert_bw_negative(ImageData)` | Simple bitwise_not (static, private) |
|
||||
|
||||
---
|
||||
|
||||
### ColorCorrector (`src/converter/color/`)
|
||||
|
||||
| Class | `ColorCorrector` : `PipelineStage` |
|
||||
|-------|-------------------------------------|
|
||||
| **Stage name** | "ColorCorrection" |
|
||||
| **Methods** | |
|
||||
| `process(ImageData) -> StageResult` | Route by film type |
|
||||
| `correct_c41(ImageData)` | C-41 orange cast removal (static, private, TODO) |
|
||||
| `auto_white_balance(ImageData)` | Gray-world AWB (static, private) |
|
||||
| `apply_exif_wb(ImageData)` | EXIF-based WB (static, private) |
|
||||
|
||||
---
|
||||
|
||||
### CropProcessor (`src/converter/crop/`)
|
||||
|
||||
| Class | `CropProcessor` : `PipelineStage` |
|
||||
|-------|-------------------------------------|
|
||||
| **Stage name** | "PostProcess" |
|
||||
| **Methods** | |
|
||||
| `process(ImageData) -> StageResult` | Auto-crop + levels + sharpen |
|
||||
| `auto_crop(ImageData)` | Frame detection via contours (static, private, TODO) |
|
||||
| `adjust_levels(ImageData)` | Percentile-based levels (static, private, TODO) |
|
||||
| `sharpen(ImageData)` | Unsharp mask (static, private, TODO) |
|
||||
| **Constants** | |
|
||||
| `kMinFrameAreaRatio` | 0.3 |
|
||||
| `kSharpenSigma` | 1.5 |
|
||||
| `kSharpenStrength` | 0.5 |
|
||||
| `kBlackPointPercentile` | 0.5 |
|
||||
| `kWhitePointPercentile` | 99.5 |
|
||||
|
||||
---
|
||||
|
||||
### OutputWriter (`src/converter/output/`)
|
||||
|
||||
| Class | `OutputWriter` : `PipelineStage` |
|
||||
|-------|-----------------------------------|
|
||||
| **Stage name** | "Output" |
|
||||
| **Constructor** | `OutputWriter(OutputConfig config)` |
|
||||
| **Methods** | |
|
||||
| `process(ImageData) -> StageResult` | Write file to disk |
|
||||
| `build_output_path(string) -> path` | Construct output filename (private) |
|
||||
| `format_extension(OutputFormat) -> string` | Map format to extension (static, private) |
|
||||
| **Config** | |
|
||||
| `OutputConfig::output_dir` | Target directory |
|
||||
| `OutputConfig::format` | PNG_16bit, PNG_8bit, TIFF_16bit, JPEG |
|
||||
| `OutputConfig::jpeg_quality` | 0-100 (default 95) |
|
||||
|
||||
---
|
||||
|
||||
### MainWindow (`src/gui/`)
|
||||
|
||||
| Class | `MainWindow` : `QMainWindow` |
|
||||
|-------|-------------------------------|
|
||||
| **Qt slots** | |
|
||||
| `on_open_files()` | File dialog with RAW filter |
|
||||
| `on_convert()` | Execute pipeline on all selected files |
|
||||
| `on_select_output_dir()` | Directory picker |
|
||||
| **Private** | |
|
||||
| `setup_ui()` | Construct widgets and layout |
|
||||
| `setup_pipeline()` | Build default pipeline |
|
||||
| `update_preview(Mat)` | Convert 16-bit BGR to QPixmap for display |
|
||||
| **State** | |
|
||||
| `input_files_` | Selected file paths |
|
||||
| `output_dir_` | Chosen output directory |
|
||||
| `pipeline_` | Pipeline instance |
|
||||
|
||||
---
|
||||
|
||||
### CliRunner (`src/cli/`)
|
||||
|
||||
| Class | `CliRunner` |
|
||||
|-------|------------|
|
||||
| **Nested types** | `Config` (input_files, output_dir, output_format, jpeg_quality, verbose) |
|
||||
| **Static methods** | |
|
||||
| `parse_args(argc, argv) -> expected<Config, Error>` | Parse CLI arguments |
|
||||
| **Methods** | |
|
||||
| `run(Config) -> expected<int, Error>` | Execute batch processing, return success count |
|
||||
|
||||
**CLI usage:**
|
||||
```
|
||||
photo-converter --cli -i file1.arw file2.cr2 -o output/ --format png16
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Targets
|
||||
|
||||
| Target | Type | Sources | Dependencies |
|
||||
|--------|------|---------|-------------|
|
||||
| `converter_core` | Static library | All `src/converter/` + `src/cli/` | OpenCV, LibRaw |
|
||||
| `photo-converter` | Executable | `src/main.cpp` + `src/gui/` | converter_core, Qt6::Widgets |
|
||||
| `test_pipeline` | Test executable | `tests/test_pipeline.cpp` | converter_core, GTest |
|
||||
| `test_rawloader` | Test executable | `tests/test_rawloader.cpp` | converter_core, GTest |
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Module | Status | Notes |
|
||||
|--------|--------|-------|
|
||||
| Pipeline | Complete | Orchestration, error propagation, progress |
|
||||
| RawLoader | Complete | Full LibRaw + OpenCV loading |
|
||||
| Preprocessor | Skeleton | Validation done, deskew TODO |
|
||||
| NegativeDetector | Functional | Basic histogram + orange mask detection |
|
||||
| Inverter | Skeleton | Basic bitwise_not, C-41 mask removal TODO |
|
||||
| ColorCorrector | Partial | Gray-world AWB implemented, C-41 TODO |
|
||||
| CropProcessor | Skeleton | All sub-stages TODO (pass-through) |
|
||||
| OutputWriter | Complete | All formats supported |
|
||||
| MainWindow | Complete | File dialogs, preview, progress |
|
||||
| CliRunner | Complete | Argument parsing, batch loop |
|
||||
277
docs/PIPELINE.md
Normal file
277
docs/PIPELINE.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Pipeline Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The processing pipeline transforms a digitized film negative into a color-corrected digital positive. Data flows through seven stages, each receiving and returning an `ImageData` struct wrapped in `std::expected`.
|
||||
|
||||
```
|
||||
ImageData flow
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 1. LOADER (RawLoader) |
|
||||
| Input: file path (string) |
|
||||
| Output: ImageData { CV_16UC3, metadata } |
|
||||
+----------------------+-----------------------+
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 2. PREPROCESS (Preprocessor) |
|
||||
| Input: ImageData (possibly wrong depth) |
|
||||
| Output: ImageData { CV_16UC3 guaranteed } |
|
||||
+----------------------+-----------------------+
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 3. DETECT (NegativeDetector) |
|
||||
| Input: ImageData { film_type = Unknown } |
|
||||
| Output: ImageData { film_type = detected } |
|
||||
+----------------------+-----------------------+
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 4. INVERT (Inverter) |
|
||||
| Input: ImageData { negative or positive } |
|
||||
| Output: ImageData { inverted if negative } |
|
||||
+----------------------+-----------------------+
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 5. COLOR (ColorCorrector) |
|
||||
| Input: ImageData { inverted positive } |
|
||||
| Output: ImageData { color-balanced } |
|
||||
+----------------------+-----------------------+
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 6. POST-PROCESS (CropProcessor) |
|
||||
| Input: ImageData { color-corrected } |
|
||||
| Output: ImageData { cropped, sharpened } |
|
||||
+----------------------+-----------------------+
|
||||
|
|
||||
+----------------------v-----------------------+
|
||||
| 7. OUTPUT (OutputWriter) |
|
||||
| Input: ImageData { final } |
|
||||
| Output: ImageData (unchanged) + file on disk |
|
||||
+----------------------+-----------------------+
|
||||
```
|
||||
|
||||
## Core Data Structures
|
||||
|
||||
### ImageData
|
||||
|
||||
```cpp
|
||||
struct ImageData {
|
||||
cv::Mat rgb; // Always CV_16UC3 (16-bit BGR)
|
||||
std::string source_path; // Original file path
|
||||
RawMetadata metadata; // Camera EXIF/RAW data
|
||||
FilmType film_type{FilmType::Unknown}; // Set by Detect stage
|
||||
std::optional<cv::Rect> crop_region; // Set by Post-Process
|
||||
};
|
||||
```
|
||||
|
||||
### RawMetadata
|
||||
|
||||
```cpp
|
||||
struct RawMetadata {
|
||||
std::string camera_make; // "Sony", "Canon", "Nikon"
|
||||
std::string camera_model; // "ILCE-7M3"
|
||||
float iso_speed;
|
||||
float shutter_speed; // seconds
|
||||
float aperture; // f-number
|
||||
float focal_length; // mm
|
||||
float wb_red, wb_green, wb_blue; // White balance multipliers
|
||||
int raw_width, raw_height;
|
||||
int raw_bit_depth;
|
||||
std::string timestamp; // ISO 8601
|
||||
};
|
||||
```
|
||||
|
||||
### FilmType
|
||||
|
||||
```cpp
|
||||
enum class FilmType {
|
||||
Unknown, // Not yet classified
|
||||
ColorNegative, // C-41 process film
|
||||
BWNegative, // Black & white negative
|
||||
ColorPositive, // Slide / E-6 film
|
||||
BWPositive, // Black & white positive
|
||||
};
|
||||
```
|
||||
|
||||
### StageResult
|
||||
|
||||
```cpp
|
||||
using StageResult = std::expected<ImageData, Error>;
|
||||
```
|
||||
|
||||
## Stage Details
|
||||
|
||||
### Stage 1: Loader (RawLoader)
|
||||
|
||||
**Purpose:** Read image files from disk into the pipeline.
|
||||
|
||||
**Input:** File path (`std::filesystem::path`)
|
||||
**Output:** `std::expected<ImageData, Error>`
|
||||
|
||||
**Behavior:**
|
||||
1. Validate file exists and size < 4GB
|
||||
2. Detect format from extension
|
||||
3. For RAW files:
|
||||
- Initialize LibRaw with lossless settings (16-bit, full resolution, sRGB)
|
||||
- Open, unpack, and process (demosaic)
|
||||
- Extract image data as cv::Mat CV_16UC3
|
||||
- Extract all metadata (camera, exposure, WB multipliers)
|
||||
- Log metadata to stdout
|
||||
- Guarantee `LibRaw::recycle()` via RAII guard
|
||||
4. For standard files (JPG/PNG/TIFF):
|
||||
- Load via `cv::imread(IMREAD_UNCHANGED)`
|
||||
- Convert to CV_16UC3 (scale 8-bit to 16-bit if needed)
|
||||
- Populate minimal metadata
|
||||
|
||||
**Supported RAW formats:** CR2, CR3, NEF, ARW, DNG, ORF, RW2, RAF, PEF
|
||||
**Supported standard formats:** JPG, JPEG, PNG, TIF, TIFF
|
||||
|
||||
**Error codes:** `FileNotFound`, `FileTooLarge`, `UnsupportedFormat`, `LibRawInitFailed`, `LibRawUnpackFailed`, `LibRawProcessFailed`, `DemosaicingFailed`, `FileReadError`
|
||||
|
||||
### Stage 2: Preprocess (Preprocessor)
|
||||
|
||||
**Purpose:** Normalize image format and correct geometric distortion.
|
||||
|
||||
**Input:** ImageData (possibly wrong bit depth or channel count)
|
||||
**Output:** ImageData (guaranteed CV_16UC3)
|
||||
|
||||
**Behavior:**
|
||||
1. Validate image is non-empty
|
||||
2. Convert to CV_16UC3 if necessary:
|
||||
- Grayscale -> BGR
|
||||
- 8-bit -> 16-bit (scale x257)
|
||||
- 4-channel -> 3-channel
|
||||
3. Apply deskew correction (future):
|
||||
- Canny edge detection
|
||||
- HoughLinesP for dominant angles
|
||||
- Affine warp if skew > 0.5 degrees
|
||||
|
||||
**Error codes:** `InvalidBitDepth`, `ConversionFailed`
|
||||
|
||||
### Stage 3: Detect (NegativeDetector)
|
||||
|
||||
**Purpose:** Classify the image as negative or positive, color or B&W.
|
||||
|
||||
**Input:** ImageData with `film_type = Unknown`
|
||||
**Output:** ImageData with `film_type` set to one of: `ColorNegative`, `BWNegative`, `ColorPositive`, `BWPositive`
|
||||
|
||||
**Detection algorithm:**
|
||||
1. **Negative detection:** Compare mean intensity to midpoint (32768 for 16-bit). Negatives have high mean intensity because dark scene regions become bright on the film.
|
||||
2. **Orange mask detection:** For negatives, compute R/B channel ratio. C-41 film has an orange dye mask with R/B ratio > 1.4.
|
||||
3. **Monochrome detection:** Convert to HSV, check mean saturation. If saturation < 15, classify as B&W.
|
||||
|
||||
**Named constants:**
|
||||
- `kOrangeMaskThreshold = 1.4f`
|
||||
- `kColorSaturationThreshold = 15.0f`
|
||||
|
||||
**Error codes:** `DetectionFailed`, `HistogramError`
|
||||
|
||||
### Stage 4: Invert (Inverter)
|
||||
|
||||
**Purpose:** Convert negatives to positives via bitwise inversion.
|
||||
|
||||
**Input:** ImageData with film_type set
|
||||
**Output:** ImageData (inverted if negative, unchanged if positive)
|
||||
|
||||
**Behavior by film type:**
|
||||
- **ColorNegative:** Remove orange mask, then `cv::bitwise_not()`
|
||||
- **BWNegative:** Simple `cv::bitwise_not()`
|
||||
- **ColorPositive / BWPositive:** Pass through unchanged
|
||||
|
||||
**Error codes:** `InversionFailed`
|
||||
|
||||
### Stage 5: Color Correction (ColorCorrector)
|
||||
|
||||
**Purpose:** Remove color casts and balance white.
|
||||
|
||||
**Input:** ImageData (inverted positive)
|
||||
**Output:** ImageData (color-balanced)
|
||||
|
||||
**Behavior by film type:**
|
||||
- **ColorNegative:** C-41 correction (LAB-space orange removal) + auto WB
|
||||
- **ColorPositive:** Auto white balance only
|
||||
- **BWNegative / BWPositive:** Skipped (no color to correct)
|
||||
|
||||
**Auto white balance algorithm (Gray World):**
|
||||
1. Compute mean of each BGR channel
|
||||
2. Compute overall gray mean = (B + G + R) / 3
|
||||
3. Scale each channel: `ch *= gray_mean / ch_mean`
|
||||
4. Clamp to [0, 65535]
|
||||
|
||||
**Error codes:** `ColorCorrectionFailed`, `WhiteBalanceFailed`
|
||||
|
||||
### Stage 6: Post-Process (CropProcessor)
|
||||
|
||||
**Purpose:** Auto-crop, levels adjustment, and sharpening.
|
||||
|
||||
**Input:** ImageData (color-corrected)
|
||||
**Output:** ImageData (cropped, levels-adjusted, sharpened)
|
||||
|
||||
**Sub-stages (executed in order):**
|
||||
|
||||
1. **Auto-crop:**
|
||||
- Edge detection (Canny) on grayscale
|
||||
- Contour analysis to find largest rectangle
|
||||
- Validate area > 30% of total (not a noise contour)
|
||||
- Crop to bounding rect
|
||||
|
||||
2. **Levels adjustment:**
|
||||
- Compute cumulative histogram per channel
|
||||
- Black point at 0.5th percentile
|
||||
- White point at 99.5th percentile
|
||||
- Remap: `output = (input - black) * 65535 / (white - black)`
|
||||
|
||||
3. **Sharpening (unsharp mask):**
|
||||
- Gaussian blur with sigma = 1.5
|
||||
- `sharpened = original + 0.5 * (original - blurred)`
|
||||
- Clamp to 16-bit range
|
||||
|
||||
**Named constants:**
|
||||
- `kMinFrameAreaRatio = 0.3`
|
||||
- `kSharpenSigma = 1.5`
|
||||
- `kSharpenStrength = 0.5`
|
||||
- `kBlackPointPercentile = 0.5`
|
||||
- `kWhitePointPercentile = 99.5`
|
||||
|
||||
**Error codes:** `CropFailed`, `FrameDetectionFailed`, `SharpeningFailed`
|
||||
|
||||
### Stage 7: Output (OutputWriter)
|
||||
|
||||
**Purpose:** Write the final image to disk.
|
||||
|
||||
**Input:** ImageData + OutputConfig (directory, format, quality)
|
||||
**Output:** ImageData (unchanged) + file written to disk
|
||||
|
||||
**Supported output formats:**
|
||||
|
||||
| Format | Extension | Bit Depth | Compression | Use Case |
|
||||
|--------|-----------|-----------|-------------|----------|
|
||||
| PNG 16-bit | .png | 16-bit | Lossless | Archival quality |
|
||||
| PNG 8-bit | .png | 8-bit | Lossless | Web/sharing |
|
||||
| TIFF 16-bit | .tif | 16-bit | None | Professional editing |
|
||||
| JPEG | .jpg | 8-bit | Lossy | Quick preview |
|
||||
|
||||
**Output filename:** `{stem}_converted.{ext}` (e.g., `DSC09246_converted.png`)
|
||||
|
||||
**Error codes:** `OutputWriteFailed`, `OutputPathInvalid`
|
||||
|
||||
## Error Propagation
|
||||
|
||||
Every stage returns `std::expected<ImageData, Error>`. The Pipeline executes stages sequentially and stops at the first error:
|
||||
|
||||
```
|
||||
Stage 1 OK -> Stage 2 OK -> Stage 3 ERROR -> [stop, return error]
|
||||
```
|
||||
|
||||
The `Error` struct contains:
|
||||
- `ErrorCode code` -- machine-readable classification
|
||||
- `std::string message` -- human-readable, actionable description
|
||||
- `std::string source_file` -- source file where error originated
|
||||
- `int source_line` -- line number for diagnostics
|
||||
|
||||
## Memory Constraints
|
||||
|
||||
- Maximum RAW file size: **4 GB** (validated at load time)
|
||||
- A 16-bit, 6000x4000 pixel image occupies ~144 MB in memory
|
||||
- The pipeline processes one image at a time; batch processing is sequential
|
||||
- `cv::Mat` uses reference counting; stage handoffs are efficient (move semantics)
|
||||
Reference in New Issue
Block a user