From ce837f1d7e390ff6a92b634f4d41e869ae0ec089 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Wed, 5 Jun 2019 21:19:17 -0700 Subject: [PATCH] Added a threshold and all mode. Reworked the application into a class and reused relevant code for each dithering method. --- .gitignore | 1 + README.md | 17 ++- lid | 317 ++++++++++++++++++++++++++++------------------------- 3 files changed, 182 insertions(+), 153 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..362e53b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +oneliner \ No newline at end of file diff --git a/README.md b/README.md index 56a77d5..fdf42c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Lid _lo-fi image dithering_ -Lid performs fairly basic 1 bit image dithering. It converts an input image to grayscale and then dithers that image. It then optimizes the output and saves the file. The result is a reduced filesize, simple image, great for small apge sizes. +Lid performs fairly basic 1 bit image dithering. It converts an input image to grayscale and then dithers that image. It then optimizes the output and saves the file. The result is a reduced filesize, simple image, great for small page sizes. ## Dependencies @@ -14,8 +14,9 @@ lid [option]... [source path] [output name] Options:
-f = output format, defaults to the recommended setting: png
--m = mode, available: o4, o9, e. default: o4. --q = quality, defaults to 90, only affects jpeg/jpg +-m = mode, available: o4, o9, e. default: o4.
+-q = quality (0 - 100), defaults to 90, only affects jpeg/jpg
+-t = threshold (0 - 255), defaults to image average level. only affects dither mode 't' source path = a path to an image file you want to dither
output name = the name of the file you would like to output, do not include file extension @@ -24,14 +25,20 @@ Lid works best outputting png files from any input. You will get the smallest fi ### Modes +#### t +This is the lowest quality mode. It produces the smallest file, but at the cost of most detail. + #### o4 -This is the lowest quality mode. It will produce the smallest file size. It is a four level ordered dither. +This mode is a noticable difference from the stark two tone appearance of threashold mode. The appearance 'gray' areas make for a slightly more photographic image. #### o9 -This mode produces medium quality dithering, it is still a very noticeable effect but not as harsh as o4. It is a none level ordered dither. +This mode produces medium quality dithering, it is still a very noticeable effect but not as harsh as o4. It is a nine level ordered dither. #### e This is the highest quality mode. It uses an error diffusion dither. The file size will be the largest of the bunch, but should still be a good reduction from a regular full color photograph. +#### a +The a, or all, mode is a quick flag to say that you want an image put out in all of the modes. The mode flag value for each will be appended to the filename. + ## License Public domain. Use it, improve it, share it! diff --git a/lid b/lid index f0112d4..f474fb7 100755 --- a/lid +++ b/lid @@ -8,156 +8,148 @@ from PIL import Image import sys import os, os.path -# Default dithering: 4 level ordered dither -def ordered_dither_4(r): - im = Image.open(r["path"]).convert("L") - width, height = im.size - new = create_image(width, height) - newpixels = new.load() +class Dither: + def __init__(self, data): + self.source_image = Image.open(data["path"]).convert("L") + self.width, self.height = self.source_image.size + self.pixel_count = self.width * self.height + self.new = self.create_image() + self.new_pixels = self.new.load() + self.output_format = data["-f"] + self.output_name = data["out"] + self.output_quality = data["-q"] + self.threshold = data["-t"] - dots = [ - [64, 128], - [192, 0] - ] - - total_px_count = width * height - counter = 1 - last_percent_val = 0 - last_hash_val = 0 - print("Dither method: ordered (4 levels)\033[?25l") - for row in range(0, height): - for col in range(0, width): - dotrow = 1 if row % 2 else 0 - dotcol = 1 if col % 2 else 0 - impx = get_pixel(im, col, row) - newpixels[col, row] = int(impx > dots[dotrow][dotcol]) - counter += 1 - new_percent_val = round(counter / total_px_count * 100) - new_hash_val = round(counter / total_px_count * 10) - if new_percent_val != last_percent_val or new_hash_val != last_hash_val: - print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="") - last_percent_val = new_percent_val - last_hash_val = new_hash_val - - new.save("{}.{}".format(r["out"], r["-f"]), r["-f"].upper(), optimize=True, quality=r["-q"]) - print("\033[20C\nFinished! ~ {}/{}.{}\033[?25h".format(os.getcwd(), r["out"], r["-f"].lower())) - -# 9 level ordered dither -def ordered_dither_9(r): - im = Image.open(r["path"]).convert("L") - width, height = im.size - new = create_image(width, height) - newpixels = new.load() - - dots = [ - [0, 196, 84], - [168, 140, 56], - [112, 28, 224] - ] - - total_px_count = width * height - counter = 1 - last_percent_val = 0 - last_hash_val = 0 - print("Dither method: ordered (9 levels)\033[?25l") - for row in range(0, height): - for col in range(0, width): - if not row % 3: - dotrow = 2 - elif not row % 2: - dotrow = 1 - else: - dotrow = 0 - - if not col % 3: - dotcol = 2 - elif not col % 2: - dotcol = 1 - else: - dotcol = 0 - impx = get_pixel(im, col, row) - newpixels[col, row] = int(impx > dots[dotrow][dotcol]) - counter += 1 - new_percent_val = round(counter / total_px_count * 100) - new_hash_val = round(counter / total_px_count * 10) - if new_percent_val != last_percent_val or new_hash_val != last_hash_val: - print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="") - last_percent_val = new_percent_val - last_hash_val = new_hash_val - - new.save("{}.{}".format(r["out"], r["-f"]), r["-f"].upper(), optimize=True, quality=r["-q"]) - print("\033[20C\nFinished! ~ {}/{}.{}\033[?25h".format(os.getcwd(), r["out"], r["-f"].lower())) - -# Alternate dither mode -def error_dither(r): - im = Image.open(r["path"]).convert("L") - width, height = im.size - new = create_image(width, height) - newpixels = new.load() - i = im.load() - - total_px_count = width * height - counter = 1 - last_percent_val = 0 - last_hash_val = 0 - print("Dither method: error diffusion\033[?25l") - for row in range(0, height): - for col in range(0, width): - newpixels[col, row] = update_error(im, i, row, col) - counter += 1 - new_percent_val = round(counter / total_px_count * 100) - new_hash_val = round(counter / total_px_count * 10) - if new_percent_val != last_percent_val or new_hash_val != last_hash_val: - print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="") - last_percent_val = new_percent_val - last_hash_val = new_hash_val - - new.save("{}.{}".format(r["out"], r["-f"]), r["-f"].upper(), optimize=True, quality=r["-q"]) - print("\033[20C\nFinished! ~ {}/{}.{}\033[?25h".format(os.getcwd(), r["out"], r["-f"].lower())) + #used for printing progress + self.counter = 1 + self.percent_val = 0 + self.hash_val = 0 -def update_error(im, i, r, c): - w, h = im.size - current = get_pixel(im, c, r) - if current > 128: - res = 1 - diff = -(255 - current) - else: - res = 0 - diff = abs(0 - current) - mov = [[0, 1, 0.4375],[1, 1, 0.0625],[1, 0, 0.3125],[1, -1, 0.1875]] - for x in mov: - if r + x[0] >= h or c + x[1] >= w or c + x[1] <= 0: - continue - p = get_pixel(im, c + x[1], r + x[0]) - p = round(diff * x[2] + p) - if p < 0: - p = 0 - elif p > 255: - p = 255 - i[c + x[1], r + x[0]] = p - return res + def save_file(self): + output_filename = "{}.{}".format(self.output_name, self.output_format.lower()) + self.new.save(output_filename, self.output_format.upper(), optimize=True, quality=self.output_quality) + print("\033[20C\nFinished! ~ {}/{}\033[?25h".format(os.getcwd(), output_filename)) - -def print_spinner(n): - print("\r|{:<10}|".format("#" * n), end="") - -# Create a new image with the given size -def create_image(i, j): - image = Image.new("1", (i, j)) - return image + # Default dithering + def ordered_dither_4(self): + dots = [[64, 128],[192, 0]] + print("Dither method: ordered (4 levels)\033[?25l") + for row in range(0, self.height): + for col in range(0, self.width): + dotrow = 1 if row % 2 else 0 + dotcol = 1 if col % 2 else 0 + px = self.get_pixel(col, row) + self.new_pixels[col, row] = int(px > dots[dotrow][dotcol]) + self.update_progress() + self.save_file() -# Get the pixel from the given image -def get_pixel(image, i, j): - width, height = image.size - if i > width or j > height: - return None + def ordered_dither_9(self): + dots = [ + [0, 196, 84], + [168, 140, 56], + [112, 28, 224] + ] + print("Dither method: ordered (9 levels)\033[?25l") + for row in range(0, self.height): + for col in range(0, self.width): + if not row % 3: + dotrow = 2 + elif not row % 2: + dotrow = 1 + else: + dotrow = 0 - # Get Pixel - pixel = image.getpixel((i, j)) - return pixel + if not col % 3: + dotcol = 2 + elif not col % 2: + dotcol = 1 + else: + dotcol = 0 + px = self.get_pixel(col, row) + self.new_pixels[col, row] = int(px > dots[dotrow][dotcol]) + self.update_progress() + self.save_file() + + + def threshold_dither(self): + if not self.threshold: + px_list = list(self.source_image.getdata()) + self.threshold = sum(px_list) // len(px_list) + print("Dither method: threshold (set to: {})\033[?25l".format(self.threshold)) + for row in range(0, self.height): + for col in range(0, self.width): + px = self.get_pixel(col, row) + self.new_pixels[col, row] = int(px > self.threshold) + self.update_progress() + self.save_file() + + + def random_dither(self): + pass + + def error_diffusion_dither(self): + i = self.source_image.load() + print("Dither method: error diffusion\033[?25l") + for row in range(0, self.height): + for col in range(0, self.width): + self.new_pixels[col, row] = self.update_error(i, row, col) + self.update_progress() + self.save_file() + + + # Helper for error_diffusion_dither + def update_error(self, i, r, c): + current = self.get_pixel(c, r) + if current > 128: + res = 1 + diff = -(255 - current) + else: + res = 0 + diff = abs(0 - current) + mov = [[0, 1, 0.4375],[1, 1, 0.0625],[1, 0, 0.3125],[1, -1, 0.1875]] + for x in mov: + if r + x[0] >= self.height or c + x[1] >= self.width or c + x[1] <= 0: + continue + p = self.get_pixel(c + x[1], r + x[0]) + p = round(diff * x[2] + p) + if p < 0: + p = 0 + elif p > 255: + p = 255 + i[c + x[1], r + x[0]] = p + return res + + + # Create a new image with the given size + def create_image(self): + image = Image.new("1", (self.width, self.height)) + return image + + + # Get the pixel from the given image + def get_pixel(self, col, row): + if col > self.width or row > self.height: + return None + + # Get Pixel + pixel = self.source_image.getpixel((col, row)) + return pixel + + + def update_progress(self): + self.counter += 1 + new_percent_val = round(self.counter / self.pixel_count * 100) + new_hash_val = round(self.counter / self.pixel_count * 10) + if new_percent_val != self.percent_val or new_hash_val != self.hash_val: + print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="") + self.percent_val = new_percent_val + self.hash_val = new_hash_val + + +#---- END Dither class ----# def display_help(): @@ -171,6 +163,7 @@ def display_help(): -f output format. defaults to: png (recommended) -m dither mode. defaults to: o4. other options: e, o9 -q image quality. defaults to: 90. only has effect on png/jpg + -t threshold. default: image average tone. onls has effect in mode 't' arguments: infile path to a valid image file on your system @@ -197,9 +190,10 @@ def parse_input(): argmap = { "-f": "png", # output format "-q": 25, # optimization quality - "-m": "o4", # mode, defaults to ordered + "-m": "o4", # mode, defaults to ordered + "-t": None, # threshold "path": None, # path to file - "out": "lid_file" # output filename + "out": "lid_file" # output filename } if len(args) < 1: print("lid error: expected image filepath, received None") @@ -232,10 +226,20 @@ def parse_input(): elif argmap["-f"] == "jpg": argmap["-f"] = "jpeg" - if not argmap["-m"] in ["o4", "e", "o9"]: + if not argmap["-m"] in ["o4", "e", "o9", "t", "a"]: print("lid error: invalid dither mode provided for -m. Options: o4, o9, e. Default: o4") return None + try: + if argmap["-t"]: + argmap["-t"] = int(argmap["-t"]) + if argmap["-t"] < 0 or argmap["-t"] > 255: + print("lid error: threshold value must be between 0 and 255") + return None + except ValueError: + print("lid error: invalid threshold value provided for -m. Must be integer from 0 to 255") + return None + try: argmap["-q"] = int(argmap["-q"]) if argmap["-q"] < 1 or argmap["-q"] > 100: @@ -254,12 +258,29 @@ def parse_input(): if __name__ == "__main__": request = parse_input() if request: + d = Dither(request) if request["-m"] == "o4": - ordered_dither_4(request) + d.ordered_dither_4() elif request["-m"] == "o9": - ordered_dither_9(request) + d.ordered_dither_9() elif request["-m"] == "e": - error_dither(request) + d.error_diffusion_dither() + elif request["-m"] == "t": + d.threshold_dither() + elif request["-m"] == "a": + name = request["out"] + request["out"] = name + "_o4" + d = Dither(request) + d.ordered_dither_4() + request["out"] = name + "_o9" + d2 = Dither(request) + d2.ordered_dither_9() + request["out"] = name + "_t" + d3 = Dither(request) + d3.threshold_dither() + request["out"] = name + "_e" + d4 = Dither(request) + d4.error_diffusion_dither() else: print("Unknown mode flag") sys.exit(2)