- 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>
278 lines
9.5 KiB
Markdown
278 lines
9.5 KiB
Markdown
# 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)
|