Files
ColorControl/MHC2Gen/IccContext.cs
T

1135 lines
43 KiB
C#

using LittleCms;
using LittleCms.Data;
using MathNet.Numerics.LinearAlgebra;
using MathNet.Numerics.LinearAlgebra.Double;
using System;
using System.Buffers.Binary;
using System.IO;
namespace MHC2Gen
{
class MHC2Tag
{
public const TagSignature Signature = (TagSignature)0x4D484332;
public double MinCLL { get; set; }
public double MaxCLL { get; set; }
public double[,]? Matrix3x4 { get; set; }
public double[,]? RegammaLUT { get; set; }
public MHC2Tag() { }
public MHC2Tag(ReadOnlySpan<byte> bytes)
{
LoadFromBytes(bytes);
}
private void LoadFromBytes(ReadOnlySpan<byte> bytes)
{
var ms0 = new MemoryStream(bytes.ToArray());
var reader = new BinaryReader(ms0);
// Signature
reader.ReadBytes(4);
// 0
reader.ReadBytes(4);
// Lut Size
var lutSize = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4));
MinCLL = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)));
MaxCLL = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)));
// Matrix offset
var maxtrixOffset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4));
// Lut0 offset
var lut0Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4));
// Lut1 offset
var lut1Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4));
// Lut2 offset
var lut2Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4));
Matrix3x4 = new double[3, 4];
for (var x = 0; x < 3; x++)
{
for (var y = 0; y < 4; y++)
{
Matrix3x4[x, y] = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)));
}
}
RegammaLUT = new double[3, lutSize];
for (var c = 0; c < 3; c++)
{
var discard = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(8));
for (var x = 0; x < lutSize; x++)
{
RegammaLUT[c, x] = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)));
}
}
}
public void ApplySdrAcm(double whiteLuminance = 120.0, double blackLuminance = 0.0, double gamma = 2.2, double boostPercentage = 0)
{
var lutSize = 1024;
var amplifier = boostPercentage == 0 ? 1 : boostPercentage > 0 ? 1 + boostPercentage / 100 : 1 - boostPercentage / 100;
RegammaLUT = new double[3, lutSize];
for (var i = 0; i < lutSize; i++)
{
var (_, value) = CmsFunctions.SrgbAcm(i, whiteLuminance, blackLuminance, gamma, lutSize - 1);
for (var c = 0; c < 3; c++)
{
RegammaLUT[c, i] = value * amplifier;
}
}
}
public void ApplyPiecewise()
{
var lutSize = 1024;
RegammaLUT = new double[3, lutSize];
for (var i = 0; i < lutSize; i++)
{
var value = (double)i / 1023;
for (var c = 0; c < 3; c++)
{
RegammaLUT[c, i] = value;
}
}
}
private static int EncodeS15F16(double value)
{
var x = (int)Math.Round(value * 65536);
return x;
}
private static double DecodeS15F16(int value)
{
return (double)value / 65536;
}
public byte[] ToBytes()
{
if (RegammaLUT!.GetLength(0) != 3) throw new ArrayTypeMismatchException();
var lut_size = RegammaLUT.GetLength(1);
if (lut_size <= 1 || lut_size > 4096) throw new IndexOutOfRangeException();
if (Matrix3x4!.Length != 12 || Matrix3x4.GetLength(0) != 3) throw new ArrayTypeMismatchException();
var ms0 = new MemoryStream();
var writer = new BinaryWriter(ms0);
// type
writer.Write(BinaryPrimitives.ReverseEndianness((int)Signature));
writer.Write(BinaryPrimitives.ReverseEndianness(0));
writer.Write(BinaryPrimitives.ReverseEndianness(lut_size));
writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(MinCLL)));
writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(MaxCLL)));
// matrix offset
writer.Write(BinaryPrimitives.ReverseEndianness(36));
// lut0 offset
writer.Write(BinaryPrimitives.ReverseEndianness(84));
// lut1 offset
var lut1_offset = 84 + 8 + lut_size * 4;
writer.Write(BinaryPrimitives.ReverseEndianness(lut1_offset));
// lut2 offset
var lut2_offset = lut1_offset + 8 + lut_size * 4;
writer.Write(BinaryPrimitives.ReverseEndianness(lut2_offset));
foreach (var e in Matrix3x4)
{
writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(e)));
}
for (int ch = 0; ch < 3; ch++)
{
writer.Write(new ReadOnlySpan<byte>(new byte[] { (byte)'s', (byte)'f', (byte)'3', (byte)'2', 0, 0, 0, 0 }));
for (int i = 0; i < lut_size; i++)
{
writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(RegammaLUT[ch, i])));
}
}
writer.Flush();
return ms0.ToArray();
}
internal static MHC2Tag? LoadFromProfile(IccProfile profile)
{
var bytes = profile.RawBytes;
if (bytes == null)
{
return null;
}
// 0x4D484332
var index = -1;
var first = true;
for (var i = 0; i < bytes.Length; i += 4)
{
var chunk = bytes.AsSpan(new Range(i, i + 4));
if (chunk[0] == 77 && chunk[1] == 72 && chunk[2] == 67 && chunk[3] == 50)
{
index = i;
if (first)
{
index = -1;
first = false;
continue;
}
break;
}
}
if (index == -1)
{
return null;
}
var mhc2Bytes = bytes.AsSpan(Range.StartAt(index));
return new MHC2Tag(mhc2Bytes);
}
}
internal class IccContext
{
protected IccProfile profile;
public CIEXYZ IlluminantRelativeWhitePoint { get; }
public Matrix<double>? ChromaticAdaptionMatrix { get; }
public Matrix<double>? InverseChromaticAdaptionMatrix { get; }
public RgbPrimaries ProfilePrimaries { get; }
public MHC2Tag? MHC2 { get; set; }
public IccContext(IccProfile profile)
{
if (profile.PCS != ColorSpaceSignature.XYZ || profile.ColorSpace != ColorSpaceSignature.Rgb)
{
throw new CmsException(CmsError.COLORSPACE_CHECK, "ICC profile is not XYZ->RGB");
}
this.profile = profile;
{
if (profile.TryReadTag(SafeTagSignature.ChromaticAdaptationTag, out var chad))
ChromaticAdaptionMatrix = DenseMatrix.OfArray(chad);
InverseChromaticAdaptionMatrix = ChromaticAdaptionMatrix?.Inverse();
}
(IlluminantRelativeWhitePoint, ProfilePrimaries) = PopulatePrimaries();
MHC2 = MHC2Tag.LoadFromProfile(profile);
}
private unsafe CIEXYZ GetIlluminantReletiveWhitePoint()
{
if (profile.TryReadTag(SafeTagSignature.MediaWhitePointTag, out var icc_wtpt))
{
if (ChromaticAdaptionMatrix == null || profile.HeaderCreator == 0x6170706c /* 'aapl' */)
{
// for profiels without 'chad' tag and Apple profiles, mediaWhitepointTag is illuminant-relative
return icc_wtpt;
}
else
{
// ... otherwise it is PCS-relative
var pcs_wtpt = icc_wtpt;
if (ChromaticAdaptionMatrix != null)
{
return ApplyInverseChad(pcs_wtpt);
}
}
}
if (ChromaticAdaptionMatrix != null)
{
// no wtpt in icc, sum RGB and reverse chad
var pcs_rXYZ = profile.ReadTag(SafeTagSignature.RedColorantTag);
var pcs_gXYZ = profile.ReadTag(SafeTagSignature.GreenColorantTag);
var pcs_bXYZ = profile.ReadTag(SafeTagSignature.BlueColorantTag);
var pcs_sumrgb = pcs_rXYZ + pcs_gXYZ + pcs_bXYZ;
return ApplyInverseChad(pcs_sumrgb);
}
else
{
throw new Exception("malformed profile: missing wtpt and chad");
}
}
protected CIEXYZ ApplyInverseChad(in CIEXYZ val)
{
var vec = InverseChromaticAdaptionMatrix!.Multiply(new DenseVector(new[] { val.X, val.Y, val.Z }));
return new() { X = vec[0], Y = vec[1], Z = vec[2] };
}
/// <summary>
/// use lcms transform to get illuminant-relative primaries.
/// </summary>
private unsafe (CIEXYZ, RgbPrimaries) PopulatePrimaries()
{
var ctx = new CmsContext();
ctx.SetAdaptionState(0);
var xyzprof = IccProfile.CreateXYZ();
var t = new CmsTransform(ctx, profile, CmsPixelFormat.RGBDouble, xyzprof, CmsPixelFormat.XYZDouble, RenderingIntent.ABSOLUTE_COLORIMETRIC, default);
var pixels = new ReadOnlySpan<double>(new double[] {
1, 0, 0,
0, 1, 0,
0, 0, 1,
1, 1, 1
});
Span<double> xyz = stackalloc double[3];
t.DoTransform(pixels.Slice(0), xyz, 1);
var rXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] };
t.DoTransform(pixels.Slice(3), xyz, 1);
var gXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] };
t.DoTransform(pixels.Slice(6), xyz, 1);
var bXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] };
t.DoTransform(pixels.Slice(9), xyz, 1);
var wXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] };
return (wXYZ, new(rXYZ.ToXY(), gXYZ.ToXY(), bXYZ.ToXY(), wXYZ.ToXY()));
}
public string GetDescription()
{
return profile.GetInfo(InfoType.Description);
}
public CIEXYZ GetIlluminantRelativeBlackPoint()
{
// NOTE: mediaBlackPointTag is no longer in ICC standard
if (profile.TryReadTag(SafeTagSignature.MediaBlackPointTag, out var bkpt))
{
// no chad in profile, bkpt is illuminant-relative
if (ChromaticAdaptionMatrix == null)
{
return bkpt;
}
else
{
return ApplyInverseChad(bkpt);
}
}
// no bkpt in tag, use lcms transform
var ctx = new CmsContext();
ctx.SetAdaptionState(0);
var t = new CmsTransform(ctx, profile, CmsPixelFormat.RGB8, IccProfile.CreateXYZ(), CmsPixelFormat.XYZDouble, RenderingIntent.ABSOLUTE_COLORIMETRIC, default);
var input = new ReadOnlySpan<byte>(new byte[] { 0, 0, 0 });
Span<double> outbuf = stackalloc double[3];
t.DoTransform(input, outbuf, 1);
bkpt = new CIEXYZ { X = outbuf[0], Y = outbuf[1], Z = outbuf[2] };
return bkpt;
}
public void WriteIlluminantRelativeMediaBlackPoint(in CIEXYZ value)
{
CIEXYZ valueToWrite;
if (ChromaticAdaptionMatrix != null)
{
var vec = new DenseVector(new double[] { value.X, value.Y, value.Z });
var pcs_vec = ChromaticAdaptionMatrix * vec;
valueToWrite = new() { X = pcs_vec[0], Y = pcs_vec[1], Z = pcs_vec[2] };
}
else
{
valueToWrite = value;
}
profile.WriteTag(SafeTagSignature.MediaBlackPointTag, valueToWrite);
}
public static Matrix<double> GetChromaticAdaptationMatrix(CIEXYZ sourceIlluminant, CIEXYZ targetIlluminant)
{
// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
// Bradford
var M_a = DenseMatrix.OfArray(new[,] {
{ 0.8951, 0.2664, -0.1614 },
{ -0.7502, 1.7135, 0.0367 },
{ 0.0389, -0.0685, 1.0296 },
});
var M_a_inv = M_a.Inverse();
var cone_s_vec = M_a * new DenseVector(new double[] { sourceIlluminant.X, sourceIlluminant.Y, sourceIlluminant.Z });
var cone_t_vec = M_a * new DenseVector(new double[] { targetIlluminant.X, targetIlluminant.Y, targetIlluminant.Z });
var M = M_a_inv * DenseMatrix.Build.DenseOfDiagonalVector(cone_t_vec / cone_s_vec) * M_a;
return M;
}
}
internal class DeviceIccContext : IccContext
{
CIEXYZ illuminantRelativeBlackPoint;
double min_nits;
double max_nits;
ToneCurve profileRedToneCurve;
ToneCurve profileGreenToneCurve;
ToneCurve profileBlueToneCurve;
ToneCurve profileRedReverseToneCurve;
ToneCurve profileGreenReverseToneCurve;
ToneCurve profileBlueReverseToneCurve;
public bool UseChromaticAdaptation { get; set; }
public DeviceIccContext(IccProfile profile) : base(profile)
{
illuminantRelativeBlackPoint = GetIlluminantRelativeBlackPoint();
(max_nits, min_nits) = GetProfileLuminance();
profileRedToneCurve = profile.ReadTag(SafeTagSignature.RedTRCTag);
profileGreenToneCurve = profile.ReadTag(SafeTagSignature.GreenTRCTag);
profileBlueToneCurve = profile.ReadTag(SafeTagSignature.BlueTRCTag);
profileRedReverseToneCurve = profileRedToneCurve.Reverse();
profileGreenReverseToneCurve = profileGreenToneCurve.Reverse();
profileBlueReverseToneCurve = profileBlueToneCurve.Reverse();
}
public static Matrix<double> RgbToXYZ(RgbPrimaries primaries)
{
var rXYZ = primaries.Red.ToXYZ();
var gXYZ = primaries.Green.ToXYZ();
var bXYZ = primaries.Blue.ToXYZ();
var wXYZ = primaries.White.ToXYZ();
var S = DenseMatrix.OfArray(new[,] {
{rXYZ.X, gXYZ.X, bXYZ.X},
{rXYZ.Y, gXYZ.Y, bXYZ.Y},
{rXYZ.Z, gXYZ.Z, bXYZ.Z},
}).Inverse().Multiply(DenseMatrix.OfArray(new[,] { { wXYZ.X }, { wXYZ.Y }, { wXYZ.Z } }));
var M = DenseMatrix.OfArray(new[,] {
{S[0,0] * rXYZ.X, S[1,0]*gXYZ.X, S[2,0]*bXYZ.X },
{S[0,0] * rXYZ.Y, S[1,0]*gXYZ.Y, S[2,0]*bXYZ.Y },
{S[0,0] * rXYZ.Z, S[1,0]*gXYZ.Z, S[2,0]*bXYZ.Z },
});
return M;
}
public static Matrix<double> XYZToRgb(RgbPrimaries primaries) => RgbToXYZ(primaries).Inverse();
public static Matrix<double> RgbToRgb(RgbPrimaries from, RgbPrimaries to)
{
var M1 = RgbToXYZ(from);
var M2 = XYZToRgb(to);
return M2 * M1;
}
public string GetDeviceDescription()
{
var model = profile.GetInfo(InfoType.Model);
if (!string.IsNullOrEmpty(model)) return model;
var desc = GetDescription();
if (!string.IsNullOrEmpty(desc)) return desc;
return "<Unknown device>";
}
private (double MaxNits, double MinNits) GetProfileLuminance()
{
var wtpt = IlluminantRelativeWhitePoint;
double max_nits = 80;
if (profile.TryReadTag(SafeTagSignature.LuminanceTag, out var lumi))
{
max_nits = lumi.Y;
}
var min_nits = 0.005;
var bkpt = illuminantRelativeBlackPoint;
if (bkpt.Y != 0)
{
var bkpt_scale = bkpt.Y / wtpt.Y;
min_nits = max_nits * bkpt_scale;
}
return (max_nits, min_nits);
}
public IccProfile CreateMhc2CscIcc(RgbPrimaries? sourcePrimaries = null, string sourceDescription = "sRGB")
{
var wtpt = IlluminantRelativeWhitePoint;
var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray();
var devicePrimaries = ProfilePrimaries;
var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve };
var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!;
var sourceEotf = new ToneCurve[] { srgbTrc, srgbTrc, srgbTrc };
sourcePrimaries ??= RgbPrimaries.sRGB;
var srgb_to_xyz = RgbToXYZ(RgbPrimaries.sRGB);
var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB);
Matrix<double> user_matrix = DenseMatrix.CreateIdentity(3);
// pipeline here: input signal converted to XYZ (interpreted as sRGB)
if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB))
{
user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix;
}
// pipeline here: input signal converted to XYZ (interpreted as custom RGB)
if (UseChromaticAdaptation)
{
user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix;
}
// pipeline here: input signal XYZ adapted to device white point
// hook: scale white point
var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 });
var mapped_y = source_white_to_xyz[1];
var profile_max_nits = max_nits * (mapped_y / wtpt.Y);
// end hook
user_matrix = XYZToRgb(devicePrimaries) * user_matrix;
// pipeline here: linear device RGB
// hack: eliminate fixed sRGB to XYZ transform
user_matrix = srgb_to_xyz * user_matrix;
var mhc2_matrix = new double[,] {
{ user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 },
{ user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 },
{ user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 },
};
double[,] mhc2_lut;
if (vcgt != null)
{
var lut_size = 1024;
mhc2_lut = new double[3, lut_size];
for (int ch = 0; ch < 3; ch++)
{
for (int iinput = 0; iinput < lut_size; iinput++)
{
var input = (float)iinput / (lut_size - 1);
var linear = sourceEotf[ch].EvalF32(input);
var dev_output = deviceOetf[ch].EvalF32(linear);
if (vcgt != null)
{
dev_output = vcgt[ch].EvalF32(dev_output);
}
mhc2_lut[ch, iinput] = dev_output;
}
}
}
else
{
mhc2_lut = new double[,]
{
{ 0, 1 },
{ 0, 1 },
{ 0, 1 },
};
}
var mhc2d = new MHC2Tag
{
MinCLL = min_nits,
MaxCLL = profile_max_nits,
Matrix3x4 = mhc2_matrix,
RegammaLUT = mhc2_lut
};
var mhc2 = mhc2d.ToBytes();
var outputProfile = IccProfile.CreateRGB(sourcePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE
{
Red = sourcePrimaries.Red.ToXYZ().ToCIExyY(),
Green = sourcePrimaries.Green.ToXYZ().ToCIExyY(),
Blue = sourcePrimaries.Blue.ToXYZ().ToCIExyY()
}, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc));
outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits });
var outctx = new IccContext(outputProfile);
outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint);
// copy device description from device profile
var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag };
unsafe
{
foreach (var tag in copy_tags)
{
var tag_ptr = profile.ReadTag(tag);
if (tag_ptr != null)
{
outputProfile.WriteTag(tag, tag_ptr);
}
}
}
// set output profile description
outputProfile.HeaderManufacturer = profile.HeaderManufacturer;
outputProfile.HeaderModel = profile.HeaderModel;
outputProfile.HeaderAttributes = profile.HeaderAttributes;
outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC;
var new_desc = $"CSC: {sourceDescription} ({GetDeviceDescription()})";
var new_desc_mlu = new MLU(new_desc);
outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu);
outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2);
outputProfile.ComputeProfileId();
return outputProfile;
}
public IccProfile CreatePQ10DecodeIcc(double? maxBrightnessOverride = null, double? minBrightnessOverride = null)
{
var sourcePrimaries = RgbPrimaries.Rec2020;
var devicePrimaries = ProfilePrimaries;
Matrix<double> user_matrix = DenseMatrix.CreateIdentity(3);
if (UseChromaticAdaptation)
{
user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix;
}
// var rgb_transform = RgbToRgb(sourcePrimaries, devicePrimaries);
// rgb_transform = XYZToRgb(devicePrimaries) * RgbToXYZ(sourcePrimaries);
// var xyz_transform = RgbToXYZ(sourcePrimaries) * rgb_transform * XYZToRgb(sourcePrimaries);
user_matrix = RgbToXYZ(sourcePrimaries) * XYZToRgb(devicePrimaries) * user_matrix;
var mhc2_matrix = new double[,] {
{ user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 },
{ user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 },
{ user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 },
};
var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray();
var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve };
var use_max_nits = maxBrightnessOverride ?? max_nits;
var use_min_nits = minBrightnessOverride ?? min_nits;
var lut_size = 4096;
var mhc2_lut = new double[3, 4096];
for (int ch = 0; ch < 3; ch++)
{
for (int iinput = 0; iinput < lut_size; iinput++)
{
var pqinput = (double)iinput / (lut_size - 1);
var nits = ST2084.SignalToNits(pqinput);
var linear = Math.Max(nits - use_min_nits, 0) / (use_max_nits - use_min_nits);
var dev_output = deviceOetf[ch].EvalF32((float)linear);
if (vcgt != null)
{
dev_output = vcgt[ch].EvalF32(dev_output);
}
// Console.WriteLine($"Channel {ch}: PQ {iinput} -> {nits} cd/m2 -> SDR {dev_output * 255}");
mhc2_lut[ch, iinput] = dev_output;
}
}
var mhc2d = new MHC2Tag
{
MinCLL = use_min_nits,
MaxCLL = use_max_nits,
Matrix3x4 = mhc2_matrix,
RegammaLUT = mhc2_lut
};
var mhc2 = mhc2d.ToBytes();
var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE
{
Red = devicePrimaries.Red.ToXYZ().ToCIExyY(),
Green = devicePrimaries.Green.ToXYZ().ToCIExyY(),
Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY()
}, new RgbToneCurve(profileRedToneCurve, profileGreenToneCurve, profileBlueToneCurve));
// copy characteristics from device profile
var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag };
unsafe
{
foreach (var tag in copy_tags)
{
var tag_ptr = profile.ReadTag(tag);
if (tag_ptr != null)
{
outputProfile.WriteTag(tag, tag_ptr);
}
}
}
outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = use_max_nits });
// the profile is not read by regular applications
// var outctx = new IccContext(outputProfile);
// outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint);
// set output profile description
outputProfile.HeaderManufacturer = profile.HeaderManufacturer;
outputProfile.HeaderModel = profile.HeaderModel;
outputProfile.HeaderAttributes = profile.HeaderAttributes;
outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC;
var new_desc = $"CSC: HDR10 to SDR ({GetDeviceDescription()}, {use_max_nits:0} nits)";
var new_desc_mlu = new MLU(new_desc);
outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu);
outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2);
outputProfile.ComputeProfileId();
return outputProfile;
}
public IccProfile CreateSdrAcmIcc(bool calibrateTransfer)
{
Matrix<double> user_matrix = DenseMatrix.CreateIdentity(3);
if (UseChromaticAdaptation)
{
user_matrix = GetChromaticAdaptationMatrix(new CIExy { x = 0.3127, y = 0.3290 }.ToXYZ(), ProfilePrimaries.White.ToXYZ()) * user_matrix;
}
var mhc2_matrix = new double[,] {
{ user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 },
{ user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 },
{ user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 },
};
double[,] mhc2_lut;
var outputprofileTrc = new RgbToneCurve(profileRedToneCurve, profileGreenToneCurve, profileBlueToneCurve);
var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray();
if (calibrateTransfer)
{
var sourceEotf = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag);
outputprofileTrc = new RgbToneCurve(sourceEotf, sourceEotf, sourceEotf);
var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve };
var lut_size = 1024;
mhc2_lut = new double[3, lut_size];
for (int ch = 0; ch < 3; ch++)
{
for (int iinput = 0; iinput < lut_size; iinput++)
{
var input = (float)iinput / (lut_size - 1);
var linear = sourceEotf.EvalF32(input);
var dev_output = deviceOetf[ch].EvalF32(linear);
if (vcgt != null)
{
dev_output = vcgt[ch].EvalF32(dev_output);
}
mhc2_lut[ch, iinput] = dev_output;
}
}
}
else if (vcgt != null)
{
// move vcgt to mhc2 only
var lut_size = 1024;
mhc2_lut = new double[3, lut_size];
for (int ch = 0; ch < 3; ch++)
{
for (int iinput = 0; iinput < lut_size; iinput++)
{
var input = (float)iinput / (lut_size - 1);
var dev_output = vcgt[ch].EvalF32(input);
mhc2_lut[ch, iinput] = dev_output;
}
}
}
else
{
mhc2_lut = new double[,] { { 0, 1 }, { 0, 1 }, { 0, 1 } };
}
var mhc2d = new MHC2Tag
{
MinCLL = min_nits,
MaxCLL = max_nits,
Matrix3x4 = mhc2_matrix,
RegammaLUT = mhc2_lut
};
var mhc2 = mhc2d.ToBytes();
var devicePrimaries = ProfilePrimaries;
var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE
{
Red = devicePrimaries.Red.ToXYZ().ToCIExyY(),
Green = devicePrimaries.Green.ToXYZ().ToCIExyY(),
Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY()
}, outputprofileTrc);
// copy characteristics from device profile
var copy_tags = new TagSignature[] {
TagSignature.LuminanceTag,
TagSignature.DeviceMfgDescTag,
TagSignature.DeviceModelDescTag,
};
unsafe
{
foreach (var tag in copy_tags)
{
var tag_ptr = profile.ReadTag(tag);
outputProfile.WriteTag(tag, tag_ptr);
}
}
// the profile is not read by regular applications
//var outctx = new IccContext(outputProfile);
//outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint);
// SDR ACM will not work if the profile has negative XYZ colorants (possibly due to limited precision)
foreach (var sig in new ISafeTagSignature<CIEXYZ>[] { SafeTagSignature.RedColorantTag, SafeTagSignature.GreenColorantTag, SafeTagSignature.BlueColorantTag })
{
var xyz = outputProfile.ReadTag(sig);
if (xyz.X < 0) xyz.X = 0;
if (xyz.Y < 0) xyz.Y = 0;
if (xyz.Z < 0) xyz.Z = 0;
outputProfile.WriteTag(sig, xyz);
}
// set output profile description
outputProfile.HeaderManufacturer = profile.HeaderManufacturer;
outputProfile.HeaderModel = profile.HeaderModel;
outputProfile.HeaderAttributes = profile.HeaderAttributes;
outputProfile.HeaderRenderingIntent = profile.HeaderRenderingIntent;
// outputProfile.ProfileVersion = profile.ProfileVersion;
var new_desc = $"SDR ACM: {profile.GetInfo(InfoType.Description)}";
var new_desc_mlu = new MLU(new_desc);
outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu);
outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2);
outputProfile.ComputeProfileId();
return outputProfile;
}
public IccProfile CreateIcc(GenerateProfileCommand command)
{
var maxNits = command.WhiteLuminance;
//var wtpt = IlluminantRelativeWhitePoint;
var devicePrimaries = command.DevicePrimaries; // new RgbPrimaries(new() { x = 0.698, y = 0.292 }, new() { x = 0.255, y = 0.699 }, new() { x = 0.148, y = 0.056 }, new() { x = 0.3127, y = 0.3290 });
var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!;
var sourcePrimaries = RgbPrimaries.sRGB;
var xyz_to_srgb = XYZToRgb(sourcePrimaries);
Matrix<double> user_matrix = DenseMatrix.CreateIdentity(3);
// pipeline here: input signal converted to XYZ (interpreted as sRGB)
if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB))
{
user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix;
}
// pipeline here: input signal converted to XYZ (interpreted as custom RGB)
if (UseChromaticAdaptation)
{
user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix;
}
// pipeline here: input signal XYZ adapted to device white point
// hook: scale white point
//var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 });
//var mapped_y = source_white_to_xyz[1];
//var profile_max_nits = max_nits * (mapped_y / wtpt.Y);
var profile_max_nits = maxNits;
// end hook
// pipeline here: linear device RGB
// hack: eliminate fixed sRGB to XYZ transform
// CSC
var targetPrimaries = command.ColorGamut switch
{
ColorGamut.Native => devicePrimaries,
ColorGamut.sRGB => RgbPrimaries.sRGB,
ColorGamut.P3 => RgbPrimaries.P3D65,
ColorGamut.Rec2020 => RgbPrimaries.Rec2020,
ColorGamut.AdobeRGB => RgbPrimaries.AdobeRGB,
_ => devicePrimaries
};
var srgb_to_xyz = RgbToXYZ(targetPrimaries);
//var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB);
user_matrix = XYZToRgb(devicePrimaries) * user_matrix;
user_matrix = srgb_to_xyz * user_matrix;
var mhc2_matrix = new double[,] {
{ user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 },
{ user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 },
{ user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 },
};
MHC2 = new MHC2Tag
{
MinCLL = command.MinCLL,
MaxCLL = command.MaxCLL
};
if (command.SDRTransferFunction == SDRTransferFunction.Piecewise)
{
MHC2.ApplyPiecewise();
}
else if (command.SDRTransferFunction == SDRTransferFunction.PurePower)
{
MHC2.ApplySdrAcm(command.SDRMaxBrightness, command.SDRMinBrightness, command.Gamma, command.SDRBrightnessBoost);
}
else if (command.SDRTransferFunction == SDRTransferFunction.BT_1886)
{
MHC2.ApplySdrAcm(120, 0.03, 2.4);
}
MHC2.Matrix3x4 = mhc2_matrix;
var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE
{
Red = devicePrimaries.Red.ToXYZ().ToCIExyY(),
Green = devicePrimaries.Green.ToXYZ().ToCIExyY(),
Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY()
}, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc));
outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits });
var outctx = new IccContext(outputProfile);
outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint);
// copy device description from device profile
var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag };
unsafe
{
foreach (var tag in copy_tags)
{
var tag_ptr = profile.ReadTag(tag);
if (tag_ptr != null)
{
outputProfile.WriteTag(tag, tag_ptr);
}
}
}
// set output profile description
outputProfile.HeaderManufacturer = profile.HeaderManufacturer;
outputProfile.HeaderModel = profile.HeaderModel;
outputProfile.HeaderAttributes = profile.HeaderAttributes;
outputProfile.HeaderRenderingIntent = RenderingIntent.PERCEPTUAL;
var new_desc = $"{command.Description} ({GetDeviceDescription()})";
var new_desc_mlu = new MLU(new_desc);
outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu);
outputProfile.WriteRawTag(MHC2Tag.Signature, MHC2.ToBytes());
outputProfile.ComputeProfileId();
return outputProfile;
}
public IccProfile CreateCscIcc(RgbPrimaries? sourcePrimaries = null, string sourceDescription = "sRGB")
{
var wtpt = IlluminantRelativeWhitePoint;
var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray();
var customPrimaries = new RgbPrimaries(new() { x = 0.698, y = 0.292 }, new() { x = 0.255, y = 0.699 }, new() { x = 0.148, y = 0.056 }, new() { x = 0.3127, y = 0.3290 });
var devicePrimaries = customPrimaries;
var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve };
var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!;
var sourceEotf = new ToneCurve[] { srgbTrc, srgbTrc, srgbTrc };
sourcePrimaries ??= RgbPrimaries.sRGB;
var srgb_to_xyz = RgbToXYZ(RgbPrimaries.sRGB);
var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB);
Matrix<double> user_matrix = DenseMatrix.CreateIdentity(3);
// pipeline here: input signal converted to XYZ (interpreted as sRGB)
if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB))
{
user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix;
}
// pipeline here: input signal converted to XYZ (interpreted as custom RGB)
if (UseChromaticAdaptation)
{
user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix;
}
// pipeline here: input signal XYZ adapted to device white point
// hook: scale white point
var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 });
var mapped_y = source_white_to_xyz[1];
var profile_max_nits = max_nits * (mapped_y / wtpt.Y);
// end hook
user_matrix = XYZToRgb(devicePrimaries) * user_matrix;
// pipeline here: linear device RGB
// hack: eliminate fixed sRGB to XYZ transform
user_matrix = srgb_to_xyz * user_matrix;
var mhc2_matrix = new double[,] {
{ user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 },
{ user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 },
{ user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 },
};
double[,] mhc2_lut;
if (vcgt != null)
{
var lut_size = 1024;
mhc2_lut = new double[3, lut_size];
for (int ch = 0; ch < 3; ch++)
{
for (int iinput = 0; iinput < lut_size; iinput++)
{
var input = (float)iinput / (lut_size - 1);
var linear = sourceEotf[ch].EvalF32(input);
var dev_output = deviceOetf[ch].EvalF32(linear);
if (vcgt != null)
{
dev_output = vcgt[ch].EvalF32(dev_output);
}
mhc2_lut[ch, iinput] = dev_output;
}
}
}
else
{
var lut_size = 1024;
mhc2_lut = new double[3, lut_size];
for (int ch = 0; ch < 3; ch++)
{
for (int iinput = 0; iinput < lut_size; iinput++)
{
var input = (float)iinput / (lut_size - 1);
var linear = sourceEotf[ch].EvalF32(input);
var dev_output = CmsFunctions.RgbToLinear(linear, 1.6);
//var dev_output = deviceOetf[ch].EvalF32(input);
//if (vcgt != null)
//{
// dev_output = vcgt[ch].EvalF32(dev_output);
//}
mhc2_lut[ch, iinput] = dev_output;
}
}
}
var mhc2d = new MHC2Tag
{
MinCLL = min_nits,
MaxCLL = profile_max_nits,
Matrix3x4 = mhc2_matrix,
RegammaLUT = mhc2_lut
};
var mhc2 = mhc2d.ToBytes();
var outputProfile = IccProfile.CreateRGB(sourcePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE
{
Red = sourcePrimaries.Red.ToXYZ().ToCIExyY(),
Green = sourcePrimaries.Green.ToXYZ().ToCIExyY(),
Blue = sourcePrimaries.Blue.ToXYZ().ToCIExyY()
}, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc));
outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits });
var outctx = new IccContext(outputProfile);
outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint);
// copy device description from device profile
var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag };
unsafe
{
foreach (var tag in copy_tags)
{
var tag_ptr = profile.ReadTag(tag);
if (tag_ptr != null)
{
outputProfile.WriteTag(tag, tag_ptr);
}
}
}
// set output profile description
outputProfile.HeaderManufacturer = profile.HeaderManufacturer;
outputProfile.HeaderModel = profile.HeaderModel;
outputProfile.HeaderAttributes = profile.HeaderAttributes;
outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC;
var new_desc = $"XCSC: {sourceDescription} ({GetDeviceDescription()})";
var new_desc_mlu = new MLU(new_desc);
outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu);
outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2);
outputProfile.ComputeProfileId();
return outputProfile;
}
}
}