From: Melissa LeBlanc-Williams Date: Wed, 20 Sep 2023 20:28:30 +0000 (-0700) Subject: Finish adding code to ColorConverter X-Git-Tag: 1.0.0^2~22 X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/commitdiff_plain/255ac2d73d09bc445dd3522b3b30f861552ccbe4 Finish adding code to ColorConverter --- diff --git a/.pylintrc b/.pylintrc index 40208c3..f945e92 100644 --- a/.pylintrc +++ b/.pylintrc @@ -396,4 +396,4 @@ min-public-methods=1 # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/displayio/_colorconverter.py b/displayio/_colorconverter.py index 01c70d6..c3c5481 100644 --- a/displayio/_colorconverter.py +++ b/displayio/_colorconverter.py @@ -21,6 +21,7 @@ __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" from ._colorspace import Colorspace +from ._structs import ColorspaceStruct class ColorConverter: @@ -36,62 +37,129 @@ class ColorConverter: :param bool dither: Adds random noise to dither the output image """ self._dither = dither - self._depth = 16 self._transparent_color = None - self._rgba = False + self._rgba = False # Todo set Output colorspace depth to 32 maybe? self._input_colorspace = input_colorspace + self._output_colorspace = ColorspaceStruct(16) + self._cached_colorspace = None + self._cached_input_pixel = None + self._cached_output_color = None - def _compute_rgb565(self, color: int): - self._depth = 16 - return (color[0] & 0xF8) << 8 | (color[1] & 0xFC) << 3 | color[2] >> 3 + @staticmethod + def _clamp(value, min_value, max_value): + return max(min(max_value, value), min_value) + + @staticmethod + def _bswap16(value): + # ABCD -> 00DC + return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8 + + def _dither_noise_1(self, noise): + noise = (noise >> 13) ^ noise + more_noise = ( + noise * (noise * noise * 60493 + 19990303) + 1376312589 + ) & 0x7FFFFFFF + return self._clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF) + + def _dither_noise_2(self, x, y): + return self._dither_noise_1(x + y * 0xFFFF) + + @staticmethod + def _compute_rgb565(color_rgb888: int): + red5 = color_rgb888 >> 19 + grn6 = (color_rgb888 >> 10) & 0x3F + blu5 = (color_rgb888 >> 3) & 0x1F + return red5 << 11 | grn6 << 5 | blu5 + + @staticmethod + def _compute_rgb332(color_rgb888: int): + red3 = color_rgb888 >> 21 + grn2 = (color_rgb888 >> 13) & 0x7 + blu2 = (color_rgb888 >> 6) & 0x3 + return red3 << 5 | grn2 << 3 | blu2 @staticmethod - def _compute_luma(color: int): - red = color >> 16 - green = (color >> 8) & 0xFF - blue = color & 0xFF - return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255 + def _compute_rgbd(color_rgb888: int): + red1 = (color_rgb888 >> 23) & 0x1 + grn1 = (color_rgb888 >> 15) & 0x1 + blu1 = (color_rgb888 >> 7) & 0x1 + return red1 << 3 | grn1 << 2 | blu1 << 1 # | dummy @staticmethod - def _compute_chroma(color: int): - red = color >> 16 - green = (color >> 8) & 0xFF - blue = color & 0xFF - return max(red, green, blue) - min(red, green, blue) - - def _compute_hue(self, color: int): - red = color >> 16 - green = (color >> 8) & 0xFF - blue = color & 0xFF - max_color = max(red, green, blue) - chroma = self._compute_chroma(color) + def _compute_luma(color_rgb888: int): + red8 = color_rgb888 >> 16 + grn8 = (color_rgb888 >> 8) & 0xFF + blu8 = color_rgb888 & 0xFF + return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255 + + @staticmethod + def _compute_chroma(color_rgb888: int): + red8 = color_rgb888 >> 16 + grn8 = (color_rgb888 >> 8) & 0xFF + blu8 = color_rgb888 & 0xFF + return max(red8, grn8, blu8) - min(red8, grn8, blu8) + + @staticmethod + def _compute_hue(color_rgb888: int): + red8 = color_rgb888 >> 16 + grn8 = (color_rgb888 >> 8) & 0xFF + blu8 = color_rgb888 & 0xFF + max_color = max(red8, grn8, blu8) + chroma = max_color - min(red8, grn8, blu8) if chroma == 0: return 0 hue = 0 - if max_color == red: - hue = (((green - blue) * 40) / chroma) % 240 - elif max_color == green: - hue = (((blue - red) + (2 * chroma)) * 40) / chroma - elif max_color == blue: - hue = (((red - green) + (4 * chroma)) * 40) / chroma + if max_color == red8: + hue = (((grn8 - blu8) * 40) / chroma) % 240 + elif max_color == grn8: + hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma + elif max_color == blu8: + hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma if hue < 0: hue += 240 return hue - @staticmethod - def _dither_noise_1(noise): - noise = (noise >> 13) ^ noise - more_noise = ( - noise * (noise * noise * 60493 + 19990303) + 1376312589 - ) & 0x7FFFFFFF - return (more_noise / (1073741824.0 * 2)) * 255 - - def _dither_noise_2(self, x, y): - return self._dither_noise_1(x + y * 0xFFFF) + def _compute_sevencolor(self, color_rgb888: int): + # pylint: disable=too-many-return-statements + chroma = self._compute_chroma(color_rgb888) + if chroma >= 64: + hue = self._compute_hue(color_rgb888) + # Red 0 + if hue < 10: + return 0x4 + # Orange 21 + if hue < 21 + 10: + return 0x6 + # Yellow 42 + if hue < 42 + 21: + return 0x5 + # Green 85 + if hue < 85 + 42: + return 0x2 + # Blue 170 + if hue < 170 + 42: + return 0x3 + # The rest is red to 255 + return 0x4 + luma = self._compute_luma(color_rgb888) + if luma >= 128: + return 0x1 # White + return 0x0 # Black - def _compute_tricolor(self): - pass + @staticmethod + def _compute_tricolor( + colorspace: ColorspaceStruct, pixel_hue: int, color: int + ) -> int: + hue_diff = colorspace.tricolor_hue - pixel_hue + if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220: + if colorspace.grayscale: + color = 0 + else: + color = 1 + elif not colorspace.grayscale: + color = 0 + return color def convert(self, color: int) -> int: "Converts the given rgb888 color to RGB565" @@ -105,11 +173,148 @@ class ColorConverter: else: raise ValueError("Color must be an integer or 3 or 4 value tuple") - if self._dither: - return color # To Do: return a dithered color - if self._rgba: - return color - return self._compute_rgb565(color) + input_pixel = { + "pixel": color, + "x": 0, + "y": 0, + "tile": 0, + "tile_x": 0, + "tile_y": 0, + } + + output_pixel = {"pixel": 0, "opaque": False} + + if input_pixel["pixel"] == self._transparent_color: + return output_pixel["pixel"] + + if ( + not self._dither + and self._cached_colorspace == self._output_colorspace + and self._cached_input_pixel == input_pixel["pixel"] + ): + return self._cached_output_color + + rgb888_pixel = input_pixel + rgb888_pixel["pixel"] = self._convert_pixel( + self._input_colorspace, input_pixel["pixel"] + ) + self._convert_color( + self._output_colorspace, self._dither, rgb888_pixel, output_pixel + ) + + if not self._dither: + self._cached_colorspace = self._output_colorspace + self._cached_input_pixel = input_pixel["pixel"] + self._cached_output_color = output_pixel["pixel"] + + return output_pixel["pixel"] + + def _convert_pixel(self, colorspace: Colorspace, pixel: int) -> int: + pixel = self._clamp(pixel, 0, 0xFFFFFFFF) + if colorspace in ( + Colorspace.RGB565_SWAPPED, + Colorspace.RGB555_SWAPPED, + Colorspace.BGR565_SWAPPED, + Colorspace.BGR555_SWAPPED, + ): + pixel = self._bswap16(pixel) + if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED): + red8 = (pixel >> 11) << 3 + grn8 = ((pixel >> 5) << 2) & 0xFF + blu8 = (pixel << 3) & 0xFF + return (red8 << 16) | (grn8 << 8) | blu8 + if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED): + red8 = (pixel >> 10) << 3 + grn8 = ((pixel >> 5) << 3) & 0xFF + blu8 = (pixel << 3) & 0xFF + return (red8 << 16) | (grn8 << 8) | blu8 + if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED): + blu8 = (pixel >> 11) << 3 + grn8 = ((pixel >> 5) << 2) & 0xFF + red8 = (pixel << 3) & 0xFF + return (red8 << 16) | (grn8 << 8) | blu8 + if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED): + blu8 = (pixel >> 10) << 3 + grn8 = ((pixel >> 5) << 3) & 0xFF + red8 = (pixel << 3) & 0xFF + return (red8 << 16) | (grn8 << 8) | blu8 + if colorspace == Colorspace.L8: + return (pixel & 0xFF) & 0x01010101 + return pixel + + def _convert_color( + self, + colorspace: ColorspaceStruct, + dither: bool, + input_pixel: dict, + output_color: dict, + ) -> None: + # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements + pixel = input_pixel["pixel"] + if dither: + rand_red = self._dither_noise_2(input_pixel["x"], input_pixel["y"]) + rand_grn = self._dither_noise_2(input_pixel["x"] + 33, input_pixel["y"]) + rand_blu = self._dither_noise_2(input_pixel["x"], input_pixel["y"] + 33) + + red8 = pixel >> 16 + grn8 = (pixel >> 8) & 0xFF + blu8 = pixel & 0xFF + + if colorspace.depth == 16: + blu8 = min(255, blu8 + (rand_blu & 0x07)) + red8 = min(255, red8 + (rand_red & 0x07)) + grn8 = min(255, grn8 + (rand_grn & 0x03)) + else: + bitmask = 0xFF >> colorspace.depth + blu8 = min(255, blu8 + (rand_blu & bitmask)) + red8 = min(255, red8 + (rand_red & bitmask)) + grn8 = min(255, grn8 + (rand_grn & bitmask)) + pixel = (red8 << 16) | (grn8 << 8) | blu8 + + if colorspace.depth == 16: + packed = self._compute_rgb565(pixel) + if colorspace.reverse_bytes_in_word: + packed = self._bswap16(packed) + output_color["pixel"] = packed + output_color["opaque"] = True + return + if colorspace.tricolor: + output_color["pixel"] = self._compute_luma(pixel) >> (8 - colorspace.depth) + if self._compute_chroma(pixel) <= 16: + if not colorspace.grayscale: + output_color["pixel"] = 0 + output_color["opaque"] = True + return + pixel_hue = self._compute_hue(pixel) + output_color["pixel"] = self._compute_tricolor( + colorspace, pixel_hue, output_color["pixel"] + ) + return + if colorspace.grayscale and colorspace.depth <= 8: + bitmask = (1 << colorspace.depth) - 1 + output_color["pixel"] = ( + self._compute_luma(pixel) >> colorspace.grayscale_bit + ) & bitmask + output_color["opaque"] = True + return + if colorspace.depth == 32: + output_color["pixel"] = pixel + output_color["opaque"] = True + return + if colorspace.depth == 8 and colorspace.grayscale: + packed = self._compute_rgb332(pixel) + output_color["pixel"] = packed + output_color["opaque"] = True + return + if colorspace.depth == 4: + if colorspace.sevencolor: + packed = self._compute_sevencolor(pixel) + else: + packed = self._compute_rgbd(pixel) + output_color["pixel"] = packed + output_color["opaque"] = True + return + output_color["opaque"] = False def make_transparent(self, color: int) -> None: """Set the transparent color or index for the ColorConverter. This will @@ -117,8 +322,7 @@ class ColorConverter: """ self._transparent_color = color - def make_opaque(self, color: int) -> None: - # pylint: disable=unused-argument + def make_opaque(self, _color: int) -> None: """Make the ColorConverter be opaque and have no transparent pixels.""" self._transparent_color = None diff --git a/displayio/_i2cdisplay.py b/displayio/_i2cdisplay.py index 2647659..42f85ec 100644 --- a/displayio/_i2cdisplay.py +++ b/displayio/_i2cdisplay.py @@ -89,10 +89,9 @@ class I2CDisplay: def _send( self, data_type: int, - chip_select: int, + _chip_select: int, # Chip select behavior data: circuitpython_typing.ReadableBuffer, ): - # pylint: disable=unused-argument if data_type == DISPLAY_COMMAND: n = len(data) if n > 0: @@ -101,12 +100,12 @@ class I2CDisplay: command_bytes[2 * i] = 0x80 command_bytes[2 * i + 1] = data[i] - self._i2c.writeto(self._dev_addr, buffer=command_bytes, stop=True) + self._i2c.writeto(self._dev_addr, buffer=command_bytes) else: data_bytes = bytearray(len(data) + 1) data_bytes[0] = 0x40 data_bytes[1:] = data - self._i2c.writeto(self._dev_addr, buffer=data_bytes, stop=True) + self._i2c.writeto(self._dev_addr, buffer=data_bytes) def _end_transaction(self) -> None: """Release the bus after sending data.""" diff --git a/displayio/_structs.py b/displayio/_structs.py index 955b9b4..9a2017e 100644 --- a/displayio/_structs.py +++ b/displayio/_structs.py @@ -49,7 +49,7 @@ class TransformStruct: @dataclass class ColorspaceStruct: - # pylint: disable=invalid-name + # pylint: disable=invalid-name, too-many-instance-attributes """Colorspace Struct Dataclass""" depth: int bytes_per_cell: int = 0 @@ -58,6 +58,7 @@ class ColorspaceStruct: grayscale_bit: int = 0 grayscale: bool = False tricolor: bool = False + sevencolor: bool = False # Acep e-ink screens. pixels_in_byte_share_row: bool = False reverse_pixels_in_byte: bool = False reverse_bytes_in_word: bool = False