Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quality loss when quantizing an image that already has (fewer than) 256 colors #2861

Closed
4 tasks done
pwitvoet opened this issue Jan 14, 2025 · 0 comments
Closed
4 tasks done

Comments

@pwitvoet
Copy link

Prerequisites

  • I have written a descriptive issue title
  • I have verified that I am running the latest version of ImageSharp
  • I have verified if the problem exist in both DEBUG and RELEASE mode
  • I have searched open and closed issues to ensure it has not already been reported

ImageSharp version

3.1.6

Other ImageSharp packages and versions

n/a

Environment (Operating system, version and so on)

Windows 10

.NET Framework version

.NET 8

Description

I've been saving 256-color images as 8-bit indexed pngs, gifs and bmps, using a PaletteQuantizer. In theory, they should not be negatively affected by quantization, because the images already contain 256 or fewer colors. In practice, I'm occasionally seeing quality loss, with output images not making use of the full palette.

This appears to be caused by the EuclideanPixelMap (specifically its ColorDistanceCache) that PaletteQuantizer is using. The color distance cache is lopping off the lowest 3 bits of the RGB values, which is quite troublesome for gradients. From the discussion in #1350 I understand that this is a memory usage optimization, but in my case a simple dictionary would work just fine, because all colors are already present in the palette anyway. If a trade-off has to be made, perhaps it should be configurable?

I also know that it's possible to create a custom IQuantizer implementation, but there are two problems with that:

  • IndexedImageFrame<TPixel> doesn't have a public constructor. It's possible to create an instance using reflection, but that's brittle.
  • For images with multiple frames, PngEncoderCore and GifEncoderCore only use the given quantizer for the first frame. For subsequent frames, they use a PaletteQuantizer, which re-introduces the quality loss problem.

What I actually want to achieve is a bit more strict: I want to preserve the exact index data of the original image, even when a palette contains duplicate colors. I don't expect a standard quantizer to handle such a niche scenario, but it would be very useful if encoders would use the given quantizer for all frames, not just the first frame.

Steps to Reproduce

// Create a grayscale palette (or any other palette with colors that are very close to each other):
var palette = Enumerable.Range(0, 256).Select(i => new Rgba32((byte)i, (byte)i, (byte)i)).ToArray();

// Create an image with a smooth black-white gradient:
using var image = new Image<Rgba32>(254, 4);
for (int y = 0; y < image.Height; y++)
    for (int x = 0; x < image.Width; x++)
        image[x, y] = palette[x];

// Correct result, for comparison:
image.Save(@"C:\Documents\temp\test_images\gradient.png");

// Banded gradient, uses only 32 colors instead of the full palette:
image.Save(@"C:\Documents\temp\test_images\gradient_8bit.png", new PngEncoder {
    ColorType = PngColorType.Palette,
    BitDepth = PngBitDepth.Bit8,
    Quantizer = new PaletteQuantizer(palette.Select(Color.FromPixel).ToArray())
});

// The same problem occurs when saving as an 8-bit bmp or gif file.

Images

Expected output:
gradient

Actual output (zoom in to see the banding):
gradient_8bit

@SixLabors SixLabors locked and limited conversation to collaborators Jan 15, 2025
@JimBobSquarePants JimBobSquarePants converted this issue into discussion #2862 Jan 15, 2025

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

1 participant