Added a threshold and all mode. Reworked the application into a class and reused relevant code for each dithering method.
This commit is contained in:
parent
c842c03993
commit
ce837f1d7e
|
@ -0,0 +1 @@
|
||||||
|
oneliner
|
17
README.md
17
README.md
|
@ -1,7 +1,7 @@
|
||||||
# Lid
|
# Lid
|
||||||
_lo-fi image dithering_
|
_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
|
## Dependencies
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@ lid [option]... [source path] [output name]
|
||||||
|
|
||||||
Options:<br>
|
Options:<br>
|
||||||
-f = output format, defaults to the recommended setting: png<br>
|
-f = output format, defaults to the recommended setting: png<br>
|
||||||
-m = mode, available: o4, o9, e. default: o4.
|
-m = mode, available: o4, o9, e. default: o4.<br>
|
||||||
-q = quality, defaults to 90, only affects jpeg/jpg
|
-q = quality (0 - 100), defaults to 90, only affects jpeg/jpg<br>
|
||||||
|
-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<br>
|
source path = a path to an image file you want to dither<br>
|
||||||
output name = the name of the file you would like to output, do not include file extension
|
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
|
### Modes
|
||||||
|
|
||||||
|
#### t
|
||||||
|
This is the lowest quality mode. It produces the smallest file, but at the cost of most detail.
|
||||||
|
|
||||||
#### o4
|
#### 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
|
#### 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
|
#### 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.
|
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
|
## License
|
||||||
Public domain. Use it, improve it, share it!
|
Public domain. Use it, improve it, share it!
|
||||||
|
|
317
lid
317
lid
|
@ -8,156 +8,148 @@ from PIL import Image
|
||||||
import sys
|
import sys
|
||||||
import os, os.path
|
import os, os.path
|
||||||
|
|
||||||
# Default dithering: 4 level ordered dither
|
class Dither:
|
||||||
def ordered_dither_4(r):
|
def __init__(self, data):
|
||||||
im = Image.open(r["path"]).convert("L")
|
self.source_image = Image.open(data["path"]).convert("L")
|
||||||
width, height = im.size
|
self.width, self.height = self.source_image.size
|
||||||
new = create_image(width, height)
|
self.pixel_count = self.width * self.height
|
||||||
newpixels = new.load()
|
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 = [
|
#used for printing progress
|
||||||
[64, 128],
|
self.counter = 1
|
||||||
[192, 0]
|
self.percent_val = 0
|
||||||
]
|
self.hash_val = 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):
|
def save_file(self):
|
||||||
w, h = im.size
|
output_filename = "{}.{}".format(self.output_name, self.output_format.lower())
|
||||||
current = get_pixel(im, c, r)
|
self.new.save(output_filename, self.output_format.upper(), optimize=True, quality=self.output_quality)
|
||||||
if current > 128:
|
print("\033[20C\nFinished! ~ {}/{}\033[?25h".format(os.getcwd(), output_filename))
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
# Default dithering
|
||||||
def print_spinner(n):
|
def ordered_dither_4(self):
|
||||||
print("\r|{:<10}|".format("#" * n), end="")
|
dots = [[64, 128],[192, 0]]
|
||||||
|
print("Dither method: ordered (4 levels)\033[?25l")
|
||||||
# Create a new image with the given size
|
for row in range(0, self.height):
|
||||||
def create_image(i, j):
|
for col in range(0, self.width):
|
||||||
image = Image.new("1", (i, j))
|
dotrow = 1 if row % 2 else 0
|
||||||
return image
|
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 ordered_dither_9(self):
|
||||||
def get_pixel(image, i, j):
|
dots = [
|
||||||
width, height = image.size
|
[0, 196, 84],
|
||||||
if i > width or j > height:
|
[168, 140, 56],
|
||||||
return None
|
[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
|
if not col % 3:
|
||||||
pixel = image.getpixel((i, j))
|
dotcol = 2
|
||||||
return pixel
|
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():
|
def display_help():
|
||||||
|
@ -171,6 +163,7 @@ def display_help():
|
||||||
-f output format. defaults to: png (recommended)
|
-f output format. defaults to: png (recommended)
|
||||||
-m dither mode. defaults to: o4. other options: e, o9
|
-m dither mode. defaults to: o4. other options: e, o9
|
||||||
-q image quality. defaults to: 90. only has effect on png/jpg
|
-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:
|
arguments:
|
||||||
infile path to a valid image file on your system
|
infile path to a valid image file on your system
|
||||||
|
@ -197,9 +190,10 @@ def parse_input():
|
||||||
argmap = {
|
argmap = {
|
||||||
"-f": "png", # output format
|
"-f": "png", # output format
|
||||||
"-q": 25, # optimization quality
|
"-q": 25, # optimization quality
|
||||||
"-m": "o4", # mode, defaults to ordered
|
"-m": "o4", # mode, defaults to ordered
|
||||||
|
"-t": None, # threshold
|
||||||
"path": None, # path to file
|
"path": None, # path to file
|
||||||
"out": "lid_file" # output filename
|
"out": "lid_file" # output filename
|
||||||
}
|
}
|
||||||
if len(args) < 1:
|
if len(args) < 1:
|
||||||
print("lid error: expected image filepath, received None")
|
print("lid error: expected image filepath, received None")
|
||||||
|
@ -232,10 +226,20 @@ def parse_input():
|
||||||
elif argmap["-f"] == "jpg":
|
elif argmap["-f"] == "jpg":
|
||||||
argmap["-f"] = "jpeg"
|
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")
|
print("lid error: invalid dither mode provided for -m. Options: o4, o9, e. Default: o4")
|
||||||
return None
|
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:
|
try:
|
||||||
argmap["-q"] = int(argmap["-q"])
|
argmap["-q"] = int(argmap["-q"])
|
||||||
if argmap["-q"] < 1 or argmap["-q"] > 100:
|
if argmap["-q"] < 1 or argmap["-q"] > 100:
|
||||||
|
@ -254,12 +258,29 @@ def parse_input():
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
request = parse_input()
|
request = parse_input()
|
||||||
if request:
|
if request:
|
||||||
|
d = Dither(request)
|
||||||
if request["-m"] == "o4":
|
if request["-m"] == "o4":
|
||||||
ordered_dither_4(request)
|
d.ordered_dither_4()
|
||||||
elif request["-m"] == "o9":
|
elif request["-m"] == "o9":
|
||||||
ordered_dither_9(request)
|
d.ordered_dither_9()
|
||||||
elif request["-m"] == "e":
|
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:
|
else:
|
||||||
print("Unknown mode flag")
|
print("Unknown mode flag")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
Loading…
Reference in New Issue