fix: respect config film_type to force negative inversion

NegativeDetector now accepts an optional forced FilmType. When
film_type != "auto" in config.ini, auto-detection is skipped and
the configured type is applied directly. build_pipeline() in
CliRunner maps c41→ColorNegative and bw→BWNegative accordingly.

Default config changed from film_type=auto to film_type=c41 to
match the project's primary use case (C-41 color negatives).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-03-14 09:56:39 +01:00
parent ee016b9a5a
commit e740234a06
4 changed files with 48 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
#include "CliRunner.h"
#include "../converter/pipeline/ImageData.h"
#include "../converter/rawloader/RawLoader.h"
#include "../converter/preprocess/Preprocessor.h"
#include "../converter/negative/NegativeDetector.h"
@@ -269,8 +270,17 @@ std::vector<std::filesystem::path> CliRunner::collect_files(
Pipeline CliRunner::build_pipeline(const AppConfig& app_cfg) {
Pipeline pipeline;
// Resolve forced film type from config.
// "auto" → Unknown (auto-detection), "c41" → ColorNegative, "bw" → BWNegative.
FilmType forced_film = FilmType::Unknown;
if (app_cfg.conversion.film_type == "c41") {
forced_film = FilmType::ColorNegative;
} else if (app_cfg.conversion.film_type == "bw") {
forced_film = FilmType::BWNegative;
}
pipeline.add_stage(std::make_unique<Preprocessor>());
pipeline.add_stage(std::make_unique<NegativeDetector>());
pipeline.add_stage(std::make_unique<NegativeDetector>(forced_film));
if (app_cfg.conversion.invert) {
pipeline.add_stage(std::make_unique<Inverter>());

View File

@@ -13,6 +13,23 @@ StageResult NegativeDetector::process(ImageData data) const {
ErrorCode::DetectionFailed, "NegativeDetector received empty image"));
}
// If the caller forced a specific film type, skip analysis entirely.
if (forced_type_ != FilmType::Unknown) {
data.film_type = forced_type_;
std::cout << std::format("[Detect] Film type forced by config: {}",
[&] {
switch (data.film_type) {
case FilmType::ColorNegative: return "Color Negative (C-41)";
case FilmType::BWNegative: return "B&W Negative";
case FilmType::ColorPositive: return "Color Positive (Slide)";
case FilmType::BWPositive: return "B&W Positive";
default: return "Unknown";
}
}()) << std::endl;
return data;
}
// Auto-detection: histogram + orange mask analysis.
const bool is_negative = is_negative_histogram(data.rgb);
const bool is_bw = is_monochrome(data.rgb);

View File

@@ -13,7 +13,11 @@ namespace photoconv {
* The detected FilmType is stored in ImageData::film_type for use
* by the Invert and Color Correction stages.
*
* Detection strategy:
* When constructed with a forced FilmType (anything other than Unknown),
* the stage skips automatic analysis and sets that type directly.
* This allows the config's film_type setting to override auto-detection.
*
* Detection strategy (auto mode):
* 1. Compute per-channel histograms
* 2. Analyze distribution skewness (negatives have inverted distributions)
* 3. Check for C-41 orange mask (dominant red/orange in unexposed regions)
@@ -27,13 +31,27 @@ public:
/// Minimum saturation to distinguish color from B&W.
static constexpr float kColorSaturationThreshold = 15.0f;
/**
* @brief Construct in auto-detection mode.
*/
NegativeDetector() = default;
/**
* @brief Construct with a forced film type (skips auto-detection).
*
* @param forced_type Film type to force. Use FilmType::Unknown for auto mode.
*/
explicit NegativeDetector(FilmType forced_type) : forced_type_(forced_type) {}
~NegativeDetector() override = default;
[[nodiscard]] StageResult process(ImageData data) const override;
[[nodiscard]] std::string name() const override { return "Detect"; }
private:
/// When set to anything other than Unknown, auto-detection is skipped.
FilmType forced_type_ = FilmType::Unknown;
/**
* @brief Analyze histogram to detect inverted (negative) distribution.
*