From 3d0680eddf8bc94e2e57290aef978191162832d2 Mon Sep 17 00:00:00 2001 From: mateuszz0000 Date: Thu, 30 Apr 2020 11:54:20 +0200 Subject: [PATCH] Added Burkes dithering algorithm. (#1916) * Added Burkes dithering algorithm * Added unit tests for burkes algorithm * Fix burkes algorithm * Added some additional information * Fixed CI tests * Update digital_image_processing/dithering/burkes.py Co-Authored-By: Christian Clauss * Update digital_image_processing/dithering/burkes.py Co-Authored-By: Christian Clauss * Update digital_image_processing/dithering/burkes.py Co-Authored-By: Christian Clauss * Propogate the += and add a doctest * Fix doctest * @staticmethod --> @ classmethod to ease testing * def test_burkes(file_path): * Fix for mypy checks * Fix variable order in get_greyscale * Fix get_greyscale method * Fix get_greyscale method * 3.753 Co-authored-by: Christian Clauss --- .../dithering/__init__.py | 0 digital_image_processing/dithering/burkes.py | 87 +++++++++++++++++++ .../test_digital_image_processing.py | 8 ++ 3 files changed, 95 insertions(+) create mode 100644 digital_image_processing/dithering/__init__.py create mode 100644 digital_image_processing/dithering/burkes.py diff --git a/digital_image_processing/dithering/__init__.py b/digital_image_processing/dithering/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/digital_image_processing/dithering/burkes.py b/digital_image_processing/dithering/burkes.py new file mode 100644 index 000000000..54a243bd2 --- /dev/null +++ b/digital_image_processing/dithering/burkes.py @@ -0,0 +1,87 @@ +""" +Implementation Burke's algorithm (dithering) +""" +from cv2 import destroyAllWindows, imread, imshow, waitKey +import numpy as np + + +class Burkes: + """ + Burke's algorithm is using for converting grayscale image to black and white version + Source: Source: https://en.wikipedia.org/wiki/Dither + + Note: + * Best results are given with threshold= ~1/2 * max greyscale value. + * This implementation get RGB image and converts it to greyscale in runtime. + """ + + def __init__(self, input_img, threshold: int): + self.min_threshold = 0 + # max greyscale value for #FFFFFF + self.max_threshold = int(self.get_greyscale(255, 255, 255)) + + if not self.min_threshold < threshold < self.max_threshold: + raise ValueError(f"Factor value should be from 0 to {self.max_threshold}") + + self.input_img = input_img + self.threshold = threshold + self.width, self.height = self.input_img.shape[1], self.input_img.shape[0] + + # error table size (+4 columns and +1 row) greater than input image because of + # lack of if statements + self.error_table = [ + [0 for _ in range(self.height + 4)] for __ in range(self.width + 1) + ] + self.output_img = np.ones((self.width, self.height, 3), np.uint8) * 255 + + @classmethod + def get_greyscale(cls, blue: int, green: int, red: int) -> float: + """ + >>> Burkes.get_greyscale(3, 4, 5) + 3.753 + """ + return 0.114 * blue + 0.587 * green + 0.2126 * red + + def process(self) -> None: + for y in range(self.height): + for x in range(self.width): + greyscale = int(self.get_greyscale(*self.input_img[y][x])) + if self.threshold > greyscale + self.error_table[y][x]: + self.output_img[y][x] = (0, 0, 0) + current_error = greyscale + self.error_table[x][y] + else: + self.output_img[y][x] = (255, 255, 255) + current_error = greyscale + self.error_table[x][y] - 255 + """ + Burkes error propagation (`*` is current pixel): + + * 8/32 4/32 + 2/32 4/32 8/32 4/32 2/32 + """ + self.error_table[y][x + 1] += int(8 / 32 * current_error) + self.error_table[y][x + 2] += int(4 / 32 * current_error) + self.error_table[y + 1][x] += int(8 / 32 * current_error) + self.error_table[y + 1][x + 1] += int(4 / 32 * current_error) + self.error_table[y + 1][x + 2] += int(2 / 32 * current_error) + self.error_table[y + 1][x - 1] += int(4 / 32 * current_error) + self.error_table[y + 1][x - 2] += int(2 / 32 * current_error) + + +if __name__ == "__main__": + # create Burke's instances with original images in greyscale + burkes_instances = [ + Burkes(imread("image_data/lena.jpg", 1), threshold) + for threshold in (1, 126, 130, 140) + ] + + for burkes in burkes_instances: + burkes.process() + + for burkes in burkes_instances: + imshow( + f"Original image with dithering threshold: {burkes.threshold}", + burkes.output_img, + ) + + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py index 5c6127337..1915f17e9 100644 --- a/digital_image_processing/test_digital_image_processing.py +++ b/digital_image_processing/test_digital_image_processing.py @@ -10,6 +10,7 @@ import digital_image_processing.filters.convolve as conv import digital_image_processing.change_contrast as cc import digital_image_processing.convert_to_negative as cn import digital_image_processing.sepia as sp +import digital_image_processing.dithering.burkes as bs from cv2 import imread, cvtColor, COLOR_BGR2GRAY from numpy import array, uint8 from PIL import Image @@ -17,6 +18,7 @@ from PIL import Image img = imread(r"digital_image_processing/image_data/lena_small.jpg") gray = cvtColor(img, COLOR_BGR2GRAY) + # Test: convert_to_negative() def test_convert_to_negative(): negative_img = cn.convert_to_negative(img) @@ -74,3 +76,9 @@ def test_sobel_filter(): def test_sepia(): sepia = sp.make_sepia(img, 20) assert sepia.all() + + +def test_burkes(file_path: str="digital_image_processing/image_data/lena_small.jpg"): + burkes = bs.Burkes(imread(file_path, 1), 120) + burkes.process() + assert burkes.output_img.any()