WedX - журнал о программировании и компьютерных науках

Как правильно применить цветовую палитру к изображению?

Поэтому мне дали задание переписать программу, которая преобразует изображение в 3 разных размера, 1 из которых представляет собой 256-цветное изображение с применением определенной палитры. Оригинальный исходный код был утерян.

Изменение размера у меня работает, но у меня проблемы с применением палитры. Палитра сохраняется в виде файла JASC-PAL.

Это мой код, основанный на моем предположении, что вы просто загружаете файл, зацикливаете текущую палитру изображения и заменяете цвета.

private List<Color> ColourPalette = new List<Color>();
void LoadColourPalette()
    {
        using (StreamReader sr = new StreamReader("HH.PAL"))
        {
            // skip first 3 lines
            sr.ReadLine();
            sr.ReadLine();
            sr.ReadLine();

            while (sr.Peek() != -1)
            {
                var readLine = sr.ReadLine();

                if(string.IsNullOrWhiteSpace(readLine))
                    continue;

                var colourBytes = readLine.Split(' ');

                ColourPalette.Add(
                    Color.FromArgb(int.Parse(colourBytes[0]),
                        int.Parse(colourBytes[1]),
                        int.Parse(colourBytes[2])
                        ));

            }
        }
    }

Загрузка файла и применение палитры.

byte[] bytes;

using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
    using (var thumbnail = System.Drawing.Image.FromStream(fs))
    {
       var imagePalette = thumbnail.Palette;

       for (int i = 0; i < imagePalette.Entries.Length; i++)
           imagePalette.Entries[i] = ColourPalette[i];

       thumbnail.Palette = imagePalette;

       using (MemoryStream memory = new MemoryStream())
       {
           thumbnail.Save(memory, ImageFormat.Bmp);
           bytes = memory.ToArray();
       }
    }
}

using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite))
{
   fs.Write(bytes, 0, bytes.Length);
}

Когда я смотрю на изображение, оно выглядит совершенно неправильно. Больше похоже на то, что кто-то бросил пиксельную краску, лол. Я не могу загрузить изображение, но чтобы дать вам представление, на фото белая тарелка, стоящая на столе. Таблица должна быть как оттенков черного/темно-синего, но это оттенки светло-коричневого и немного зеленого.

Затем я взял изображение, к которому была применена палитра, извлек палитру и сохранил ее в файл в формате JASC-PAL. Когда я сравнил с файлом палитры, который мне дали, я увидел, что они идеально подходят друг другу.

Ясно, что есть что-то еще, но я не могу найти ничего по этому вопросу.

27.05.2021

  • Нет необходимости использовать FileStream. Класс Bitmap (фактическая реализация Image) имеет конструктор new Bitmap(path), который внутренне автоматически открывает поток, а для записи байтов вы можете просто использовать File.WriteAllBytes(path, byteArray). 16.07.2021

Ответы:


1

Я бы предположил, что исходное изображение имеет 24- или 32-битные цвета. т.е. каждый пиксель имеет байт для каждого из каналов красного, зеленого и синего. Изображения с палитрой так не работают. Вместо этого он имеет один байт, который используется в качестве индекса в таблице поиска.

Таким образом, чтобы найти правильный индекс, вам нужно будет выполнить поиск в палитре, чтобы найти цвет, который наиболее похож на фактический цвет пикселя. Я не думаю, что вы можете использовать 8-битное растровое изображение в качестве цели рендеринга для Graphics.FromImage, поэтому вам может потребоваться выполнить эту обработку самостоятельно, но могут быть доступные библиотеки, которые могут сделать это за вас.

Использование растровых изображений с палитрой уже не так распространено, поскольку любое уменьшение размера обычно достигается гораздо более эффективно с использованием современного алгоритма сжатия изображения.

Честно говоря, я не уверен, что произойдет, если вы просто установите палитру обычного 24-битного растрового изображения, но, по-видимому, результат неверен, поэтому он может быть неактуален.

27.05.2021
  • Нет, попытка изменить палитру на 24- или 32-битном изображении просто ничего не даст с этим кодом, поскольку длина палитры равна 0. Тот факт, что он испортился, доказывает, что его исходное изображение уже 8-битное. 16.07.2021

  • 2

    Применение палитры — это не то же самое, что замена палитры. Шаг, который вам не хватает, — это сопоставить фактические пиксели изображения с новыми цветами.

    Видите, пиксели на проиндексированных изображениях не содержат цветов. Они содержат ссылки на палитру. Итак, если у вас есть красный пиксель на изображении, это не пиксель со значением красного, это пиксель со значением от 0 до 255, относящимся к цвету с этим индексом в палитре. Итак, скажем, у него значение «23», тогда это означает, что он использует цвет с индексом 23 на палитре, и этот цвет красный.

    Но вы заменяете эти цвета в палитре. Таким образом, очевидно, что после вашей операции этот пиксель, относящийся к индексу 23, больше не будет красным, как раньше; он будет иметь любой цвет, на который вы заменили индекс 23. Поэтому все цвета на изображении будут перемешаны.

    По сути, вам нужно посмотреть на цвета, используемые вашими пикселями, найти их наиболее близкое соответствие в новой палитре, а затем сохранить индекс, по которому это самое близкое совпадение было найдено в новой палитре, в ваши пиксели.

    Способ сделать это немного сложнее, чем вы могли ожидать. Обычные функции .Net для рисования на изображениях не могут получить доступ к этим необработанным значениям индекса; они могут работать только с цветами. Так что вам придется пойти немного глубже.

    Поскольку это изображение с 8 битами на пиксель, каждое значение индекса цвета в изображении составляет один байт. Итак, первое, что вам нужно сделать, это получить эти байты. Это делается с помощью операции LockBits, которая дает вам доступ к базовой памяти изображения. Затем вы можете использовать Marshal.Copy для получения байтов без необходимости возиться с голыми указателями.

    Итак, давайте начнем с функции для получения этих байтов:

    /// <summary>
    /// Gets the raw bytes from an image.
    /// </summary>
    /// <param name="sourceImage">The image to get the bytes from.</param>
    /// <param name="stride">Stride of the retrieved image data.</param>
    /// <returns>The raw bytes of the image</returns>
    public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride)
    {
        BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
        stride = sourceData.Stride;
        Byte[] data = new Byte[stride * sourceImage.Height];
        Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
        sourceImage.UnlockBits(sourceData);
        return data;
    }
    

    Важное примечание: шаг — это количество байтов в каждой строке изображения. Поскольку это обычно округляется до кратного 4 байтам, жизненно важно помнить об этом, потому что очень часто это не будет соответствовать ширине изображения.

    Теперь мы получили байты, нам нужно пройтись по ним, посмотреть, какой у них цвет на текущей палитре, и найти наиболее близкое совпадение на новой палитре. Я уже описывал этот процесс в этом ответе, но я повторю его здесь для полноты картины.

    Обычный способ сопоставить изображение с определенными цветами — использовать пифагорейское расстояние между цветами в трехмерной среде с осями R, G и B. Обратите внимание, что вычисление расстояния по Пифагору на самом деле не требуется; нам не нужно знать фактическое расстояние, нам нужно только сравнить их, и это работает так же хорошо без этой довольно тяжелой операции с процессором.

    Обратите внимание, что если ваше изображение содержит более 256 пикселей (что, как я предполагаю, будет в большинстве изображений), гораздо проще просто найти наиболее близкое соответствие для каждого индекса в вашей палитре, а не для каждого пикселя в полном изображении, а затем применить это сопоставление с самими данными изображения. Затем вам нужно только один раз выполнить поиск цвета для каждого фактического цвета.

    /// <summary>
    /// Uses Pythagorean distance in 3D colour space to find the closest match to a given colour on
    /// a given colour palette, and returns the index on the palette at which that match was found.
    /// </summary>
    /// <param name="col">The colour to find the closest match to</param>
    /// <param name="colorPalette">The palette of available colours to match</param>
    /// <returns>The index on the palette of the colour that is the closest to the given colour.</returns>
    public static Int32 GetClosestPaletteIndexMatch(Color col, Color[] colorPalette)
    {
        Int32 colorMatch = 0;
        Int32 leastDistance = Int32.MaxValue;
        Int32 red = col.R;
        Int32 green = col.G;
        Int32 blue = col.B;
        for (Int32 i = 0; i < colorPalette.Length; ++i)
        {
            Color paletteColor = colorPalette[i];
            Int32 redDistance = paletteColor.R - red;
            Int32 greenDistance = paletteColor.G - green;
            Int32 blueDistance = paletteColor.B - blue;
            Int32 distance = (redDistance * redDistance) + (greenDistance * greenDistance) + (blueDistance * blueDistance);
            if (distance >= leastDistance)
                continue;
            colorMatch = i;
            leastDistance = distance;
            if (distance == 0)
                return i;
        }
        return colorMatch;
    }
    

    Найдя наилучшее совпадение цветов для всех цветов в старой палитре, вы можете просмотреть сами данные изображения и заменить все индексы на изображении правильным соответствием для новой палитры. Я поставлю этот код в конце, когда мы соберем все части.

    Как только это будет сделано, вы получите свой 8-битный массив изображений, готовый к преобразованию обратно в 8-битное изображение. Делается это той же операцией LockBits, только теперь в режиме записи. Наконец, вы применяете новую палитру к новому изображению почти так же, как это делает ваш собственный код. Вот функция BuildImage, которую я использую для всего этого:

    /// <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">Pixel format</param>
    /// <param name="palette">Color palette</param>
    /// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
    /// <returns>The new image</returns>
    public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
    {
        Bitmap newImage = new Bitmap(width, height, pixelFormat);
        BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
        // Get the actual minimum data width
        Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
        // Cache these to avoid unnecessary getter calls.
        Int32 targetStride = targetData.Stride;
        Int64 scan0 = targetData.Scan0.ToInt64();
        // Copy data per line into the target memory.
        for (Int32 y = 0; y < height; ++y)
            Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
        newImage.UnlockBits(targetData);
        // For indexed images, set the palette.
        if ((pixelFormat & PixelFormat.Indexed) != 0 && (palette != null || defaultColor.HasValue))
        {
            if (palette == null)
                palette = new Color[0];
            ColorPalette pal = newImage.Palette;
            Int32 minLen = Math.Min(pal.Entries.Length, palette.Length);
            for (Int32 i = 0; i < minLen; ++i)
                pal.Entries[i] = palette[i];
            // Fill in remainder with default if needed.
            if (pal.Entries.Length > palette.Length && defaultColor.HasValue)
                for (Int32 i = palette.Length; i < pal.Entries.Length; ++i)
                    pal.Entries[i] = defaultColor.Value;
            newImage.Palette = pal;
        }
        return newImage;
    }
    

    Итак, теперь, чтобы объединить все это:

    Int32 stride;
    Int32 width;
    Int32 height;
    Color[] curPalette;
    // Easier as array. Maybe you should do that right away;
    // it's always 256 entries anyway.
    Color[] newPalette = ColourPalette.ToArray();
    Byte[] imageData;
    // This 'using' block is kept small; extract the data and then dispose everything.
    using (Bitmap image = new Bitmap(filename))
    {
        if (image.PixelFormat != PixelFormat.Format8bppIndexed)
            return;
        width = image.Width;
        height = image.Height;
        curPalette = image.Palette.Entries;
        imageData = GetImageData(image, out stride);
    }
    // Make remap table to translate from old palette indices to new ones.
    Byte[] match = new Byte[curPalette.Length];
    for (Int32 i = 0; i < curPalette.Length; ++i)
        match[i] = (Byte)GetClosestPaletteIndexMatch(curPalette[i], newPalette);
    // Go over the actual pixels in the image data and replace the colours.
    Int32 currentLineOffset = 0;
    for (Int32 y = 0; y < height; ++y)
    {
        Int32 offset = currentLineOffset;
        for (Int32 x = 0; x < width; ++x)
        {
            // Replace index with index of the closest match found before for that colour.
            imageData[offset] = match[imageData[offset]];
            // Increase offset on this line
            offset++;
        }
        // Increase to start of next line
        currentLineOffset += stride;
    }
    using (Bitmap newbm = BuildImage(imageData, width, height, stride, PixelFormat.Format8bppIndexed, newPalette, Color.Black))
    {
        // Old bitmap is already disposed, so there is no issue saving to the same filename now
        newbm.Save(filename, ImageFormat.Bmp);
    }
    
    16.07.2021
    Новые материалы

    Я хотел выучить язык программирования MVC4, но не мог выучить его раньше, потому что это выглядит сложно…
    Просто начните и учитесь самостоятельно Я хотел выучить язык программирования MVC4, но не мог выучить его раньше, потому что он кажется мне сложным, и я бросил его. Это в основном инструмент..

    Лицензии с открытым исходным кодом: руководство для разработчиков и создателей
    В динамичном мире разработки программного обеспечения открытый исходный код стал мощной парадигмой, способствующей сотрудничеству, инновациям и прогрессу, движимому сообществом. В основе..

    Объяснение документов 02: BERT
    BERT представил двухступенчатую структуру обучения: предварительное обучение и тонкая настройка. Во время предварительного обучения модель обучается на неразмеченных данных с помощью..

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..


    Для любых предложений по сайту: [email protected]