Skip to content

Conversation

bablokb
Copy link

@bablokb bablokb commented May 7, 2025

Images that are already in mode='P' are remapped with a fix remapping that ignores the internal colormap of the image. The result is terrible.

This patch generates the remap-array automatically. Note that this is a patch only for the e673, the other drivers probably need this as well.

@Reid-Kornman
Copy link

@bablokb - is this able to be merged or reviewed?

@bablokb
Copy link
Author

bablokb commented Jun 6, 2025

Yes, this is tested and ready to merge.

One note though: this patch works as long as the image actually uses the palette of pure colors that are defined in the PR (RGB_TO_COLOR). If the internal palette is different, it won't work. The solution would be:

  • extract the palette used in the image
  • calculate chroma for each palette-color
  • create RGB_TO_COLOR dynamically based on chroma values

I did not feel like it is worth the extra work, since pre-dithered images hopefully use pure colors. Best thing is to document this though.

@Gadgetoid
Copy link
Member

Sorry for the delay getting back here. We've had other problems with the image remapping code (where P mode images have more than six colours) and have attempted a more thorough rewrite - most of it was very, very old code I wrote back when I had much less idea what I was doing - which should fix.. I would tentatively say... all cases.

We pretty much just let PIL do the work, since that's what it's supposed to be good for 😆 and try to avoid special casing image formats (except for the one exception where a P mode image has no palette at all, and thus no colour information.)

If you have a moment, I'd appreciate knowing if the changes work for you: #228

@bablokb
Copy link
Author

bablokb commented Jun 16, 2025

I will test it, but just by looking at the code I am not sure if this is correct:

image = image.convert("RGB").quantize(6, palette=palette_image)

This will take the original P-mode image (already quantized and dithered), convert it to RGB and re-quantize and re-dither it again?! But maybe my understanding of the code is just wrong.

The main reason (at least for me) to use P-mode images is that I have the control over dithering. I noticed that the current code (i.e. from main) tends to blow-out the highlights (on the plus side it does not create as many artifacts).

@Gadgetoid
Copy link
Member

This will take the original P-mode image (already quantized and dithered), convert it to RGB and re-quantize and re-dither it again?! But maybe my understanding of the code is just wrong.

It will. In theory this should be a no-op since the conversion to RGB will preserve the colour information, and the pure colours should not be dithered. In practise I expect this will be complicated by the palette saturation code.

Frustratingly we'll probably still have to edge case this. My goal is to lean on PIL as much as possible and avoid doing messy manual remapping, since I hope this function can be ported to the other drivers:

    def set_image(self, image, saturation=0.5):
        """Copy an image to the display.

        :param image: PIL image to copy, must be 800x480
        :param saturation: Saturation for quantization palette - higher value results in a more saturated image

        """
        if not image.size == (self.width, self.height):
            raise ValueError(f"Image must be ({self.width}x{self.height}) pixels!")

        dither = Image.Dither.FLOYDSTEINBERG

        # Image size doesn't matter since it's just the palette we're using
        palette_image = Image.new("P", (1, 1))

        if image.mode == "P":
            # Create a pure colour palette from DESATURATED_PALETTE
            palette = numpy.array(DESATURATED_PALETTE, dtype=numpy.uint8).flatten().tobytes()

            # Assume that palette mode images with an unset palette use the
            # default colour order and "DESATURATED_PALETTE" pure colours
            if not image.palette.colors:
                image.putpalette(palette)

            # Assume that palette mode images with exactly six colours use
            # all the correct colours, but not exactly in the right order.
            if len(image.palette.colors) == 6:
                dither = Image.Dither.NONE
                palette_image.putpalette(palette)
        else:
            # All other image should be quantized and dithered
            palette = self._palette_blend(saturation)
            palette_image.putpalette(palette)

        image = image.convert("RGB").quantize(6, palette=palette_image, dither=dither)

        # Remap our sequential palette colours to display native (missing colour 4)
        remap = numpy.array([0, 1, 2, 3, 5, 6])
        self.buf = remap[numpy.array(image, dtype=numpy.uint8).reshape((self.rows, self.cols))]

The conversion to RGB, and quantisation with a fixed palette and no dither should be equivalent to manually remapping the colours (though the Quantize method might have something to say about this).

Things would be simpler if we excluded/ignored saturation and assumed anyone wanting to achieve a particular look would pre-dither their images.

And if we're exposing stuff like saturation we should probably allow users to override the dither option, for a posterize effects on >6 colour "P" or "RGB" images.

@bablokb
Copy link
Author

bablokb commented Jun 16, 2025

So the set_image method above is an updated version to the one in #228?

BTW this all came up because I ported the e673 to CircuitPython. CP does no dithering (or better: no palette dithering), so I needed pre-dithered images anyhow. But with CP the images on the spectra6 look identical to what I see on my laptop/PC. Color-mapping within CP is purely based on chroma, so even if I don't use a specific palette for quantization, I will get sensible results as long as the palette that I use will have something that is red or near red (and so on for other colors).

The majority of users will just throw a RGB-image on the screen and will be happy. Anybody using pre-dithered images will certainly expect to see them "as is".

@Gadgetoid
Copy link
Member

The majority of users will just throw a RGB-image on the screen and will be happy. Anybody using pre-dithered images will certainly expect to see them "as is".

I think our comic example is the exception that proves the rule here, images can randomly be palette-mode and there's no real way to tell if they're dithered or not. (in the case of the comic example, they'd never be native to our display anyway)

I've updated the linked branch to detect 6-colour images and assume they are as-is, pre-dithered images (with the exception that their colours will be remapped correctly).

A lot of the complexity comes from efforts to make the palette colours somewhat true to the screen, or at least a reasonable balance of realistic screen colours and the intended pure colours. Basing this on crudely mixed RGB values was probably a bad way to start.

Thanks for your feedback/suggestions here, a lot of Inky is just my best attempt at making stuff work... circa 5-10 years ago. It doesn't always pan out 😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants