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:
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