C# Farbenmanagement #2

Ein paar kleine Optimierungen / Refactorings haben sich in meiner Farbenkonvertierungsklasse breitgemacht.

Zum einen habe ich den Code vereinfacht, indem ich sich wiederholenden Code entfernt habe, zum anderen brauchte ich noch eine weitere Konvertierungsart, und zwar vom XYZ Farbraum zurück in den L*ab Farbraum.

Farbmessgeräte werden üblicherweise einmal am Tag kalibriert, dazu haben sie einen Weiß-Standard der gemessen wird und an dem sich das Gerät orienterien kann. Somit werden die Alterungserscheinungen des Blitzlichtes wieder kompensiert und man bekommt gleichmässige Messergebnisse.

Die Werte dieses Standards liegen aber nur als XYZ Werte vor, das Messgerät liefert aber in der gebrauchten Einstellung nur L*ab Werte zurück, daher die kleine zusätzliche Konvertierungsmethode.

Bei den Refactorings hat sich gezeigt wie unendlich praktisch Unit-Tests sind. Man kann seine Methoden optimieren, kürzen, verändern. Ist man fertig lässt man die Unit-Tests drüberlaufen und kann sich dann sicher sein, dass die Überarbeitungen nichts an den Ergebnissen geändert hat.

Und hier nun der gesamte neue Code:

public static class ColourConverter
{
    // Daylight reference WHITE
    private static readonly XYZ whiteD65 = new XYZ(95.047m, 100.000m, 108.883m);
    private static readonly XYZ whiteD50 = new XYZ(96.52m, 100.00m, 82.49m);

    private static readonly decimal epsilon = 0.008856m;

    public static LCH toLCH(LAB colour)
    {
        decimal C = (decimal)Math.Sqrt(Math.Pow((double)colour.a, 2) + Math.Pow((double)colour.b, 2));
        decimal H = (decimal) (Math.Atan((double)(colour.b / colour.a)) * (180.0/Math.PI));
        if (H < 0) H = H + 360.0m; return new LCH(colour.L, C, H); } public static XYZ toXYZ(LAB colour) { decimal fy = (colour.L + 16.0m) / 116.0m; decimal fx = (colour.a / 500.0m) + fy; decimal fz = fy - (colour.b / 200.0m); decimal x = XYZConversion(fx); decimal y = XYZConversion(fy); decimal z = XYZConversion(fz); return new XYZ(whiteD65.X * x, whiteD65.Y * y, whiteD65.Z * z); } private static decimal XYZConversion(decimal fComponent) { decimal component; if ((decimal)Math.Pow((double)fComponent, 3) > ColourConverter.epsilon)
        {
            component = (decimal)Math.Pow((double)fComponent, 3.0);
        }
        else
        {
            component = (fComponent - 16.0m / 116.0m) / 7.787m;
        }
        return component;
    }

    public static RGB toRGB(XYZ colour)
    {
        decimal[,] conversionMatrix = new decimal[3,3] { {3.2406m, -0.9689m,  0.0557m} , {-1.5372m,  1.8758m, -0.2040m}, {-0.4986m,  0.0415m,  1.0570m} };

        XYZ norm = new XYZ(colour.X / 100.0m, colour.Y / 100.0m, colour.Z / 100.0m);

        decimal r = norm.X * conversionMatrix[0, 0] + norm.Y * conversionMatrix[1, 0] + norm.Z * conversionMatrix[2, 0];
        decimal g = norm.X * conversionMatrix[0, 1] + norm.Y * conversionMatrix[1, 1] + norm.Z * conversionMatrix[2, 1];
        decimal b = norm.X * conversionMatrix[0, 2] + norm.Y * conversionMatrix[1, 2] + norm.Z * conversionMatrix[2, 2];

        r = sRGBConversion(r);
        g = sRGBConversion(g);
        b = sRGBConversion(b);

        return new RGB(r*255.0m, g*255.0m, b*255.0m);
    }

    private static decimal sRGBConversion(decimal colourComponent)
    {
        if (colourComponent > 0.0031308m)
        {
            colourComponent = 1.055m * (decimal)Math.Pow((double)colourComponent, (1.0 / 2.4)) - 0.055m;
        }
        else
        {
            colourComponent = 12.92m * colourComponent;
        }
        return colourComponent;
    }

    public static LAB toLAB(XYZ colour)
    {
        XYZ normalizedColour = new XYZ(colour.X / whiteD65.X, colour.Y / whiteD65.Y, colour.Z / whiteD65.Z);
        XYZ convertedColour = new XYZ(
            labConversion(normalizedColour.X), 
            labConversion(normalizedColour.Y), 
            labConversion(normalizedColour.Z)
            );
        return new LAB( 
            (116.0m * convertedColour.Y) - 16.0m, 
            500.0m * (convertedColour.X - convertedColour.Y), 
            200.0m * (convertedColour.Y - convertedColour.Z)
            );
    }

    private static decimal labConversion(decimal component)
    {
        if (component > ColourConverter.epsilon)
        {
            component = (decimal)Math.Pow((double) component, 1.0 / 3.0);
        }
        else
        {
            component = (7.787m * component) + (16.0m / 116.0m);
        }
        return component;
    }

    public static RGB normalizeRGB(RGB colour)
    {
        return new RGB(colour.R / 255.0m, colour.G/255.0m, colour.B/255.0m);
    }

    public static RGB deNormalizeRGB(RGB colour)
    {
        return new RGB(colour.R * 255.0m, colour.G * 255.0m, colour.B * 255.0m);
    }

}

Was hat sich hier grossartig geändert?

Zum einem die vereinfachte Mehtode:

public static XYZ toXYZ(LAB colour)
{
    decimal fy = (colour.L + 16.0m) / 116.0m;
    decimal fx = (colour.a / 500.0m) + fy;
    decimal fz = fy - (colour.b / 200.0m);

    decimal x = XYZConversion(fx);
    decimal y = XYZConversion(fy);
    decimal z = XYZConversion(fz);

    return new XYZ(whiteD65.X * x, whiteD65.Y * y, whiteD65.Z * z);
}

Die lange, dreimal benutzte Formel hab ich in eine eigene private Methode ausgelagert:

private static decimal XYZConversion(decimal fComponent)
{
    decimal component;
    if ((decimal)Math.Pow((double)fComponent, 3) > ColourConverter.epsilon)
    {
        component = (decimal)Math.Pow((double)fComponent, 3.0);
    }
    else
    {
        component = (fComponent - 16.0m / 116.0m) / 7.787m;
    }
    return component;
}

Und das sind die beiden neuen Konvertierungsmethoden:

public static LAB toLAB(XYZ colour)
{
    XYZ normalizedColour = new XYZ(colour.X / whiteD65.X, colour.Y / whiteD65.Y, colour.Z / whiteD65.Z);
    XYZ convertedColour = new XYZ(
        labConversion(normalizedColour.X), 
        labConversion(normalizedColour.Y), 
        labConversion(normalizedColour.Z)
        );
    return new LAB( 
        (116.0m * convertedColour.Y) - 16.0m, 
        500.0m * (convertedColour.X - convertedColour.Y), 
        200.0m * (convertedColour.Y - convertedColour.Z)
        );
}

private static decimal labConversion(decimal component)
{
    if (component > ColourConverter.epsilon)
    {
        component = (decimal)Math.Pow((double) component, 1.0 / 3.0);
    }
    else
    {
        component = (7.787m * component) + (16.0m / 116.0m);
    }
    return component;
}

Vollständigerweise gibt es natürlich auch einen Unti-Test dazu:

[TestMethod()]
public void toLABTest()
{
    XYZ colour = new XYZ(56.064m, 50.492m, 8.159m); // TODO: Passenden Wert initialisieren
    LAB expected = new LAB(76.370m, 21.178m, 74.941m); // TODO: Passenden Wert initialisieren
    LAB actual;
    actual = ColourConverter.toLAB(colour);
    Assert.AreEqual((double)expected.L, (double)actual.L, 0.01d);
    Assert.AreEqual((double)expected.a, (double)actual.a, 0.01d);
    Assert.AreEqual((double)expected.b, (double)actual.b, 0.01d);
    // Assert.Inconclusive("Überprüfen Sie die Richtigkeit dieser Testmethode.");
}

 

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.