# 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 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; ``` ## Stage Details ### Stage 1: Loader (RawLoader) **Purpose:** Read image files from disk into the pipeline. **Input:** File path (`std::filesystem::path`) **Output:** `std::expected` **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`. 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)