C# Farbenmanagement #1

Für eine Auftragsarbeit musst ich mich Farbräumen und deren Konvertierungen herumschlagen. Herausgefallen ist dabei so eine Art kleine „Farbverwaltungs“-Klassenbibliothek, mal sehen vielleicht wird ja in Zukunft noch mehr draus, es gibt ja noch viele Farbräume zu begutachten.

Anfangen tut das mit den verschiedenen Farbmodellen die es gibt, konkret brauchte ich nur einige wenige:

  • L*ab – ein gängiger (uralter) Standard in der „Farbenindustrie“
  • L*CH – eine andere Darstellung der Farbwerte
  • XYZ – eine Beschreibung des CIE Farbraumes
  • sRGB – wird u.a. von Windows verwendet um Farben auf dem Bildschirm darzustellen

Falls ihr mehr zu diesem Thema wissen wollt findet ihr ganz viele Ressourcen unter: Easy RGB

Um kurz auf die Programmiertechnik einzugehen: ich habe mich dazu entschieden die Klassen so in der Art immutable zu machen, das heisst eine einmal erzeugte Instanz kann nicht mehr geändert werden. Man kann die Werte einer Instanz nur über den Konstruktor ändern. So richtig immutable ist es aber wiederum doch nicht programmier, weil instanzen der gleichen Klasse bzw. die von einer dieser Modelklassen erben könnten ja theoretisch Werte ändern. Aber ich hatte jetzt keine Lust mich da im Moment weiter einzulesen und das wirklich 100%ig zu machen.

Und nun zu den Listings, die Namespaces hab ich rausgenommen, könnt ihr ja euch ganz einfach dazudenken 😉

Hier das Modell für den Farbraum L*ab:

    public class LAB
    {
        private decimal _L;
        private decimal _a;
        private decimal _b;

        public decimal L { get { return _L; } }
        public decimal a { get { return _a; } }
        public decimal b { get { return _b; } }

        public LAB(decimal L, decimal a, decimal b)
        {
            _L = L;
            _a = a;
            _b = b;
        }

        public decimal deltaE(LAB otherColour)
        {
            return ColourCalculations.deltaE(this, otherColour);
        }
    }

Das Modell für den Farbraum L*CH:

    public class LCH
    {
        private decimal _L;
        private decimal _C;
        private decimal _H;

        public decimal L { get { return _L; } }
        public decimal C { get { return _C; } }
        public decimal H { get { return _H; } }

        public LCH(decimal L, decimal C, decimal H)
        {
            _L = L;
            _C = C;
            _H = H;
        }
    }

Das Modell für den Farbraum XYZ:

    public class XYZ
    {
        private decimal _X;
        private decimal _Y;
        private decimal _Z;

        public decimal X { get { return _X; } }
        public decimal Y { get { return _Y; } }
        public decimal Z { get { return _Z; } }

        public XYZ(decimal X, decimal Y, decimal Z)
        {
            _X = X;
            _Y = Y;
            _Z = Z;
        }
    }

Und zuguterletzt das Modell für RGB:

    public class RGB
    {
        private decimal _R;
        private decimal _G;
        private decimal _B;

        public decimal R { get { return _R; } }
        public decimal G { get { return _G; } }
        public decimal B { get { return _B; } }

        public RGB(decimal R, decimal G, decimal B)
        {
            _R = R;
            _G = G;
            _B = B;
        }
    }

Dann habe ich die Klasse geschrieben, die mir statische Methoden gibt um Farbmodelle in Farbmodelle eines anderen Systemes umzurechnen:

    public static class ColourConverter
    {
        // Daylight reference WHITE
        private static readonly XYZ whiteD65 = new XYZ(95.047m, 100.00m, 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/Math.PI));
            if (H < 0) H = H + 360;
            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 = 0;
            if ((decimal) Math.Pow((double) fx, 3) > ColourConverter.epsilon)
            {
                x = (decimal) Math.Pow((double) fx, 3);
            }
            else
            {
                x = (fx - 16.0m / 116.0m) / 7.787m;
            }

            decimal y = 0;
            if ((decimal)Math.Pow((double)fy, 3) > ColourConverter.epsilon)
            {
                y = (decimal)Math.Pow((double)fy, 3);
            }
            else
            {
                y = (fy - 16.0m / 116.0m) / 7.787m;
            }

            decimal z = 0;
            if ((decimal)Math.Pow((double)fz, 3) > ColourConverter.epsilon)
            {
                z = (decimal)Math.Pow((double)fz, 3);
            }
            else
            {
                z = (fz - 16.0m / 116.0m) / 7.787m;
            }

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

        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, colour.Y / 100, colour.Z / 100);

            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 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);
        }

    }

Um die ganze Richtigkeit dieser Konvertierungen hab ich für (fast) alle Methoden auch ein paar Unittests geschrieben, die – man mag es kaum glauben – auch erfolgreich durchlaufen:

    /// 
    ///Dies ist eine Testklasse für "ColourConverterTest" und soll
    ///alle ColourConverterTest Komponententests enthalten.
    ///
    [TestClass()]
    public class ColourConverterTest
    {
        private TestContext testContextInstance;

        /// 
        ///Ruft den Testkontext auf, der Informationen
        ///über und Funktionalität für den aktuellen Testlauf bietet, oder legt diesen fest.
        ///
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        #region Zusätzliche Testattribute
        // 
        //Sie können beim Verfassen Ihrer Tests die folgenden zusätzlichen Attribute verwenden:
        //
        //Mit ClassInitialize führen Sie Code aus, bevor Sie den ersten Test in der Klasse ausführen.
        //[ClassInitialize()]
        //public static void MyClassInitialize(TestContext testContext)
        //{
        //}
        //
        //Mit ClassCleanup führen Sie Code aus, nachdem alle Tests in einer Klasse ausgeführt wurden.
        //[ClassCleanup()]
        //public static void MyClassCleanup()
        //{
        //}
        //
        //Mit TestInitialize können Sie vor jedem einzelnen Test Code ausführen.
        //[TestInitialize()]
        //public void MyTestInitialize()
        //{
        //}
        //
        //Mit TestCleanup können Sie nach jedem einzelnen Test Code ausführen.
        //[TestCleanup()]
        //public void MyTestCleanup()
        //{
        //}
        //
        #endregion


        /// 
        ///Ein Test für "toXYZ"
        ///
        [TestMethod()]
        public void toXYZTest()
        {
            LAB colour = new LAB(76.37m, 21.18m, 74.94m); // TODO: Passenden Wert initialisieren
            XYZ expected = new XYZ(56.064m, 50.492m, 8.159m); // TODO: Passenden Wert initialisieren
            XYZ actual;
            actual = ColourConverter.toXYZ(colour);
            Assert.AreEqual((double)expected.X, (double)actual.X, 0.01d);
            Assert.AreEqual((double)expected.Y, (double)actual.Y, 0.01d);
            Assert.AreEqual((double)expected.Z, (double)actual.Z, 0.01d);
            // Assert.Inconclusive("Überprüfen Sie die Richtigkeit dieser Testmethode.");
        }

        /// 
        ///Ein Test für "toRGB"
        ///
        [TestMethod()]
        public void toRGBTest()
        {
            XYZ colour = new XYZ(56.064m, 50.492m, 8.159m); // TODO: Passenden Wert initialisieren
            RGB expected = new RGB(255.00m, 171.01m, 32.03m) ; // TODO: Passenden Wert initialisieren
            RGB actual;
            actual = ColourConverter.toRGB(colour);
            Assert.AreEqual((double)expected.R, (double)actual.R, 0.01d);
            Assert.AreEqual((double)expected.G, (double)actual.G, 0.01d);
            Assert.AreEqual((double)expected.B, (double)actual.B, 0.01d);
            // Assert.Inconclusive("Überprüfen Sie die Richtigkeit dieser Testmethode.");
        }

        /// 
        ///Ein Test für "toLCH"
        ///
        [TestMethod()]
        public void toLCHTest()
        {
            LAB colour = new LAB(76.37m, 21.18m, 74.94m); // TODO: Passenden Wert initialisieren
            LCH expected = new LCH(76.370m, 77.876m, 74.218m); // TODO: Passenden Wert initialisieren
            LCH actual;
            actual = ColourConverter.toLCH(colour);
            Assert.AreEqual((double)expected.L, (double)actual.L, 0.01d);
            Assert.AreEqual((double)expected.C, (double)actual.C, 0.01d);
            Assert.AreEqual((double)expected.H, (double)actual.H, 0.01d);
            // Assert.Inconclusive("Überprüfen Sie die Richtigkeit dieser Testmethode.");
        }
    }

So, das war der Anfang für das Farbzeug – was dann noch kommt sind eine Klasse die noch gewisse Berechnungen durchführt wie „DeltaE“ und Konsorten.
Wenn ich dann mehr Zeit habe werde ich sicher noch das eine oder andere Farbmodell hinzufügen – doch fürs erste konzetriere ich mich auf die für die Arbeit unbedingt notwendigen Komponenten.

Schreibe einen Kommentar

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