Files

704 lines
20 KiB
C#

/* NeuQuant Neural-Net Quantization Algorithm
* ------------------------------------------
*
* Copyright (c) 1994 Anthony Dekker
*
* NeuQuant algorithm ported to a Burst-compatible, flat data layout. The
* learning order and integer arithmetic intentionally match the previous
* managed implementation so existing GIF output remains stable.
*/
using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
namespace uGIF
{
public sealed class NeuQuant
{
private const int NetSize = 256;
private const int NetworkStride = 4;
private const int NetworkLength = NetSize * NetworkStride;
private const int InitRad = NetSize >> 3;
private readonly Color32[] picture;
private readonly int lengthCount;
private readonly int sampleFactor;
private readonly int pictureWidth;
private readonly int pictureHeight;
private readonly bool flipVertical;
private int[] network;
private int[] netIndex;
public NeuQuant(Color32[] thePicture, int length, int sample)
: this(thePicture, length, sample, 0, 0, false)
{
}
public NeuQuant(
Color32[] thePicture,
int length,
int sample,
int width,
int height,
bool shouldFlipVertical)
{
picture = thePicture ?? throw new ArgumentNullException(nameof(thePicture));
lengthCount = Math.Min(length, thePicture.Length);
sampleFactor = Math.Max(1, sample);
pictureWidth = width;
pictureHeight = height;
flipVertical = shouldFlipVertical &&
width > 0 && height > 0 && width * height == lengthCount;
}
public byte[] Process()
{
if (lengthCount <= 0)
return new byte[NetSize * 3];
network = new int[NetworkLength];
netIndex = new int[NetSize];
var colorMap = new byte[NetSize * 3];
using (var nativePicture = new NativeArray<Color32>(picture, Allocator.TempJob))
using (var nativeNetwork = new NativeArray<int>(NetworkLength, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeNetIndex = new NativeArray<int>(NetSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeBias = new NativeArray<int>(NetSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeFrequency = new NativeArray<int>(NetSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeRadiusPower = new NativeArray<int>(InitRad, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeColorMap = new NativeArray<byte>(colorMap.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
{
new NeuQuantProcessJob
{
Picture = nativePicture,
Network = nativeNetwork,
NetIndex = nativeNetIndex,
Bias = nativeBias,
Frequency = nativeFrequency,
RadiusPower = nativeRadiusPower,
ColorMap = nativeColorMap,
LengthCount = lengthCount,
SampleFactor = sampleFactor,
PictureWidth = pictureWidth,
PictureHeight = pictureHeight,
FlipVertical = flipVertical ? 1 : 0
}.Schedule().Complete();
nativeNetwork.CopyTo(network);
nativeNetIndex.CopyTo(netIndex);
nativeColorMap.CopyTo(colorMap);
}
return colorMap;
}
public void ProcessAndMap(
bool useTransparency,
int transparentIndex,
out byte[] colorMap,
out byte[] indexedPixels)
{
colorMap = new byte[NetSize * 3];
indexedPixels = new byte[lengthCount];
network = new int[NetworkLength];
netIndex = new int[NetSize];
if (lengthCount <= 0)
return;
using (var nativePicture = new NativeArray<Color32>(picture, Allocator.TempJob))
using (var nativeNetwork = new NativeArray<int>(NetworkLength, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeNetIndex = new NativeArray<int>(NetSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeBias = new NativeArray<int>(NetSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeFrequency = new NativeArray<int>(NetSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeRadiusPower = new NativeArray<int>(InitRad, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeColorMap = new NativeArray<byte>(colorMap.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
using (var nativeResult = new NativeArray<byte>(indexedPixels.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
{
JobHandle processHandle = new NeuQuantProcessJob
{
Picture = nativePicture,
Network = nativeNetwork,
NetIndex = nativeNetIndex,
Bias = nativeBias,
Frequency = nativeFrequency,
RadiusPower = nativeRadiusPower,
ColorMap = nativeColorMap,
LengthCount = lengthCount,
SampleFactor = sampleFactor,
PictureWidth = pictureWidth,
PictureHeight = pictureHeight,
FlipVertical = flipVertical ? 1 : 0
}.Schedule();
JobHandle mapHandle = new MapPixelsJob
{
Pixels = nativePicture,
Network = nativeNetwork,
NetIndex = nativeNetIndex,
ColorMap = nativeColorMap,
Result = nativeResult,
UseTransparency = useTransparency ? 1 : 0,
TransparentIndex = transparentIndex,
Width = pictureWidth,
Height = pictureHeight,
FlipVertical = flipVertical ? 1 : 0
}.Schedule(lengthCount, 256, processHandle);
mapHandle.Complete();
nativeNetwork.CopyTo(network);
nativeNetIndex.CopyTo(netIndex);
nativeColorMap.CopyTo(colorMap);
nativeResult.CopyTo(indexedPixels);
}
}
public byte[] MapPixels(
Color32[] sourcePixels,
byte[] colorMap,
bool useTransparency,
int transparentIndex,
int width,
int height,
bool shouldFlipVertical)
{
if (network == null || netIndex == null)
throw new InvalidOperationException("Process() must be called before MapPixels().");
if (sourcePixels == null)
throw new ArgumentNullException(nameof(sourcePixels));
if (colorMap == null)
throw new ArgumentNullException(nameof(colorMap));
var result = new byte[sourcePixels.Length];
bool validFlip = shouldFlipVertical && width > 0 && height > 0 && width * height == sourcePixels.Length;
using (var nativePixels = new NativeArray<Color32>(sourcePixels, Allocator.TempJob))
using (var nativeNetwork = new NativeArray<int>(network, Allocator.TempJob))
using (var nativeNetIndex = new NativeArray<int>(netIndex, Allocator.TempJob))
using (var nativeColorMap = new NativeArray<byte>(colorMap, Allocator.TempJob))
using (var nativeResult = new NativeArray<byte>(result.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory))
{
new MapPixelsJob
{
Pixels = nativePixels,
Network = nativeNetwork,
NetIndex = nativeNetIndex,
ColorMap = nativeColorMap,
Result = nativeResult,
UseTransparency = useTransparency ? 1 : 0,
TransparentIndex = transparentIndex,
Width = width,
Height = height,
FlipVertical = validFlip ? 1 : 0
}.Schedule(result.Length, 256).Complete();
nativeResult.CopyTo(result);
}
return result;
}
// Preserved for compatibility with callers outside GIFEncoder.
public int Map(int b, int g, int r)
{
if (network == null || netIndex == null)
throw new InvalidOperationException("Process() must be called before Map().");
int bestDistance = 1000;
int best = -1;
int i = netIndex[g];
int j = i - 1;
while (i < NetSize || j >= 0)
{
if (i < NetSize)
{
int offset = i * NetworkStride;
int distance = network[offset + 1] - g;
if (distance >= bestDistance)
i = NetSize;
else
{
i++;
if (distance < 0) distance = -distance;
int delta = network[offset] - b;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
delta = network[offset + 2] - r;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
bestDistance = distance;
best = network[offset + 3];
}
}
}
}
if (j >= 0)
{
int offset = j * NetworkStride;
int distance = g - network[offset + 1];
if (distance >= bestDistance)
j = -1;
else
{
j--;
if (distance < 0) distance = -distance;
int delta = network[offset] - b;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
delta = network[offset + 2] - r;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
bestDistance = distance;
best = network[offset + 3];
}
}
}
}
}
return best;
}
[BurstCompile]
private struct NeuQuantProcessJob : IJob
{
private const int Prime1 = 499;
private const int Prime2 = 491;
private const int Prime3 = 487;
private const int Prime4 = 503;
private const int MinPictureBytes = 3 * Prime4;
private const int NetBiasShift = 4;
private const int NCycles = 100;
private const int IntBiasShift = 16;
private const int IntBias = 1 << IntBiasShift;
private const int GammaShift = 10;
private const int BetaShift = 10;
private const int Beta = IntBias >> BetaShift;
private const int BetaGamma = IntBias << (GammaShift - BetaShift);
private const int RadiusBiasShift = 6;
private const int RadiusBias = 1 << RadiusBiasShift;
private const int InitRadius = InitRad * RadiusBias;
private const int RadiusDecrease = 30;
private const int AlphaBiasShift = 10;
private const int InitialAlpha = 1 << AlphaBiasShift;
private const int RadBiasShift = 8;
private const int RadBias = 1 << RadBiasShift;
private const int AlphaRadBias = 1 << (AlphaBiasShift + RadBiasShift);
[ReadOnly] public NativeArray<Color32> Picture;
public NativeArray<int> Network;
public NativeArray<int> NetIndex;
public NativeArray<int> Bias;
public NativeArray<int> Frequency;
public NativeArray<int> RadiusPower;
[WriteOnly] public NativeArray<byte> ColorMap;
public int LengthCount;
public int SampleFactor;
public int PictureWidth;
public int PictureHeight;
public int FlipVertical;
public void Execute()
{
InitializeNetwork();
Learn();
UnbiasNetwork();
BuildIndex();
BuildColorMap();
}
private void InitializeNetwork()
{
for (int i = 0; i < NetSize; i++)
{
int offset = i * NetworkStride;
int value = (i << (NetBiasShift + 8)) / NetSize;
Network[offset] = value;
Network[offset + 1] = value;
Network[offset + 2] = value;
Network[offset + 3] = 0;
Frequency[i] = IntBias / NetSize;
Bias[i] = 0;
}
}
private void Learn()
{
int sampleFactor = SampleFactor;
if (LengthCount < MinPictureBytes)
sampleFactor = 1;
int alphaDecrease = 30 + (sampleFactor - 1) / 3;
int samplePixels = LengthCount / (3 * sampleFactor);
int delta = samplePixels / NCycles;
int alpha = InitialAlpha;
int radius = InitRadius;
int rad = radius >> RadiusBiasShift;
if (rad <= 1) rad = 0;
UpdateRadiusPower(rad, alpha);
int step;
if (LengthCount < MinPictureBytes)
step = 3;
else if (LengthCount % Prime1 != 0)
step = 3 * Prime1;
else if (LengthCount % Prime2 != 0)
step = 3 * Prime2;
else if (LengthCount % Prime3 != 0)
step = 3 * Prime3;
else
step = 3 * Prime4;
int pixelIndex = 0;
int i = 0;
while (i < samplePixels)
{
Color32 pixel = Picture[ResolvePictureIndex(pixelIndex)];
int b = (pixel.r & 0xff) << NetBiasShift;
int g = (pixel.g & 0xff) << NetBiasShift;
int r = (pixel.b & 0xff) << NetBiasShift;
int winner = Contest(b, g, r);
AlterSingle(alpha, winner, b, g, r);
if (rad != 0)
AlterNeighbours(rad, winner, b, g, r);
pixelIndex += step;
if (pixelIndex >= LengthCount)
pixelIndex -= LengthCount;
i++;
if (delta == 0) delta = 1;
if (i % delta == 0)
{
alpha -= alpha / alphaDecrease;
radius -= radius / RadiusDecrease;
rad = radius >> RadiusBiasShift;
if (rad <= 1) rad = 0;
UpdateRadiusPower(rad, alpha);
}
}
}
private int ResolvePictureIndex(int logicalIndex)
{
if (FlipVertical == 0)
return logicalIndex;
int y = logicalIndex / PictureWidth;
int x = logicalIndex - y * PictureWidth;
return (PictureHeight - 1 - y) * PictureWidth + x;
}
private void UpdateRadiusPower(int rad, int alpha)
{
for (int i = 0; i < rad; i++)
RadiusPower[i] = alpha * (((rad * rad - i * i) * RadBias) / (rad * rad));
}
private int Contest(int b, int g, int r)
{
int bestDistance = int.MaxValue;
int bestBiasDistance = int.MaxValue;
int bestPosition = -1;
int bestBiasPosition = -1;
for (int i = 0; i < NetSize; i++)
{
int offset = i * NetworkStride;
int distance = Abs(Network[offset] - b) +
Abs(Network[offset + 1] - g) +
Abs(Network[offset + 2] - r);
if (distance < bestDistance)
{
bestDistance = distance;
bestPosition = i;
}
int biasDistance = distance - (Bias[i] >> (IntBiasShift - NetBiasShift));
if (biasDistance < bestBiasDistance)
{
bestBiasDistance = biasDistance;
bestBiasPosition = i;
}
int betaFrequency = Frequency[i] >> BetaShift;
Frequency[i] -= betaFrequency;
Bias[i] += betaFrequency << GammaShift;
}
Frequency[bestPosition] += Beta;
Bias[bestPosition] -= BetaGamma;
return bestBiasPosition;
}
private void AlterSingle(int alpha, int index, int b, int g, int r)
{
int offset = index * NetworkStride;
Network[offset] -= alpha * (Network[offset] - b) / InitialAlpha;
Network[offset + 1] -= alpha * (Network[offset + 1] - g) / InitialAlpha;
Network[offset + 2] -= alpha * (Network[offset + 2] - r) / InitialAlpha;
}
private void AlterNeighbours(int rad, int index, int b, int g, int r)
{
int low = index - rad;
if (low < -1) low = -1;
int high = index + rad;
if (high > NetSize) high = NetSize;
int upper = index + 1;
int lower = index - 1;
int powerIndex = 1;
while (upper < high || lower > low)
{
int power = RadiusPower[powerIndex++];
if (upper < high)
AlterNeighbour(upper++, power, b, g, r);
if (lower > low)
AlterNeighbour(lower--, power, b, g, r);
}
}
private void AlterNeighbour(int index, int power, int b, int g, int r)
{
int offset = index * NetworkStride;
Network[offset] -= power * (Network[offset] - b) / AlphaRadBias;
Network[offset + 1] -= power * (Network[offset + 1] - g) / AlphaRadBias;
Network[offset + 2] -= power * (Network[offset + 2] - r) / AlphaRadBias;
}
private void UnbiasNetwork()
{
for (int i = 0; i < NetSize; i++)
{
int offset = i * NetworkStride;
Network[offset] >>= NetBiasShift;
Network[offset + 1] >>= NetBiasShift;
Network[offset + 2] >>= NetBiasShift;
Network[offset + 3] = i;
}
}
private void BuildIndex()
{
int previousColor = 0;
int startPosition = 0;
for (int i = 0; i < NetSize; i++)
{
int currentOffset = i * NetworkStride;
int smallestPosition = i;
int smallestValue = Network[currentOffset + 1];
for (int j = i + 1; j < NetSize; j++)
{
int value = Network[j * NetworkStride + 1];
if (value < smallestValue)
{
smallestPosition = j;
smallestValue = value;
}
}
if (i != smallestPosition)
{
int smallestOffset = smallestPosition * NetworkStride;
for (int component = 0; component < NetworkStride; component++)
{
int temp = Network[smallestOffset + component];
Network[smallestOffset + component] = Network[currentOffset + component];
Network[currentOffset + component] = temp;
}
}
if (smallestValue != previousColor)
{
NetIndex[previousColor] = (startPosition + i) >> 1;
for (int j = previousColor + 1; j < smallestValue; j++)
NetIndex[j] = i;
previousColor = smallestValue;
startPosition = i;
}
}
NetIndex[previousColor] = (startPosition + NetSize - 1) >> 1;
for (int j = previousColor + 1; j < NetSize; j++)
NetIndex[j] = NetSize - 1;
}
private void BuildColorMap()
{
// Bias is no longer needed after learning, so reuse it as the color-order table.
for (int i = 0; i < NetSize; i++)
Bias[Network[i * NetworkStride + 3]] = i;
int outputIndex = 0;
for (int i = 0; i < NetSize; i++)
{
int offset = Bias[i] * NetworkStride;
ColorMap[outputIndex++] = (byte)Network[offset];
ColorMap[outputIndex++] = (byte)Network[offset + 1];
ColorMap[outputIndex++] = (byte)Network[offset + 2];
}
}
private static int Abs(int value)
{
return value < 0 ? -value : value;
}
}
[BurstCompile]
private struct MapPixelsJob : IJobParallelFor
{
[ReadOnly] public NativeArray<Color32> Pixels;
[ReadOnly] public NativeArray<int> Network;
[ReadOnly] public NativeArray<int> NetIndex;
[ReadOnly] public NativeArray<byte> ColorMap;
[WriteOnly] public NativeArray<byte> Result;
public int UseTransparency;
public int TransparentIndex;
public int Width;
public int Height;
public int FlipVertical;
public void Execute(int index)
{
int sourceIndex = index;
if (FlipVertical != 0)
{
int y = index / Width;
int x = index - y * Width;
sourceIndex = (Height - 1 - y) * Width + x;
}
Color32 pixel = Pixels[sourceIndex];
if (UseTransparency != 0 && pixel.a < 128)
{
Result[index] = (byte)TransparentIndex;
return;
}
int mapped = MapColor(pixel.r & 0xff, pixel.g & 0xff, pixel.b & 0xff);
if (UseTransparency != 0 && mapped == TransparentIndex)
mapped = FindClosestOpaquePaletteIndex(pixel, TransparentIndex);
Result[index] = (byte)mapped;
}
private int MapColor(int b, int g, int r)
{
int bestDistance = 1000;
int best = -1;
int i = NetIndex[g];
int j = i - 1;
while (i < NetSize || j >= 0)
{
if (i < NetSize)
{
int offset = i * NetworkStride;
int distance = Network[offset + 1] - g;
if (distance >= bestDistance)
i = NetSize;
else
{
i++;
if (distance < 0) distance = -distance;
int delta = Network[offset] - b;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
delta = Network[offset + 2] - r;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
bestDistance = distance;
best = Network[offset + 3];
}
}
}
}
if (j >= 0)
{
int offset = j * NetworkStride;
int distance = g - Network[offset + 1];
if (distance >= bestDistance)
j = -1;
else
{
j--;
if (distance < 0) distance = -distance;
int delta = Network[offset] - b;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
delta = Network[offset + 2] - r;
if (delta < 0) delta = -delta;
distance += delta;
if (distance < bestDistance)
{
bestDistance = distance;
best = Network[offset + 3];
}
}
}
}
}
return best;
}
private int FindClosestOpaquePaletteIndex(Color32 color, int excludedIndex)
{
int bestIndex = 0;
int bestDistance = int.MaxValue;
int colorCount = ColorMap.Length / 3;
if (colorCount > NetSize)
colorCount = NetSize;
for (int i = 0; i < colorCount; i++)
{
if (i == excludedIndex)
continue;
int offset = i * 3;
int dr = color.r - (ColorMap[offset] & 0xff);
int dg = color.g - (ColorMap[offset + 1] & 0xff);
int db = color.b - (ColorMap[offset + 2] & 0xff);
int distance = dr * dr + dg * dg + db * db;
if (distance < bestDistance)
{
bestDistance = distance;
bestIndex = i;
}
}
return bestIndex;
}
}
}
}