268 lines
8.2 KiB
Python
Executable File
268 lines
8.2 KiB
Python
Executable File
#!/usr/bin/python3
|
|
# lid: lo-fi image dithering
|
|
# written by sloum
|
|
# licence: public domain - use it, improve it, share it!
|
|
#########
|
|
|
|
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()
|
|
|
|
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()))
|
|
|
|
|
|
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 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
|
|
|
|
|
|
# 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
|
|
|
|
# Get Pixel
|
|
pixel = image.getpixel((i, j))
|
|
return pixel
|
|
|
|
|
|
def display_help():
|
|
helptext = """
|
|
lid - lo-fi image dithering
|
|
|
|
syntax:
|
|
lid [option]... [infile] [outfile]
|
|
|
|
options:
|
|
-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
|
|
|
|
arguments:
|
|
infile path to a valid image file on your system
|
|
outfile file name to output, do not include file extension
|
|
|
|
example usage:
|
|
lid -f jpeg -q 75 -m e ~/my_picture.png my_dithered_picture
|
|
|
|
notes:
|
|
it is highly recommended that users output their files as PNG. the file
|
|
size will be significantly smaller than jpeg and the quality in most
|
|
cases will be about the same.
|
|
|
|
the quality flag mostly seems to effect jpegs and does not do a whole
|
|
heap to PNG files, but play around with it as mileage may vary.
|
|
"""
|
|
print(helptext)
|
|
|
|
|
|
# Get and validate the arguments
|
|
# returns dict or None
|
|
def parse_input():
|
|
args = sys.argv[1:]
|
|
argmap = {
|
|
"-f": "png", # output format
|
|
"-q": 25, # optimization quality
|
|
"-m": "o4", # mode, defaults to ordered
|
|
"path": None, # path to file
|
|
"out": "lid_file" # output filename
|
|
}
|
|
if len(args) < 1:
|
|
print("lid error: expected image filepath, received None")
|
|
return None
|
|
|
|
if "help" in args or "-h" in args or "--help" in args:
|
|
display_help()
|
|
sys.exit(1)
|
|
|
|
item = None
|
|
for x in args:
|
|
if x in argmap:
|
|
item = x
|
|
continue
|
|
elif item:
|
|
argmap[item] = x
|
|
item = None
|
|
elif len(x) > 0 and x[0] == "-":
|
|
print("lid error: unknown flag {}".format(x))
|
|
return None
|
|
else:
|
|
if argmap["path"]:
|
|
argmap["out"] = x
|
|
else:
|
|
argmap["path"] = x
|
|
|
|
if not argmap["-f"].lower() in ["jpeg", "jpg", "png", "gif"]:
|
|
print("lid error: invalid format requested with -f flag. Options: jpeg, png, gif. Defaults to png")
|
|
return None
|
|
elif argmap["-f"] == "jpg":
|
|
argmap["-f"] = "jpeg"
|
|
|
|
if not argmap["-m"] in ["o4", "e", "o9"]:
|
|
print("lid error: invalid dither mode provided for -m. Options: o4, o9, e. Default: o4")
|
|
return None
|
|
|
|
try:
|
|
argmap["-q"] = int(argmap["-q"])
|
|
if argmap["-q"] < 1 or argmap["-q"] > 100:
|
|
print("lid error: -q value must be between 1 and 100")
|
|
return None
|
|
except ValueError:
|
|
print("lid error: -q value must be an integer")
|
|
return None
|
|
|
|
if not argmap["path"] or not os.path.isfile(argmap["path"]):
|
|
print("lid error: invalid filepath")
|
|
return None
|
|
|
|
return argmap
|
|
|
|
if __name__ == "__main__":
|
|
request = parse_input()
|
|
if request:
|
|
if request["-m"] == "o4":
|
|
ordered_dither_4(request)
|
|
elif request["-m"] == "o9":
|
|
ordered_dither_9(request)
|
|
elif request["-m"] == "e":
|
|
error_dither(request)
|
|
else:
|
|
print("Unknown mode flag")
|
|
sys.exit(2)
|
|
sys.exit(0)
|
|
sys.exit(1)
|