updates
This commit is contained in:
254
Backend/venv/bin/priditherpng
Executable file
254
Backend/venv/bin/priditherpng
Executable file
@@ -0,0 +1,254 @@
|
||||
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
|
||||
|
||||
# pipdither
|
||||
# Error Diffusing image dithering.
|
||||
# Now with serpentine scanning.
|
||||
|
||||
# See http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
|
||||
|
||||
# http://www.python.org/doc/2.4.4/lib/module-bisect.html
|
||||
from bisect import bisect_left
|
||||
|
||||
|
||||
import png
|
||||
|
||||
|
||||
def dither(
|
||||
out,
|
||||
input,
|
||||
bitdepth=1,
|
||||
linear=False,
|
||||
defaultgamma=1.0,
|
||||
targetgamma=None,
|
||||
cutoff=0.5, # see :cutoff:default
|
||||
):
|
||||
"""Dither the input PNG `inp` into an image with a smaller bit depth
|
||||
and write the result image onto `out`. `bitdepth` specifies the bit
|
||||
depth of the new image.
|
||||
|
||||
Normally the source image gamma is honoured (the image is
|
||||
converted into a linear light space before being dithered), but
|
||||
if the `linear` argument is true then the image is treated as
|
||||
being linear already: no gamma conversion is done (this is
|
||||
quicker, and if you don't care much about accuracy, it won't
|
||||
matter much).
|
||||
|
||||
Images with no gamma indication (no ``gAMA`` chunk) are normally
|
||||
treated as linear (gamma = 1.0), but often it can be better
|
||||
to assume a different gamma value: For example continuous tone
|
||||
photographs intended for presentation on the web often carry
|
||||
an implicit assumption of being encoded with a gamma of about
|
||||
0.45 (because that's what you get if you just "blat the pixels"
|
||||
onto a PC framebuffer), so ``defaultgamma=0.45`` might be a
|
||||
good idea. `defaultgamma` does not override a gamma value
|
||||
specified in the file itself: It is only used when the file
|
||||
does not specify a gamma.
|
||||
|
||||
If you (pointlessly) specify both `linear` and `defaultgamma`,
|
||||
`linear` wins.
|
||||
|
||||
The gamma of the output image is, by default, the same as the input
|
||||
image. The `targetgamma` argument can be used to specify a
|
||||
different gamma for the output image. This effectively recodes the
|
||||
image to a different gamma, dithering as we go. The gamma specified
|
||||
is the exponent used to encode the output file (and appears in the
|
||||
output PNG's ``gAMA`` chunk); it is usually less than 1.
|
||||
|
||||
"""
|
||||
|
||||
# Encoding is what happened when the PNG was made (and also what
|
||||
# happens when we output the PNG). Decoding is what we do to the
|
||||
# source PNG in order to process it.
|
||||
|
||||
# The dithering algorithm is not completely general; it
|
||||
# can only do bit depth reduction, not arbitrary palette changes.
|
||||
import operator
|
||||
|
||||
maxval = 2 ** bitdepth - 1
|
||||
r = png.Reader(file=input)
|
||||
|
||||
_, _, pixels, info = r.asDirect()
|
||||
planes = info["planes"]
|
||||
# :todo: make an Exception
|
||||
assert planes == 1
|
||||
width = info["size"][0]
|
||||
sourcemaxval = 2 ** info["bitdepth"] - 1
|
||||
|
||||
if linear:
|
||||
gamma = 1
|
||||
else:
|
||||
gamma = info.get("gamma") or defaultgamma
|
||||
|
||||
# Calculate an effective gamma for input and output;
|
||||
# then build tables using those.
|
||||
|
||||
# `gamma` (whether it was obtained from the input file or an
|
||||
# assumed value) is the encoding gamma.
|
||||
# We need the decoding gamma, which is the reciprocal.
|
||||
decode = 1.0 / gamma
|
||||
|
||||
# `targetdecode` is the assumed gamma that is going to be used
|
||||
# to decoding the target PNG.
|
||||
# Note that even though we will _encode_ the target PNG we
|
||||
# still need the decoding gamma, because
|
||||
# the table we use maps from PNG pixel value to linear light level.
|
||||
if targetgamma is None:
|
||||
targetdecode = decode
|
||||
else:
|
||||
targetdecode = 1.0 / targetgamma
|
||||
|
||||
incode = build_decode_table(sourcemaxval, decode)
|
||||
|
||||
# For encoding, we still build a decode table, because we
|
||||
# use it inverted (searching with bisect).
|
||||
outcode = build_decode_table(maxval, targetdecode)
|
||||
|
||||
# The table used for choosing output codes. These values represent
|
||||
# the cutoff points between two adjacent output codes.
|
||||
# The cutoff parameter can be varied between 0 and 1 to
|
||||
# preferentially choose lighter (when cutoff > 0.5) or
|
||||
# darker (when cutoff < 0.5) values.
|
||||
# :cutoff:default: The default for this used to be 0.75, but
|
||||
# testing by drj on 2021-07-30 showed that this produces
|
||||
# banding when dithering left-to-right gradients;
|
||||
# test with:
|
||||
# priforgepng grl | priditherpng | kitty icat
|
||||
choosecode = list(zip(outcode[1:], outcode))
|
||||
p = cutoff
|
||||
choosecode = [x[0] * p + x[1] * (1.0 - p) for x in choosecode]
|
||||
|
||||
rows = repeat_header(pixels)
|
||||
dithered_rows = run_dither(incode, choosecode, outcode, width, rows)
|
||||
dithered_rows = remove_header(dithered_rows)
|
||||
|
||||
info["bitdepth"] = bitdepth
|
||||
info["gamma"] = 1.0 / targetdecode
|
||||
w = png.Writer(**info)
|
||||
w.write(out, dithered_rows)
|
||||
|
||||
|
||||
def build_decode_table(maxval, gamma):
|
||||
"""Build a lookup table for decoding;
|
||||
table converts from pixel values to linear space.
|
||||
"""
|
||||
|
||||
assert maxval == int(maxval)
|
||||
assert maxval > 0
|
||||
|
||||
f = 1.0 / maxval
|
||||
table = [f * v for v in range(maxval + 1)]
|
||||
if gamma != 1.0:
|
||||
table = [v ** gamma for v in table]
|
||||
return table
|
||||
|
||||
|
||||
def run_dither(incode, choosecode, outcode, width, rows):
|
||||
"""
|
||||
Run an serpentine dither.
|
||||
Using the incode and choosecode tables.
|
||||
"""
|
||||
|
||||
# Errors diffused downwards (into next row)
|
||||
ed = [0.0] * width
|
||||
flipped = False
|
||||
for row in rows:
|
||||
# Convert to linear...
|
||||
row = [incode[v] for v in row]
|
||||
# Add errors...
|
||||
row = [e + v for e, v in zip(ed, row)]
|
||||
|
||||
if flipped:
|
||||
row = row[::-1]
|
||||
targetrow = [0] * width
|
||||
|
||||
for i, v in enumerate(row):
|
||||
# `it` will be the index of the chosen target colour;
|
||||
it = bisect_left(choosecode, v)
|
||||
targetrow[i] = it
|
||||
t = outcode[it]
|
||||
# err is the error that needs distributing.
|
||||
err = v - t
|
||||
|
||||
# Sierra "Filter Lite" distributes * 2
|
||||
# as per this diagram. 1 1
|
||||
ef = err * 0.5
|
||||
# :todo: consider making rows one wider at each end and
|
||||
# removing "if"s
|
||||
if i + 1 < width:
|
||||
row[i + 1] += ef
|
||||
ef *= 0.5
|
||||
ed[i] = ef
|
||||
if i:
|
||||
ed[i - 1] += ef
|
||||
|
||||
if flipped:
|
||||
ed = ed[::-1]
|
||||
targetrow = targetrow[::-1]
|
||||
yield targetrow
|
||||
flipped = not flipped
|
||||
|
||||
|
||||
WARMUP_ROWS = 32
|
||||
|
||||
|
||||
def repeat_header(rows):
|
||||
"""Repeat the first row, to "warm up" the error register."""
|
||||
for row in rows:
|
||||
yield row
|
||||
for _ in range(WARMUP_ROWS):
|
||||
yield row
|
||||
break
|
||||
yield from rows
|
||||
|
||||
|
||||
def remove_header(rows):
|
||||
"""Remove the same number of rows that repeat_header added."""
|
||||
|
||||
for _ in range(WARMUP_ROWS):
|
||||
next(rows)
|
||||
yield from rows
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import sys
|
||||
|
||||
# https://docs.python.org/3.5/library/argparse.html
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
progname, *args = argv
|
||||
|
||||
parser.add_argument("--bitdepth", type=int, default=1, help="bitdepth of output")
|
||||
parser.add_argument(
|
||||
"--cutoff",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="cutoff to select adjacent output values",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--defaultgamma",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="gamma value to use when no gamma in input",
|
||||
)
|
||||
parser.add_argument("--linear", action="store_true", help="force linear input")
|
||||
parser.add_argument(
|
||||
"--targetgamma",
|
||||
type=float,
|
||||
help="gamma to use in output (target), defaults to input gamma",
|
||||
)
|
||||
parser.add_argument(
|
||||
"input", nargs="?", default="-", type=png.cli_open, metavar="PNG"
|
||||
)
|
||||
|
||||
ns = parser.parse_args(args)
|
||||
|
||||
return dither(png.binary_stdout(), **vars(ns))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user