Files
negative-converter/docs/PIPELINE.md
Christoph K. 65b411b23d 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>
2026-03-14 09:28:32 +01:00

9.5 KiB

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

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

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

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

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)