114 lines
4.5 KiB
Swift
114 lines
4.5 KiB
Swift
import XCTest
|
|
@testable import ForgeColor
|
|
|
|
final class CDLTests: XCTestCase {
|
|
|
|
// Identity CDL passes pixels through untouched.
|
|
func testIdentityPassthrough() {
|
|
let cdl = CDL.identity
|
|
let px = SIMD3<Float>(0.18, 0.5, 0.9)
|
|
let out = cdl.apply(px)
|
|
XCTAssertEqual(out.x, 0.18, accuracy: 1e-6)
|
|
XCTAssertEqual(out.y, 0.5, accuracy: 1e-6)
|
|
XCTAssertEqual(out.z, 0.9, accuracy: 1e-6)
|
|
}
|
|
|
|
// Hand-computed SOP vector.
|
|
// in=0.5, slope=2, offset=0.1, power=1 -> 0.5*2+0.1 = 1.1
|
|
func testSlopeOffset() {
|
|
let cdl = CDL(
|
|
slope: SIMD3(2, 2, 2),
|
|
offset: SIMD3(0.1, 0.1, 0.1),
|
|
power: SIMD3(1, 1, 1),
|
|
saturation: 1)
|
|
let out = cdl.apply(SIMD3(0.5, 0.5, 0.5))
|
|
XCTAssertEqual(out.x, 1.1, accuracy: 1e-6)
|
|
XCTAssertEqual(out.y, 1.1, accuracy: 1e-6)
|
|
XCTAssertEqual(out.z, 1.1, accuracy: 1e-6)
|
|
}
|
|
|
|
// Power applies after slope+offset: (0.25*1+0)^0.5 = 0.5
|
|
func testPower() {
|
|
let cdl = CDL(
|
|
slope: .one, offset: .zero,
|
|
power: SIMD3(0.5, 0.5, 0.5),
|
|
saturation: 1)
|
|
let out = cdl.apply(SIMD3(0.25, 0.25, 0.25))
|
|
XCTAssertEqual(out.x, 0.5, accuracy: 1e-6)
|
|
}
|
|
|
|
// Per-channel independence.
|
|
func testPerChannel() {
|
|
let cdl = CDL(
|
|
slope: SIMD3(1, 2, 0.5),
|
|
offset: SIMD3(0, 0.1, -0.05),
|
|
power: SIMD3(1, 1, 2),
|
|
saturation: 1)
|
|
let out = cdl.apply(SIMD3(0.4, 0.4, 0.4))
|
|
XCTAssertEqual(out.x, 0.4, accuracy: 1e-6) // 0.4*1+0
|
|
XCTAssertEqual(out.y, 0.9, accuracy: 1e-6) // 0.4*2+0.1
|
|
XCTAssertEqual(out.z, pow(0.4 * 0.5 - 0.05, 2), accuracy: 1e-6) // (0.15)^2
|
|
}
|
|
|
|
// ASC: value after SOP clamped to >= 0 before power (avoid NaN on negatives).
|
|
func testNegativePrePowerClamp() {
|
|
let cdl = CDL(
|
|
slope: .one,
|
|
offset: SIMD3(-1, -1, -1),
|
|
power: SIMD3(0.5, 0.5, 0.5),
|
|
saturation: 1)
|
|
let out = cdl.apply(SIMD3(0.5, 0.5, 0.5)) // 0.5-1 = -0.5 -> clamp 0 -> 0^0.5 = 0
|
|
XCTAssertEqual(out.x, 0, accuracy: 1e-6)
|
|
XCTAssertFalse(out.x.isNaN)
|
|
}
|
|
|
|
// Power=1 keeps negatives (linear passthrough, no clamp needed) per common practice:
|
|
// clamp only applies to the pow() branch. Spec-conformant tools clamp pre-power always;
|
|
// we clamp always for determinism.
|
|
func testClampAlwaysPrePower() {
|
|
let cdl = CDL(slope: .one, offset: SIMD3(-1, -1, -1), power: .one, saturation: 1)
|
|
let out = cdl.apply(SIMD3(0.5, 0.5, 0.5))
|
|
XCTAssertEqual(out.x, 0, accuracy: 1e-6)
|
|
}
|
|
|
|
// Saturation 0 -> pure luma gray using Rec709 weights (0.2126, 0.7152, 0.0722).
|
|
func testSaturationZeroGivesLumaGray() {
|
|
let cdl = CDL(slope: .one, offset: .zero, power: .one, saturation: 0)
|
|
let px = SIMD3<Float>(1, 0, 0)
|
|
let out = cdl.apply(px)
|
|
let luma: Float = 0.2126
|
|
XCTAssertEqual(out.x, luma, accuracy: 1e-6)
|
|
XCTAssertEqual(out.y, luma, accuracy: 1e-6)
|
|
XCTAssertEqual(out.z, luma, accuracy: 1e-6)
|
|
}
|
|
|
|
// Saturation 1 is no-op on chroma.
|
|
func testSaturationOneNoOp() {
|
|
let cdl = CDL(slope: .one, offset: .zero, power: .one, saturation: 1)
|
|
let px = SIMD3<Float>(0.7, 0.2, 0.4)
|
|
let out = cdl.apply(px)
|
|
XCTAssertEqual(out.x, 0.7, accuracy: 1e-6)
|
|
XCTAssertEqual(out.y, 0.2, accuracy: 1e-6)
|
|
XCTAssertEqual(out.z, 0.4, accuracy: 1e-6)
|
|
}
|
|
|
|
// Saturation 2 doubles distance from luma: out = luma + 2*(in - luma)
|
|
func testSaturationBoost() {
|
|
let cdl = CDL(slope: .one, offset: .zero, power: .one, saturation: 2)
|
|
let px = SIMD3<Float>(0.6, 0.3, 0.3)
|
|
let luma: Float = 0.2126 * 0.6 + 0.7152 * 0.3 + 0.0722 * 0.3
|
|
let out = cdl.apply(px)
|
|
XCTAssertEqual(out.x, luma + 2 * (0.6 - luma), accuracy: 1e-5)
|
|
XCTAssertEqual(out.y, luma + 2 * (0.3 - luma), accuracy: 1e-5)
|
|
}
|
|
|
|
// Identity constant is actually identity params.
|
|
func testIdentityConstants() {
|
|
XCTAssertEqual(CDL.identity.slope, SIMD3<Float>(1, 1, 1))
|
|
XCTAssertEqual(CDL.identity.offset, SIMD3<Float>(0, 0, 0))
|
|
XCTAssertEqual(CDL.identity.power, SIMD3<Float>(1, 1, 1))
|
|
XCTAssertEqual(CDL.identity.saturation, 1)
|
|
XCTAssertTrue(CDL.identity.isIdentity)
|
|
XCTAssertFalse(CDL(slope: SIMD3(1.1, 1, 1), offset: .zero, power: .one, saturation: 1).isIdentity)
|
|
}
|
|
}
|