OSDN Git Service

Replace the color quantizer from Octree to NeuQuant
authorKazuhiro <fujieda@users.osdn.me>
Fri, 23 Oct 2015 11:28:57 +0000 (20:28 +0900)
committerKazuhiro <fujieda@users.osdn.me>
Fri, 23 Oct 2015 16:26:33 +0000 (01:26 +0900)
BurageSnap/AnimationGifEncoder.cs
BurageSnap/BurageSnap.csproj
BurageSnap/NeuQuant.cs [new file with mode: 0644]

index a0b53ae..8d76503 100644 (file)
@@ -39,7 +39,7 @@ namespace BurageSnap
         public void AddFrame(Bitmap bmp, int delay)
         {
             var gif = new MemoryStream();
-            using (var quant = OctreeQuantizer.Quantize(bmp))
+            using (var quant = NeuQuant.Quantize(bmp, 10))
                 quant.Save(gif, ImageFormat.Gif);
             gif.Position = 6; // skip header
             var lsd = new byte[7];
index 2508cc3..d993dfd 100644 (file)
@@ -59,6 +59,7 @@
     <Compile Include="ConfirmDialog.Designer.cs">
       <DependentUpon>ConfirmDialog.cs</DependentUpon>
     </Compile>
+    <Compile Include="NeuQuant.cs" />
     <Compile Include="OctreeQuantizer.cs" />
     <Compile Include="Recorder.cs" />
     <Compile Include="Config.cs" />
diff --git a/BurageSnap/NeuQuant.cs b/BurageSnap/NeuQuant.cs
new file mode 100644 (file)
index 0000000..53d7fe6
--- /dev/null
@@ -0,0 +1,455 @@
+// Copyright (C) 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>
+//
+// This program is part of BurageSnap.
+//
+// BurageSnap is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+/* NeuQuant Neural-Net Quantization Algorithm
+ * ------------------------------------------
+ *
+ * Copyright (c) 1994 Anthony Dekker
+ *
+ * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
+ * See "Kohonen neural networks for optimal colour quantization"
+ * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
+ * for a discussion of the algorithm.
+ * See also  http://www.acm.org/~dekker/NEUQUANT.HTML
+ *
+ * Any party obtaining a copy of these files from the author, directly or
+ * indirectly, is granted, free of charge, a full and unrestricted irrevocable,
+ * world-wide, paid up, royalty-free, nonexclusive right and license to deal
+ * in this software and documentation files (the "Software"), including without
+ * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons who receive
+ * copies from any such party to do so, with the only requirement being
+ * that this copyright notice remain intact.
+ */
+
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace BurageSnap
+{
+    public class NeuQuant
+    {
+        private const int Cycles = 100; // learning cycles
+        private const int NetSize = 256; // colors used
+        private const int Specials = 2; // reserved colors
+        private const int CutNetSize = NetSize - Specials;
+        private const int MaxNetPos = NetSize - 1;
+        private const int InitRad = NetSize / 8; // for 256 cols, radius starts at 32
+        private const int RadiusBiasShift = 6;
+        private const int RadiusBias = 1 << RadiusBiasShift;
+        private const int InitBiasRadius = InitRad * RadiusBias;
+        private const int RadiusDec = 30; // factor of 1/30 each cycle
+        private const int AlphaBiasShift = 10; // alpha starts at 1
+        private const int InitAlpha = 1 << AlphaBiasShift; // biased by 10 bits
+        private const double Gamma = 1024.0;
+        private const double Beta = 1.0 / 1024.0;
+        private const double BetaGamma = Beta * Gamma;
+
+        private readonly double[][] _network = new double[NetSize][]; // the network itself
+        private readonly int[][] _colormap = new int[NetSize][]; // the network itself
+        private readonly int[] _netIndex = new int[256]; // for network lookup - really 256
+        private readonly double[] _bias = new double[NetSize]; // bias and freq arrays for learning
+        private readonly double[] _freq = new double[NetSize];
+
+        // four primes near 500 - assume no image has a length so large
+        // that it is divisible by all four primes
+        private const int Prime1 = 499;
+        private const int Prime2 = 491;
+        private const int Prime3 = 487;
+        private const int Prime4 = 503;
+
+        private int[] _pixels;
+        private readonly int _sampleFac;
+
+        public NeuQuant(Bitmap bmp)
+        {
+            _sampleFac = 1;
+            SetPixels(bmp);
+            SetUpArrays();
+        }
+
+        public NeuQuant(Bitmap bmp, int sample)
+        {
+            if (sample < 1 || 30 < sample)
+                throw new ArgumentException();
+            _sampleFac = sample;
+            SetPixels(bmp);
+            SetUpArrays();
+        }
+
+        public static Bitmap Quantize(Bitmap bmp, int sample)
+        {
+            var nq = new NeuQuant(bmp, sample);
+            nq.Init();
+            var width = bmp.Width;
+            var height = bmp.Height;
+            var bmp8 = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
+            var palette = bmp8.Palette;
+            for (var i = 0; i < NetSize; i++)
+                palette.Entries[i] = nq.GetColor(i);
+            bmp8.Palette = palette;
+            var data8 = bmp8.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bmp8.PixelFormat);
+            Parallel.For(0, height, h =>
+            {
+                for (var x = 0; x < width; x++)
+                {
+                    unsafe
+                    {
+                        var pix = nq._pixels[h * width + x];
+                        var ptr = (byte*)data8.Scan0 + h * data8.Stride + x;
+                        *ptr = (byte)nq.Lookup(pix);
+                    }
+                }
+            });
+            bmp8.UnlockBits(data8);
+            return bmp8;
+        }
+
+        public int ColorCount => NetSize;
+
+        public Color GetColor(int i)
+        {
+            if (i < 0 || i >= NetSize)
+                return Color.Empty;
+            return Color.FromArgb(_colormap[i][2], _colormap[i][1], _colormap[i][0]);
+        }
+
+        private void SetUpArrays()
+        {
+            _network[0] = new[] {0.0, 0.0, 0.0};
+            _network[1] = new[] {255.0, 255.0, 255.0};
+
+            for (var i = 0; i < Specials; i++)
+            {
+                _freq[i] = 1.0 / NetSize;
+                _bias[i] = 0.0;
+            }
+
+            for (var i = Specials; i < NetSize; i++)
+            {
+                var p = _network[i] = new double[3];
+                p[0] = (255.0 * (i - Specials)) / CutNetSize;
+                p[1] = (255.0 * (i - Specials)) / CutNetSize;
+                p[2] = (255.0 * (i - Specials)) / CutNetSize;
+                _freq[i] = 1.0 / NetSize;
+                _freq[i] = 0.0;
+            }
+
+            for (var i = 0; i < NetSize; i++)
+                _colormap[i] = new int[4];
+        }
+
+        private void SetPixels(Bitmap bmp)
+        {
+            var width = bmp.Width;
+            var height = bmp.Height;
+            _pixels = new int[width * height];
+            var data = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly,
+                PixelFormat.Format32bppArgb);
+            Marshal.Copy(data.Scan0, _pixels, 0, width * height);
+            bmp.UnlockBits(data);
+        }
+
+        public void Init()
+        {
+            Learn();
+            Fix();
+            InxBuild();
+        }
+
+        private void Learn()
+        {
+            var biasRadius = InitBiasRadius;
+            var alphadec = 30 + ((_sampleFac - 1) / 3);
+            var lengthCount = _pixels.Length;
+            var samplepixels = lengthCount / _sampleFac;
+            var delta = samplepixels / Cycles;
+            var alpha = InitAlpha;
+
+            var rad = biasRadius >> RadiusBiasShift;
+            if (rad <= 1)
+                rad = 0;
+
+            var step = (lengthCount % Prime1) != 0
+                ? Prime1
+                : (lengthCount % Prime2) != 0
+                    ? Prime2
+                    : (lengthCount % Prime3) != 0 ? Prime3 : Prime4;
+
+            var pos = 0;
+            var i = 0;
+            while (i < samplepixels)
+            {
+                var p = _pixels[pos];
+                var red = (p >> 16) & 0xff;
+                var green = (p >> 8) & 0xff;
+                var blue = (p) & 0xff;
+
+                double b = blue;
+                double g = green;
+                double r = red;
+
+                var j = SpecialFind(b, g, r);
+                j = j < 0 ? Contest(b, g, r) : j;
+
+                if (j >= Specials)
+                {
+                    // don't learn for specials
+                    var a = (1.0 * alpha) / InitAlpha;
+                    AlterSingle(a, j, b, g, r);
+                    if (rad > 0)
+                        AlterNeigh(a, rad, j, b, g, r); // alter neighbours
+                }
+
+                pos += step;
+                while (pos >= lengthCount)
+                    pos -= lengthCount;
+
+                if (delta == 0 || ++i % delta != 0)
+                    continue;
+                alpha -= alpha / alphadec;
+                biasRadius -= biasRadius / RadiusDec;
+                rad = biasRadius >> RadiusBiasShift;
+                if (rad <= 1)
+                    rad = 0;
+            }
+        }
+
+        private void AlterSingle(double alpha, int i, double b, double g, double r)
+        {
+            // Move neuron i towards biased (b,g,r) by factor alpha
+            var n = _network[i]; // alter hit neuron
+            n[0] -= (alpha * (n[0] - b));
+            n[1] -= (alpha * (n[1] - g));
+            n[2] -= (alpha * (n[2] - r));
+        }
+
+        private void AlterNeigh(double alpha, int rad, int i, double b, double g, double r)
+        {
+            var lo = i - rad;
+            if (lo < Specials - 1) lo = Specials - 1;
+            var hi = i + rad;
+            if (hi > NetSize) hi = NetSize;
+
+            var j = i + 1;
+            var k = i - 1;
+            var q = 0;
+            while ((j < hi) || (k > lo))
+            {
+                var a = (alpha * (rad * rad - q * q)) / (rad * rad);
+                q++;
+                if (j < hi)
+                {
+                    var p = _network[j];
+                    p[0] -= (a * (p[0] - b));
+                    p[1] -= (a * (p[1] - g));
+                    p[2] -= (a * (p[2] - r));
+                    j++;
+                }
+                if (k > lo)
+                {
+                    var p = _network[k];
+                    p[0] -= (a * (p[0] - b));
+                    p[1] -= (a * (p[1] - g));
+                    p[2] -= (a * (p[2] - r));
+                    k--;
+                }
+            }
+        }
+
+        private int Contest(double b, double g, double r)
+        {
+            // Search for biased BGR values
+            // finds closest neuron (min dist) and updates freq
+            // finds best neuron (min dist-bias) and returns position
+            // for frequently chosen neurons, freq[i] is high and bias[i] is negative
+            // bias[i] = gamma*((1/netsize)-freq[i])
+
+            var bestd = double.MaxValue;
+            var bestbiasd = bestd;
+            var bestpos = -1;
+            var bestbiaspos = bestpos;
+            for (var i = Specials; i < NetSize; i++)
+            {
+                var n = _network[i];
+                var dist = Math.Abs(n[0] - b) + Math.Abs(n[1] - g) + Math.Abs(n[2] - r);
+                if (dist < bestd)
+                {
+                    bestd = dist;
+                    bestpos = i;
+                }
+                var biasdist = dist - _bias[i];
+                if (biasdist < bestbiasd)
+                {
+                    bestbiasd = biasdist;
+                    bestbiaspos = i;
+                }
+                _freq[i] -= Beta * _freq[i];
+                _bias[i] += BetaGamma * _freq[i];
+            }
+            _freq[bestpos] += Beta;
+            _bias[bestpos] -= BetaGamma;
+            return bestbiaspos;
+        }
+
+        private int SpecialFind(double b, double g, double r)
+        {
+            for (var i = 0; i < Specials; i++)
+            {
+                var n = _network[i];
+                // ReSharper disable CompareOfFloatsByEqualityOperator
+                if (n[0] == b && n[1] == g && n[2] == r)
+                    return i;
+                // ReSharper restore CompareOfFloatsByEqualityOperator
+            }
+            return -1;
+        }
+
+        private void Fix()
+        {
+            for (var i = 0; i < NetSize; i++)
+            {
+                for (var j = 0; j < 3; j++)
+                {
+                    var x = (int)(0.5 + _network[i][j]);
+                    if (x < 0) x = 0;
+                    if (x > 255) x = 255;
+                    _colormap[i][j] = x;
+                }
+                _colormap[i][3] = i;
+            }
+        }
+
+        private void InxBuild()
+        {
+            // Insertion sort of network and building of netindex[0..255]
+            var previouscol = 0;
+            var startpos = 0;
+
+            for (var i = 0; i < NetSize; i++)
+            {
+                var smallpos = i;
+                var smallval = _colormap[i][1]; // index on g
+                // find smallest in i..netsize-1
+                for (var j = i + 1; j < NetSize; j++)
+                {
+                    var g = _colormap[j][1];
+                    if (g < smallval)
+                    {
+                        // index on g
+                        smallpos = j;
+                        smallval = g;
+                    }
+                }
+                // swap p (i) and q (smallpos) entries
+                if (i != smallpos)
+                {
+                    var tmp = _colormap[smallpos];
+                    _colormap[smallpos] = _colormap[i];
+                    _colormap[i] = tmp;
+                }
+                // smallval entry is now in position i
+                if (smallval != previouscol)
+                {
+                    _netIndex[previouscol] = (startpos + i) >> 1;
+                    for (var j = previouscol + 1; j < smallval; j++)
+                        _netIndex[j] = i;
+                    previouscol = smallval;
+                    startpos = i;
+                }
+            }
+            _netIndex[previouscol] = (startpos + MaxNetPos) >> 1;
+            for (var j = previouscol + 1; j < 256; j++)
+                _netIndex[j] = MaxNetPos; // really 256
+        }
+
+        public int Lookup(int pixel) => InxSearch(pixel);
+
+        public int Lookup(Color c) => InxSearch(c.R << 16 | c.G << 8 | c.B);
+
+        private int InxSearch(int pixel)
+        {
+            var r = (pixel >> 16) & 0xff;
+            var g = (pixel >> 8) & 0xff;
+            var b = (pixel) & 0xff;
+
+            // Search for BGR values 0..255 and return colour index
+            var bestd = 1000; // biggest possible dist is 256*3
+            var best = -1;
+            var i = _netIndex[g]; // index on g
+            var j = i - 1; // start at netindex[g] and work outwards
+
+            while (i < NetSize || j >= 0)
+            {
+                if (i < NetSize)
+                {
+                    var p = _colormap[i];
+                    var dist = p[1] - g; // inx key
+                    if (dist >= bestd)
+                    {
+                        i = NetSize; // stop iter
+                    }
+                    else
+                    {
+                        var db = p[0] - b;
+                        dist = (dist < 0 ? -dist : dist) + (db < 0 ? -db : db);
+                        if (dist < bestd)
+                        {
+                            var dr = p[2] - r;
+                            dist += dr < 0 ? -dr : dr;
+                            if (dist < bestd)
+                            {
+                                bestd = dist;
+                                best = i;
+                            }
+                        }
+                        i++;
+                    }
+                }
+                if (j >= 0)
+                {
+                    var p = _colormap[j];
+                    var dist = g - p[1]; // inx key - reverse dif
+                    if (dist >= bestd)
+                    {
+                        j = -1; // stop iter
+                    }
+                    else
+                    {
+                        var db = p[0] - b;
+                        dist = (dist < 0 ? -dist : dist) + (db < 0 ? -db : db);
+                        if (dist < bestd)
+                        {
+                            var dr = p[2] - r;
+                            dist += dr < 0 ? -dr : dr;
+                            if (dist < bestd)
+                            {
+                                bestd = dist;
+                                best = j;
+                            }
+                        }
+                        j--;
+                    }
+                }
+            }
+            return best;
+        }
+    }
+}
\ No newline at end of file