diff --git a/CMakeLists.txt b/CMakeLists.txt index 30274675..a650a339 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set( RAWTOACES_MAJOR_VERSION 2 ) set( RAWTOACES_MINOR_VERSION 1 ) -set( RAWTOACES_PATCH_VERSION 0 ) +set( RAWTOACES_PATCH_VERSION 1 ) set( RAWTOACES_VERSION ${RAWTOACES_MAJOR_VERSION}.${RAWTOACES_MINOR_VERSION}.${RAWTOACES_PATCH_VERSION} ) set(RAWTOACES_CORE_LIB "rawtoaces_core") diff --git a/README.md b/README.md index b1d2838d..1e27e280 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ A help message with a description of all command line options can be obtained by --highlight-mode VAL 0 = clip, 1 = unclip, 2 = blend, 3..9 = rebuild. (default: 0) --crop-box X Y W H Apply custom crop. If not present, the default crop is applied, which should match the crop of the in-camera JPEG. --crop-mode STR Cropping mode. Supported options: 'none' (write out the full sensor area), 'soft' (write out full image, mark the crop as the display window), 'hard' (write out only the crop area). (default: soft) - --flip VAL If not 0, override the orientation specified in the metadata. 1..8 correspond to EXIF orientation codes (3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.) (default: 0) + --flip VAL If not -1, override the orientation specified in the metadata. 1..8 correspond to EXIF orientation codes (0 = none, 3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.) (default: -1) --denoise-threshold VAL Wavelet denoising threshold (default: 0) --demosaic STR Demosaicing algorithm. Supported options: 'linear', 'VNG', 'PPG', 'AHD', 'DCB', 'AHD-Mod', 'AFD', 'VCD', 'Mixed', 'LMMSE', 'AMaZE', 'DHT', 'AAHD', 'AHD'. (default: AHD) Benchmarking and debugging: diff --git a/docs/CHANGES.md b/docs/CHANGES.md index 835fc23b..04dac69c 100644 --- a/docs/CHANGES.md +++ b/docs/CHANGES.md @@ -1,4 +1,17 @@ -Release 2.1.0 (March ?? 2026) -- compared to 2.0.0 +Release 2.1.1 (June ?? 2026) -- compared to 2.1.0 +-------------------------------------------------------- + + +**This version is API-compatible and ABI-compatible with the previous version.** + +### Changes: + +- *fix*: fix colour tint when processing DNG images [#280](https://github.com/AcademySoftwareFoundation/rawtoaces/pull/280) +- *fix*: allow custom white-balancing weights in DNG mode [#272](https://github.com/AcademySoftwareFoundation/rawtoaces/pull/272) +- *fix*: fix default orientation and box white balance [#268](https://github.com/AcademySoftwareFoundation/rawtoaces/pull/268) +- *fix*: add homebrew location to default DB search path [#264](https://github.com/AcademySoftwareFoundation/rawtoaces/pull/264) + +Release 2.1.0 (March 18 2026) -- compared to 2.0.0 -------------------------------------------------------- diff --git a/docs/CREDITS.md b/docs/CREDITS.md index 9cf3b9a0..12007709 100644 --- a/docs/CREDITS.md +++ b/docs/CREDITS.md @@ -14,6 +14,7 @@ by first name. - Miaoqi Zhu (@miaoqi) - Mikael Sundell (@mikaelsundell) - Pavan Madduri (@pmady) +- Rémi Achard (@remia) - Reto (@retokromer) - Scott Dyer (@scottdyer) - Sean Cooper (@scoopxyz) diff --git a/include/rawtoaces/image_converter.h b/include/rawtoaces/image_converter.h index 21a886ef..d9e37085 100644 --- a/include/rawtoaces/image_converter.h +++ b/include/rawtoaces/image_converter.h @@ -188,10 +188,10 @@ class ImageConverter /// 0 = clip, 1 = unclip, 2 = blend, 3..9 = rebuild. int highlight_mode = 0; - /// If not 0, override the orientation specified in the metadata. + /// If not -1, override the orientation specified in the metadata. /// 1..8 correspond to EXIF orientation codes - /// (3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.) - int flip = 0; + /// (0 = none, 3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.) + int flip = -1; /// Apply custom crop. If not specified (all values are zeroes), /// the default crop is applied, which should match the crop of the diff --git a/src/docs/api/python/image_converter.rst b/src/docs/api/python/image_converter.rst index d66e9e41..4d359176 100644 --- a/src/docs/api/python/image_converter.rst +++ b/src/docs/api/python/image_converter.rst @@ -121,9 +121,9 @@ ImageConverter .. py:attribute:: int flip - If not 0, override the orientation specified in the metadata. + If not -1, override the orientation specified in the metadata. 1..8 correspond to EXIF orientation codes - (3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.) + (0 = none, 3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.) .. py:attribute:: float denoise_threshold diff --git a/src/rawtoaces_core/mathOps.h b/src/rawtoaces_core/mathOps.h index 03521610..a91acd9e 100644 --- a/src/rawtoaces_core/mathOps.h +++ b/src/rawtoaces_core/mathOps.h @@ -414,35 +414,54 @@ template vector XYZ_to_uv( const vector &XYZ ) template std::vector> calculate_CAT( - const std::vector &src_white_XYZ, const std::vector &dst_white_XYZ ) + const std::vector &src_white_XYZ, + const std::vector &dst_white_XYZ, + bool use_bradford ) { assert( src_white_XYZ.size() == 3 ); assert( dst_white_XYZ.size() == 3 ); // clang-format off - // Color Adaptation Matrices - CAT02 (default) + + // Color Adaptation Matrices - Bradford + static const std::vector> Bradford = { + { 0.8951, 0.2664, -0.1614 }, + { -0.7502, 1.7135, 0.0367 }, + { 0.0389, -0.0685, 1.0296 } + }; + + static const std::vector> Bradford_inv = { + { 0.98699290546671225, -0.14705425642099007, 0.15996265166373122 }, + { 0.43230526972339445, 0.51836027153677744, 0.049291228212855594 }, + { -0.0085286645751773294, 0.040042821654084869, 0.96848669578754998 } + }; + + // Color Adaptation Matrices - CAT02 static const std::vector> CAT02 = { { 0.7328, 0.4296, -0.1624 }, { -0.7036, 1.6975, 0.0061 }, { 0.0030, 0.0136, 0.9834 } }; - + static const std::vector> CAT02_inv = { - { 1.0961238208355142, -0.27886900021828726, 0.18274517938277304 }, + { 1.0961238208355142, -0.27886900021828726, 0.18274517938277304 }, { 0.45436904197535921, 0.47353315430741177, 0.072097803717229125 }, - { -0.0096276087384293551, -0.0056980312161134198, 1.0153256399545427 } + { -0.0096276087384293551, -0.0056980312161134198, 1.0153256399545427 } }; // clang-format on - std::vector src_white_LMS = mulVector( src_white_XYZ, CAT02 ); - std::vector dst_white_LMS = mulVector( dst_white_XYZ, CAT02 ); + const auto &M1 = use_bradford ? Bradford : CAT02; + const auto &M2 = use_bradford ? Bradford_inv : CAT02_inv; + + std::vector src_white_LMS = mulVector( src_white_XYZ, M1 ); + std::vector dst_white_LMS = mulVector( dst_white_XYZ, M1 ); std::vector> mat( 3, std::vector( 3, 0 ) ); for ( size_t i = 0; i < 3; i++ ) mat[i][i] = dst_white_LMS[i] / src_white_LMS[i]; - mat = mulVector( mat, transposeVec( CAT02 ) ); - mat = mulVector( CAT02_inv, transposeVec( mat ) ); + mat = mulVector( mat, transposeVec( M1 ) ); + mat = mulVector( M2, transposeVec( mat ) ); return mat; } diff --git a/src/rawtoaces_core/rawtoaces_core.cpp b/src/rawtoaces_core/rawtoaces_core.cpp index dec09949..3864c9d5 100644 --- a/src/rawtoaces_core/rawtoaces_core.cpp +++ b/src/rawtoaces_core/rawtoaces_core.cpp @@ -259,8 +259,16 @@ SpectralSolver::collect_data_files( const std::string &type ) const } else { - std::cerr << "Warning: Database location '" << directory - << "' is not a directory." << std::endl; + if ( std::filesystem::exists( directory ) ) + { + std::cerr << "Warning: Database location '" << directory + << "' is not a directory." << std::endl; + } + else + { + std::cerr << "Warning: Database location '" << directory + << "' does not exist." << std::endl; + } } } return result; @@ -589,7 +597,8 @@ std::vector> calculate_XYZ( ( observer_z * illuminant_spectrum ).integrate() / y; XYZ = mulVector( - XYZ, calculate_CAT( source_white_point, reference_white_point ) ); + XYZ, + calculate_CAT( source_white_point, reference_white_point, false ) ); return XYZ; } @@ -1155,9 +1164,6 @@ vector matrix_RGB_to_XYZ( const double chromaticities[][2] ) /// optimization, then calculates the white point either from the neutral RGB /// values or from the calibration illuminant's color temperature. /// -/// The function also applies baseline exposure compensation and normalizes -/// the white point to ensure proper color scaling in the transformation pipeline. -/// /// @param metadata Camera metadata containing calibration and exposure information /// @param out_camera_to_XYZ_matrix Output camera to XYZ transformation matrix /// @param out_camera_XYZ_white_point Output camera white point in XYZ space @@ -1171,9 +1177,6 @@ void get_camera_XYZ_matrix_and_white_point( invertV( find_XYZ_to_camera_matrix( metadata, metadata.neutral_RGB ) ); assert( std::fabs( sumVector( out_camera_to_XYZ_matrix ) - 0.0 ) > 1e-09 ); - scaleVector( - out_camera_to_XYZ_matrix, std::pow( 2.0, metadata.baseline_exposure ) ); - if ( metadata.neutral_RGB.size() > 0 ) { out_camera_XYZ_white_point = @@ -1202,38 +1205,62 @@ vector> MetadataSolver::calculate_CAT_matrix() vector output_XYZ_white_point = mulVector( output_RGB_to_XYZ_matrix, deviceWhiteV ); vector> CAT_matrix = - calculate_CAT( camera_XYZ_white_point, output_XYZ_white_point ); + calculate_CAT( camera_XYZ_white_point, output_XYZ_white_point, true ); return CAT_matrix; } -vector> MetadataSolver::calculate_IDT_matrix() +/// Colour space transform from XYZ D60 to ACES +const std::vector> &get_XYZ_D60_to_ACES() { - // 1. Obtains the CAT matrix for white point adaptation - vector> CAT_matrix = calculate_CAT_matrix(); + // clang-format off + static const std::vector> XYZ_D60_to_ACES = { + { 1.0498110175, 0.0000000000, -0.0000974845 }, + { -0.4959030231, 1.3733130458, 0.0982400361 }, + { 0.0000000000, 0.0000000000, 0.9912520182 } + }; + // clang-format on - // 2. Converts the CAT matrix to a flattened format for matrix multiplication - vector XYZ_D65_acesrgb( 9 ), CAT( 9 ); - for ( size_t i = 0; i < 3; i++ ) - for ( size_t j = 0; j < 3; j++ ) - { - XYZ_D65_acesrgb[i * 3 + j] = XYZ_D65_acesrgb_3[i][j]; - CAT[i * 3 + j] = CAT_matrix[i][j]; - } + return XYZ_D60_to_ACES; +} - // 3. Multiplies the D65 ACES RGB to XYZ matrix with the CAT matrix - vector matrix = mulVector( XYZ_D65_acesrgb, CAT, 3 ); +std::vector> MetadataSolver::calculate_IDT_matrix() +{ + std::vector camera_to_XYZ_matrix; + std::vector camera_XYZ_white_point; + + get_camera_XYZ_matrix_and_white_point( + _metadata, camera_to_XYZ_matrix, camera_XYZ_white_point ); - // 4. Reshapes the result into a 3×3 transformation matrix - vector> DNG_IDT_matrix( 3, vector( 3 ) ); + assert( camera_to_XYZ_matrix.size() == 9 ); + assert( camera_XYZ_white_point.size() == 3 ); + + std::vector deviceWhiteV( 3, 1.0 ); + std::vector output_RGB_to_XYZ_matrix = + matrix_RGB_to_XYZ( chromaticitiesACES ); + std::vector output_XYZ_white_point = + mulVector( output_RGB_to_XYZ_matrix, deviceWhiteV ); + std::vector> CAT_matrix = + calculate_CAT( camera_XYZ_white_point, output_XYZ_white_point, true ); + + std::vector> camera_to_XYZ( 3, vector( 3 ) ); for ( size_t i = 0; i < 3; i++ ) for ( size_t j = 0; j < 3; j++ ) - DNG_IDT_matrix[i][j] = matrix[i * 3 + j]; + camera_to_XYZ[i][j] = camera_to_XYZ_matrix[i * 3 + j]; + + // The camera_to_XYZ_matrix expects camera raw values, but the pixels + // we get from libraw are white-balanced. Undo the white-balancing as + // the first step. + std::vector> result( 3, std::vector( 3 ) ); + result[0][0] = _metadata.neutral_RGB[0]; + result[1][1] = _metadata.neutral_RGB[1]; + result[2][2] = _metadata.neutral_RGB[2]; - // 5. Validates the matrix properties (non-zero determinant) - assert( std::fabs( sumVectorM( DNG_IDT_matrix ) - 0.0 ) > 1e-09 ); + result = mulVector( camera_to_XYZ, result ); + result = mulVector( CAT_matrix, transposeVec( result ) ); + result = mulVector( get_XYZ_D60_to_ACES(), transposeVec( result ) ); - return DNG_IDT_matrix; + return result; } /// Cost function operator for Ceres optimization of IDT matrix parameters. diff --git a/src/rawtoaces_util/image_converter.cpp b/src/rawtoaces_util/image_converter.cpp index e376522b..1edd7b79 100644 --- a/src/rawtoaces_util/image_converter.cpp +++ b/src/rawtoaces_util/image_converter.cpp @@ -223,6 +223,12 @@ std::vector database_paths( const std::string &override_path = "" ) #if defined( WIN32 ) || defined( WIN64 ) const std::string separator = ";"; const std::string default_path = "."; +#elif defined( __APPLE__ ) + const std::string separator = ":"; + const std::string legacy_path = "/usr/local/include/rawtoaces/data"; + const std::string default_path = + "/usr/local/share/rawtoaces/data" + separator + + "/opt/homebrew/share/rawtoaces/data" + separator + legacy_path; #else const std::string separator = ":"; const std::string legacy_path = "/usr/local/include/rawtoaces/data"; @@ -506,6 +512,7 @@ bool prepare_transform_spectral( bool prepare_transform_DNG( const OIIO::ImageSpec &image_spec, const ImageConverter::Settings &settings, + const std::vector &wb_multipliers, std::vector> &IDT_matrix, std::vector> &CAT_matrix, std::string &error_message ) @@ -520,15 +527,30 @@ bool prepare_transform_DNG( image_spec.get_float_attribute( "raw:dng:baseline_exposure" ) ); // Step 2: Extract neutral RGB values from camera multipliers - metadata.neutral_RGB.resize( 3 ); + if ( wb_multipliers.size() == 4 ) + { + double r = wb_multipliers[0]; + double g = wb_multipliers[1]; + double b = wb_multipliers[2]; + double g2 = wb_multipliers[3]; + + if ( std::abs( g2 ) > 1e-9 ) + { + g = ( g + g2 ) * 0.5; + } + + assert( std::abs( r ) > 1e-9 ); + assert( std::abs( g ) > 1e-9 ); + assert( std::abs( b ) > 1e-9 ); - auto attr = image_spec.find_attribute( - "raw:cam_mul", OIIO::TypeDesc( OIIO::TypeDesc::FLOAT, 4 ) ); - if ( attr ) + metadata.neutral_RGB.resize( 3 ); + metadata.neutral_RGB[0] = 1.0 / r; + metadata.neutral_RGB[1] = 1.0 / g; + metadata.neutral_RGB[2] = 1.0 / b; + } + else { - for ( int i = 0; i < 3; i++ ) - metadata.neutral_RGB[i] = - 1.0 / static_cast( attr->get_float_indexed( i ) ); + metadata.neutral_RGB.resize( 0 ); } // Step 3: Extract calibration data for two illuminants @@ -989,11 +1011,11 @@ void ImageConverter::init_parser( OIIO::ArgParse &arg_parser ) arg_parser.arg( "--flip" ) .help( - "If not 0, override the orientation specified in the metadata. " + "If not -1, override the orientation specified in the metadata. " "1..8 correspond to EXIF orientation codes " - "(3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.)" ) + "(0 = none, 3 = 180 deg, 6 = 90 deg CCW, 8 = 90 deg CW.)" ) .metavar( "VAL" ) - .defaultval( 0 ) + .defaultval( -1 ) .action( OIIO::ArgParse::store() ); arg_parser.arg( "--denoise-threshold" ) @@ -1818,8 +1840,13 @@ bool ImageConverter::configure( settings.chromatic_aberration ); } - bool is_DNG = - image_spec.extra_attribs.find( "raw:dng:version" )->get_int() > 0; + bool is_DNG = false; + auto dng_version_attribute = image_spec.find_attribute( + "raw:dng:version", OIIO::TypeDesc( OIIO::TypeDesc::INT ) ); + if ( dng_version_attribute ) + { + is_DNG = dng_version_attribute->get_int() > 0; + } bool require_spectral = settings.WB_method == Settings::WBMethod::Illuminant || @@ -1877,12 +1904,9 @@ bool ImageConverter::configure( bool is_empty_box = settings.WB_box[2] == 0 || settings.WB_box[3] == 0; - if ( is_empty_box ) - { - // use whole image (auto white balancing) - options["raw:use_auto_wb"] = 1; - } - else + options["raw:use_auto_wb"] = 1; + + if ( !is_empty_box ) { int32_t WB_box[4]; for ( int i = 0; i < 4; i++ ) @@ -1954,8 +1978,16 @@ bool ImageConverter::configure( options["raw:use_camera_matrix"] = 0; break; case Settings::MatrixMethod::Metadata: - options["raw:ColorSpace"] = "XYZ"; - options["raw:use_camera_matrix"] = is_DNG ? 1 : 3; + if ( is_DNG ) + { + options["raw:ColorSpace"] = "raw"; + options["raw:use_camera_matrix"] = 0; + } + else + { + options["raw:ColorSpace"] = "XYZ"; + options["raw:use_camera_matrix"] = 3; + } break; case Settings::MatrixMethod::Adobe: options["raw:ColorSpace"] = "XYZ"; @@ -2029,12 +2061,12 @@ bool ImageConverter::configure( if ( is_DNG ) { options["raw:use_camera_matrix"] = 1; - options["raw:use_camera_wb"] = 1; std::string error_msg; if ( !prepare_transform_DNG( image_spec, settings, + _wb_multipliers, _idt_matrix, _cat_matrix, error_msg ) ) diff --git a/tests/python/test_core_bindings.py b/tests/python/test_core_bindings.py index 7b2f3d82..960d831d 100644 --- a/tests/python/test_core_bindings.py +++ b/tests/python/test_core_bindings.py @@ -127,42 +127,39 @@ def test_metadata_calibration_requires_two_entries(self): metadata.calibration = [calibration] assert str(exc_info.value) == "The calibration array must contain 2 elements." - +def compare_mat(m1, m2): + assert len(m1) == len(m2) + for i in range(len(m1)): + assert len(m1[i]) == len(m2[i]) + for j in range(len(m1[i])): + assert abs(m1[i][j] - m2[i][j]) < 1e-5 + +def true_cat(): + return [ + [ 0.99249998682567019, -0.0029995338200207045, 0.019985821819872025 ], + [ -0.0023403202593997633, 0.99648084986598617, 0.006295370469840196 ], + [ 0.0043810803525381123, -0.0076107605403616758, 1.1122427032269608 ] + ] + +def true_idt(): + return [ + [ 0.6820640403922289, 0.21830620601468764, 0.097819588932001586 ], + [ -0.010414610202425199, 0.99916462206472656, 0.0094398234395704668 ], + [ -0.088115635108323973, -0.49312503931360652, 1.5794305097175558 ] + ] + class TestMetadataSolverBindings: def test_metadata_solver_calculate_cat_matrix(self): metadata = _init_reference_metadata() solver = rawtoaces.MetadataSolver(metadata) cat = solver.calculate_CAT_matrix() - - assert len(cat) == 3 - assert len(cat[0]) == 3 - assert abs(cat[0][0] - 0.9907763427) < 1e-5 - assert abs(cat[0][1] - -0.0022862289) < 1e-5 - assert abs(cat[0][2] - 0.0209908807) < 1e-5 - assert abs(cat[1][0] - -0.0017882434) < 1e-5 - assert abs(cat[1][1] - 0.9941341374) < 1e-5 - assert abs(cat[1][2] - 0.0083008330) < 1e-5 - assert abs(cat[2][0] - 0.0003777587) < 1e-5 - assert abs(cat[2][1] - 0.0015609315) < 1e-5 - assert abs(cat[2][2] - 1.1063201101) < 1e-5 + compare_mat(cat, true_cat()) def test_metadata_solver_calculate_idt_matrix(self): metadata = _init_reference_metadata() solver = rawtoaces.MetadataSolver(metadata) idt = solver.calculate_IDT_matrix() - - assert len(idt) == 3 - assert len(idt[0]) == 3 - assert abs(idt[0][0] - 1.0536466144) < 1e-5 - assert abs(idt[0][1] - 0.0039044182) < 1e-5 - assert abs(idt[0][2] - 0.0049084502) < 1e-5 - assert abs(idt[1][0] - -0.4899562165) < 1e-5 - assert abs(idt[1][1] - 1.3614787986) < 1e-5 - assert abs(idt[1][2] - 0.1020844728) < 1e-5 - assert abs(idt[2][0] - -0.0024498461) < 1e-5 - assert abs(idt[2][1] - 0.0060497128) < 1e-5 - assert abs(idt[2][2] - 1.0139159537) < 1e-5 - + compare_mat(idt, true_idt()) class TestSpectralSolverBindings: def test_spectral_solver_default_constructor(self): diff --git a/tests/python/test_image_converter.py b/tests/python/test_image_converter.py index a75cbb51..3ecde567 100644 --- a/tests/python/test_image_converter.py +++ b/tests/python/test_image_converter.py @@ -136,18 +136,19 @@ def test_configure_metadata(self): # bindings, we'll just check that either of the two paths was # successful. if len(idt) == 3 and len(cat) == 0: - assert len(idt[0]) == 3 - assert abs(idt[0][0] - 1.0536466144250152) < 0.0001 - assert abs(idt[0][1] - 0.00390441818863832) < 0.0001 - assert abs(idt[0][2] - 0.004908450238340354) < 0.0001 - assert len(idt[1]) == 3 - assert abs(idt[1][0] - -0.48995621645381615) < 0.0001 - assert abs(idt[1][1] - 1.3614787985962031) < 0.0001 - assert abs(idt[1][2] - 0.10208447284831194) < 0.0001 - assert len(idt[2]) == 3 - assert abs(idt[2][0] - -0.0024498461419844484) < 0.0001 - assert abs(idt[2][1] - 0.006049712791275535) < 0.0001 - assert abs(idt[2][2] - 1.013915953697747) < 0.0001 + mat = [ + [ 0.6820640403922289, 0.21830620601468764, 0.097819588932001586 ], + [ -0.010414610202425199, 0.99916462206472656, 0.0094398234395704668 ], + [ -0.088115635108323973, -0.49312503931360652, 1.5794305097175558 ] + ] + + assert len(idt) == 3 + + for i in range(3): + assert len(idt[i]) == 3 + for j in range(3): + v = mat[i][j] + assert abs(idt[i][j] - v) < 0.0001 elif len(idt) == 0 and len(cat) == 3: assert len(cat[0]) == 3 assert abs(cat[0][0] - 1.0097583639200136) < 0.0001 diff --git a/tests/testDNGIdt.cpp b/tests/testDNGIdt.cpp index 7550799c..ad670a30 100644 --- a/tests/testDNGIdt.cpp +++ b/tests/testDNGIdt.cpp @@ -266,9 +266,12 @@ void testIDT_GetDNGCATMatrix() rta::core::Metadata metadata; init_metadata( metadata ); rta::core::MetadataSolver *di = new rta::core::MetadataSolver( metadata ); - double matrix[3][3] = { { 0.9907763427, -0.0022862289, 0.0209908807 }, - { -0.0017882434, 0.9941341374, 0.0083008330 }, - { 0.0003777587, 0.0015609315, 1.1063201101 } }; + double matrix[3][3] = { + { 0.99249998682567019, -0.0029995338200207045, 0.019985821819872025 }, + { -0.0023403202593997633, 0.99648084986598617, 0.006295370469840196 }, + { 0.0043810803525381123, -0.0076107605403616758, 1.1122427032269608 } + }; + std::vector> result = di->calculate_CAT_matrix(); delete di; @@ -283,9 +286,12 @@ void testIDT_GetDNGIDTMatrix() rta::core::Metadata metadata; init_metadata( metadata ); rta::core::MetadataSolver *di = new rta::core::MetadataSolver( metadata ); - double matrix[3][3] = { { 1.0536466144, 0.0039044182, 0.0049084502 }, - { -0.4899562165, 1.3614787986, 0.1020844728 }, - { -0.0024498461, 0.0060497128, 1.0139159537 } }; + double matrix[3][3] = { + { 0.6820640403922289, 0.21830620601468764, 0.097819588932001586 }, + { -0.010414610202425199, 0.99916462206472656, 0.0094398234395704668 }, + { -0.088115635108323973, -0.49312503931360652, 1.5794305097175558 } + }; + std::vector> result = di->calculate_IDT_matrix(); delete di; diff --git a/tests/testLogic.cpp b/tests/testLogic.cpp index 4bd8a9b1..58a8c33d 100644 --- a/tests/testLogic.cpp +++ b/tests/testLogic.cpp @@ -23,7 +23,8 @@ void test_getCAT() vector src( D65, D65 + 3 ); vector des( D60, D60 + 3 ); - vector> final_Output_getCAT = calculate_CAT( src, des ); + std::vector> final_Output_getCAT = + calculate_CAT( src, des, false ); vector destination( 3, 0 ); diff --git a/tests/testMath.cpp b/tests/testMath.cpp index 3b2b7d5e..9e5c21d4 100644 --- a/tests/testMath.cpp +++ b/tests/testMath.cpp @@ -363,7 +363,8 @@ void testIDT_GetCAT() vector dIV( d50_white_point_XYZ, d50_white_point_XYZ + 3 ); vector dOV( d60_white_point_XYZ, d60_white_point_XYZ + 3 ); - vector> CAT_test = calculate_CAT( dIV, dOV ); + std::vector> CAT_test = + calculate_CAT( dIV, dOV, false ); float CAT[3][3] = { { 0.9711790957f, -0.0217386019f, 0.0460288393f }, { -0.0156935400f, 1.0000112293f, 0.0183278569f }, diff --git a/tests/test_image_converter.cpp b/tests/test_image_converter.cpp index 727eb397..2423f071 100644 --- a/tests/test_image_converter.cpp +++ b/tests/test_image_converter.cpp @@ -414,10 +414,15 @@ void test_database_paths_default() OIIO_CHECK_EQUAL( paths.empty(), false ); // On Unix systems, should have both new and legacy paths -#ifdef WIN32 +#if defined( WIN32 ) // On Windows, should have just the current directory OIIO_CHECK_EQUAL( paths.size(), 1 ); OIIO_CHECK_EQUAL( paths[0], "." ); +#elif defined( __APPLE__ ) + OIIO_CHECK_EQUAL( paths.size(), 3 ); + OIIO_CHECK_EQUAL( paths[0], "/usr/local/share/rawtoaces/data" ); + OIIO_CHECK_EQUAL( paths[1], "/opt/homebrew/share/rawtoaces/data" ); + OIIO_CHECK_EQUAL( paths[2], "/usr/local/include/rawtoaces/data" ); #else OIIO_CHECK_EQUAL( paths.size(), 2 ); OIIO_CHECK_EQUAL( paths[0], "/usr/local/share/rawtoaces/data" ); @@ -1285,6 +1290,60 @@ void test_database_location_not_directory_warning() std::filesystem::remove( file_path ); } +/// Tests that a warning is issued when a database location path does not exist. +void test_database_location_missing_warning() +{ + std::cout << "\n" << __FUNCTION__ << std::endl; + + // Create test directory + TestFixture fixture; + auto &test_dir = + fixture.with_camera( "Blackmagic", "Cinema Camera" ).build(); + + std::filesystem::path directory_path = + std::filesystem::temp_directory_path() / "missing_directory"; + + // Create a mock ImageSpec with camera metadata + auto image_spec = + ImageSpecBuilder().camera( "Blackmagic", "Cinema Camera" ).build(); + + // Configure settings with missing directory as database location + ImageConverter::Settings settings; + settings.database_directories = { directory_path.string(), + test_dir.get_database_path() }; + settings.illuminant = ""; // Empty to trigger auto-detection + settings.verbosity = 1; + + // Make sure the transform is not in the cache, otherwise DB look up + // will no be triggered. + settings.disable_cache = true; + + // Provide WB_multipliers + std::vector WB_multipliers = { 1.5, 1.0, 1.2, 1.0 }; + std::vector> IDT_matrix; + std::vector> CAT_matrix; + + bool success; + std::string error_message; + std::string output = capture_stderr( [&]() { + // This should succeed (using the valid database path) + // but should warn about the file path not being a directory + success = prepare_transform_spectral( + image_spec, + settings, + WB_multipliers, + IDT_matrix, + CAT_matrix, + error_message ); + } ); + + OIIO_CHECK_ASSERT( success ); + + // Assert on expected warning + ASSERT_CONTAINS( output, "Warning: Database location '" ); + ASSERT_CONTAINS( output, "' does not exist." ); +} + /// Tests that spectral data can be loaded using an absolute file path void test_load_spectral_data_absolute_path() { @@ -2698,6 +2757,54 @@ void test_lens_correction_type() rta::util::ImageConverter::Settings::LensCorrectionType::Aberration ) ); } +void test_metadata_wb_being_set() +{ + // Set the WB multipliers in the image spec, as if they came from an + // image file. + float wb[4] = { 2.0f, 1.0f, 1.5f, 1.2f }; + OIIO::ImageSpec spec; + spec.extra_attribs.attribute( + "raw:cam_mul", OIIO::TypeDesc( OIIO::TypeDesc::FLOAT, 4 ), wb ); + + rta::util::ImageConverter converter; + converter.settings.WB_method = + rta::util::ImageConverter::Settings::WBMethod::Metadata; + + OIIO::ParamValueList hints; + bool success = converter.configure( spec, hints ); + const auto &wb_multipliers = converter.get_WB_multipliers(); + + OIIO_CHECK_ASSERT( success ); + OIIO_CHECK_EQUAL( wb_multipliers.size(), 4 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[0], 2.0f, 1e-5 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[1], 1.0f, 1e-5 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[2], 1.5f, 1e-5 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[3], 1.2f, 1e-5 ); +} + +void test_custom_wb_being_set() +{ + rta::util::ImageConverter converter; + converter.settings.WB_method = + rta::util::ImageConverter::Settings::WBMethod::Custom; + converter.settings.custom_WB[0] = 2.0f; + converter.settings.custom_WB[1] = 1.0f; + converter.settings.custom_WB[2] = 1.5f; + converter.settings.custom_WB[3] = 1.2f; + + OIIO::ImageSpec spec; + OIIO::ParamValueList hints; + bool success = converter.configure( spec, hints ); + const auto &wb_multipliers = converter.get_WB_multipliers(); + + OIIO_CHECK_ASSERT( success ); + OIIO_CHECK_EQUAL( wb_multipliers.size(), 4 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[0], 2.0f, 1e-5 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[1], 1.0f, 1e-5 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[2], 1.5f, 1e-5 ); + OIIO_CHECK_EQUAL_THRESH( wb_multipliers[3], 1.2f, 1e-5 ); +} + int main( int, char ** ) { try @@ -2756,6 +2863,7 @@ int main( int, char ** ) test_invalid_blackbody_cct_exits(); test_auto_detect_illuminant_with_wb_multipliers(); test_database_location_not_directory_warning(); + test_database_location_missing_warning(); test_load_spectral_data_absolute_path(); test_illuminant_file_load_failure(); test_illuminant_type_mismatch(); @@ -2804,6 +2912,9 @@ int main( int, char ** ) test_main_error_message_without_hint(); test_main_error_data_dir_hint(); + test_metadata_wb_being_set(); + test_custom_wb_being_set(); + // Tests for lens correction types test_lens_correction_type(); } diff --git a/tests/usage_example_core.cpp b/tests/usage_example_core.cpp index 98d3621b..85bcdcb4 100644 --- a/tests/usage_example_core.cpp +++ b/tests/usage_example_core.cpp @@ -126,9 +126,9 @@ void test_MetadataSolver() // Check the results. const std::vector> true_IDT = { - { 1.053647, 0.003904, 0.004908 }, - { -0.489956, 1.361479, 0.102084 }, - { -0.002450, 0.006050, 1.013916 } + { 0.6820640403922289, 0.21830620601468764, 0.097819588932001586 }, + { -0.010414610202425199, 0.99916462206472656, 0.0094398234395704668 }, + { -0.088115635108323973, -0.49312503931360652, 1.5794305097175558 } }; for ( size_t row = 0; row < 3; row++ ) for ( size_t col = 0; col < 3; col++ )