Как сохранить растровое изображение в виде 16-цветного GIF или PNG в оттенках серого в ASP.NET?
В ASP.NET C# я пытаюсь сохранить растровое изображение в виде 16-цветного непрозрачного изображения в градациях серого в формате PNG или GIF. Я предполагаю, что должен создать палитру и затем каким-то образом прикрепить палитру к изображению, но не уверен, как это сделать.
Исходное изображение - 24-битное цветное растровое изображение.
3 ответа
Это называется квантованием, и это сложно. Я много работал с этой проблемой, и мои лучшие результаты были в использовании квантования Октри и собственного алгоритма диффузии.
Ваша самая быстрая точка от A до B - захватить мой код (с открытым исходным кодом, но $69 для загрузки) и использовать чрезвычайно простой API, чтобы установить счетчик цветов до 16 и сохранить как GIF или PNG. Должно быть около 2 строк кода, если вы хотите сделать это через код позади... или, вы можете использовать строку запроса, если она находится в файловой системе:
image.bmp?format=gif&colors=16
Если изображение еще не в оттенках серого, вы можете сделать это с помощью класса ImageAttributes модуля. Полученный GIF автоматически будет иметь палитру градаций серого. Минимальная работа, отличные результаты.
Помните, что вам не нужно использовать его как HttpModule - это прежде всего библиотека для изменения размера, изменения и кодирования изображений.
Если вы хотите бросить свой собственный, вот что я начал с: http://codebetter.com/blogs/brendan.tompkins/archive/2007/06/14/gif-image-color-quantizer-now-with-safe-goodness.aspx
Прочитайте комментарии и исправьте арифметические ошибки указателя в соответствии с моими комментариями....
Тем не менее, никаких размышлений, и у вас могут возникнуть проблемы с запуском оригинала в среде, отличной от полного доверия. Я сделал много исправлений за эти годы, и я не помню их все.
Другая возможность, если вы не возражаете против использования открытого исходного кода, это загрузить Paint.Net. Я полагаю, что он может конвертироваться в оттенки серого, но я могу ошибаться, так как это было давно, так как у меня была необходимость его использовать.
На самом деле это совсем не сложно, как только вы получили наборы инструментов, и я собрал довольно много из них. Вещи, которые вам нужны:
- 16-цветная палитра оттенков серого.
- Функция для сопоставления данных изображения с ближайшим цветом (для получения данных в палитре)
- Функция для преобразования этих совпадений в 4-битные данные (полбайта на значение)
- Способ записи этих данных в новый 4-битный объект изображения.
Палитра проста. Значения серого - это цвета с одинаковым значением для красного, зеленого и синего, а для одинаковых шагов яркости между цветами на 16 цветах это значение находится в диапазоне от 0x00, 0x11, 0x22 и т. Д. До 0xFF. Не должно быть трудно сделать.
Следующим шагом является сопоставление цветов изображения с цветами палитры и создание байтового массива этих значений. Есть несколько методов для получения самого близкого соответствия, доступного на stackru. Этот вопрос имеет несколько из них:
Как сравнить цветовой объект и получить ближайший цвет в цвете []?
Далее идет сложная часть: преобразование фактических данных изображения в 4-битное.
Следует иметь в виду, что изображения сохраняются в каждой строке, и такая линия (называемая "линией развертки") не обязательно имеет такую же ширину, что и изображение. Например, в 4 битах на пиксель можно разместить 2 пикселя на каждый байт, поэтому логично, что ширина равна ширине, деленной на 2. Однако, если ширина является неравномерным числом, каждая строка будет иметь байт в конце, который является только наполовину. Системы не помещают туда первый пиксель следующей строки; вместо этого это просто оставлено пустым. А для 8-битных или даже 16-битных изображений я знаю, что шаг часто выравнивает строки сканирования по кратности 4 байта. Поэтому никогда не предполагайте, что ширина равна длине линии сканирования.
Для функции, которую я опишу в этом ответе, я использую минимально необходимую длину строки сканирования. Так как это только ширина, умноженная на длину битов, разделенную на восемь, плюс один, если в этом делении был остаток, его можно легко вычислить как ((bpp * width) + 7) / 8
,
Теперь, если вы сгенерировали свою палитру в оттенках серого, а затем создали байтовый массив, содержащий ближайшее значение палитры для каждого пикселя на изображении, у вас есть все значения для передачи в действительную функцию преобразования 8-бит в 4-бит.
Я написал функцию для преобразования 8-битных данных в любую длину битов. Так что это понадобится bitsLength=4
для вашего 4-битного изображения.
Параметр BigEndian будет определять, переключаются ли значения внутри одного байта или нет. Я не уверен насчет изображений.Net здесь, но я знаю, что многие форматы 1BPP используют биты с прямым порядком байтов, в то время как я столкнулся с форматами 4BPP, которые вместо этого начинались с наименьшего куска.
/// <summary>
/// Converts given raw image data for a paletted 8-bit image to lower amount of bits per pixel.
/// </summary>
/// <param name="data8bit">The eight bit per pixel image data</param>
/// <param name="width">The width of the image</param>
/// <param name="height">The height of the image</param>
/// <param name="newBpp">The new amount of bits per pixel</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <param name="bigEndian">Values inside a single byte are read from the largest to the smallest bit.</param>
/// <returns>The image data converted to the requested amount of bits per pixel.</returns>
private static Byte[] ConvertFrom8Bit(Byte[] data8bit, Int32 width, Int32 height, Int32 bitsLength, Boolean bigEndian)
{
if (newBpp > 8)
throw new ArgumentException("Cannot convert to bit format greater than 8!", "newBpp");
if (stride < width)
throw new ArgumentException("Stride is too small for the given width!", "stride");
if (data8bit.Length < stride * height)
throw new ArgumentException("Data given data is too small to contain an 8-bit image of the given dimensions", "data8bit");
Int32 parts = 8 / bitsLength;
// Amount of bytes to write per width
Int32 stride = ((bpp * width) + 7) / 8;
// Bit mask for reducing original data to actual bits maximum.
// Should not be needed if data is correct, but eh.
Int32 bitmask = (1 << bitsLength) - 1;
Byte[] dataXbit = new Byte[stride * height];
// Actual conversion porcess.
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
// This will hit the same byte multiple times
Int32 indexXbit = y * stride + x / parts;
// This will always get a new index
Int32 index8bit = y * width + x;
// Amount of bits to shift the data to get to the current pixel data
Int32 shift = (x % parts) * bitsLength;
// Reversed for big-endian
if (bigEndian)
shift = 8 - shift - bitsLength;
// Get data, reduce to bit rate, shift it and store it.
dataXbit[indexXbit] |= (Byte)((data8bit[index8bit] & bitmask) << shift);
}
}
return dataXbit;
}
Следующим шагом является создание изображения с правильными размерами и форматом пикселя, открытие его массива в памяти и выгрузка в него ваших данных. Формат пикселя для 16-цветного изображения PixelFormat.Format4bppIndexed
,
/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data</param>
/// <param name="pixelFormat"></param>
/// <param name="palette">Color palette</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette)
{
if (width == 0 || height == 0)
return null;
Bitmap newImage = new Bitmap(width, height, pixelFormat);
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
CopyMemory(targetData.Scan0, sourceData, sourceData.Length, stride, targetData.Stride);
newImage.UnlockBits(targetData);
// For 8-bit images, set the palette.
if ((pixelFormat == PixelFormat.Format8bppIndexed || pixelFormat == PixelFormat.Format4bppIndexed) && palette != null)
{
ColorPalette pal = newImage.Palette;
for (Int32 i = 0; i < pal.Entries.Length; i++)
if (i < palette.Length)
pal.Entries[i] = palette[i];
newImage.Palette = pal;
}
return newImage;
}
И, наконец, функции для копирования памяти, используемые этим. Как видите, этот метод специально копирует построчно, используя шаг, заданный в качестве аргумента, поэтому внутренний шаг, используемый Bitmap
что.Net Framework создает можно игнорировать. В любом случае оно будет одинаковым или большим.
public static void CopyMemory(IntPtr target, Byte[] sourceBytes, Int32 length, Int32 origStride, Int32 targetStride)
{
IntPtr unmanagedPointer = Marshal.AllocHGlobal(sourceBytes.Length);
Marshal.Copy(sourceBytes, 0, unmanagedPointer, sourceBytes.Length);
CopyMemory(target, unmanagedPointer, length, origStride, targetStride);
Marshal.FreeHGlobal(unmanagedPointer);
}
public static void CopyMemory(IntPtr target, IntPtr source, Int32 length, Int32 origStride, Int32 targetStride)
{
IntPtr sourcePos = source;
IntPtr destPos = target;
Int32 minStride = Math.Min(origStride, targetStride);
Byte[] imageData = new Byte[targetStride];
while (length >= origStride && length > 0)
{
Marshal.Copy(sourcePos, imageData, 0, minStride);
Marshal.Copy(imageData, 0, destPos, targetStride);
length -= origStride;
sourcePos = new IntPtr(sourcePos.ToInt64() + origStride);
destPos = new IntPtr(destPos.ToInt64() + targetStride);
}
if (length > 0)
{
Marshal.Copy(sourcePos, imageData, 0, length);
Marshal.Copy(imageData, 0, destPos, length);
}
}