Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- import os
- import sys
- import traceback
- import math
- import numpy as np
- import itertools
- import html
- import argparse
- from PIL import Image
- VERSION = '1.0.0.0'
- class Error (Exception):
- pass
- class ArgError (Error):
- pass
- class FileError(Error):
- pass
- class DataError(Error):
- pass
- class CmdError(Error):
- pass
- def bilevel_array(M):
- #threshold = np.uint8(np.rint(np.median(M)))
- # Oddly, the median doesn’t seem to work as well as expected
- # for severely distressed images.
- # Use a coarse histogram, resistent to spikes caused by quantisation.
- H, E = np.histogram(M, bins=10)
- H = H.tolist()
- # Find the highest peak.
- hmax = max(H)
- hmax_ix = H.index(hmax)
- # Find the highest most distant secondary peak.
- scores = [h * abs(i - hmax_ix) for i, h in enumerate(H)]
- othermax = max(scores)
- othermax_ix = scores.index(othermax)
- # Identify the histogram indices for the light and dark bin modes.
- dark_ix = othermax_ix
- light_ix = hmax_ix
- if dark_ix > light_ix:
- dark_ix, light_ix = light_ix, dark_ix
- # Set the threshold to be halfway between the outer edges
- # of the light and dark bin modes.
- threshold = np.uint8(np.rint(0.5 * (E[dark_ix] + E[light_ix + 1])))
- M = 255 * np.array(np.greater_equal(M, threshold), dtype=np.uint8)
- #Image.fromarray(M).save("xxx.png")
- return M
- def trim_array(M, bgvalue=None):
- if bgvalue is None:
- bgvalue = M[0, 0]
- top = 0
- bottom = M.shape[0]
- left = 0
- right = 0
- trimmed = dict(top=0, bottom=0, left=0, right=0)
- while bottom > top and np.all(np.equal(M[bottom - 1], bgvalue)):
- bottom -= 1
- while top < bottom and np.all(np.equal(M[top], bgvalue)):
- top += 1
- trimmed['top'] = top
- trimmed['bottom'] = M.shape[0] - bottom
- M = M[top : bottom]
- right = M.shape[1]
- while right > left and np.all(np.equal(M[:, right - 1], bgvalue)):
- right -= 1
- while left < right and np.all(np.equal(M[:, left], bgvalue)):
- left += 1
- trimmed['left'] = left
- trimmed['right'] = M.shape[1] - right
- M = M[:, left : right]
- return M, trimmed
- def runs_metrics(edges):
- e = np.asarray(edges)
- # Run lengths
- d = e[1:] - e[:-1]
- # Find the single runs.
- d1 = np.equal(d, 1)
- # Find the groups of single runs.
- # (The boundaries wlll also be used in the augmentation step later.)
- t1 = np.pad(np.logical_and(d1, ~np.pad(d1[1:], [(0, 1)])), [(1, 0)])
- b1 = np.nonzero(t1)[0]
- # The array of lengths of runs of singles may include a spurious
- # zero, but that is fine for the max() function. What is important
- # is that b1 is still good for the augemtation step.
- s1 = [sum(seg) for seg in np.split(d1, b1)]
- max_srl = max(s1)
- num_singles = sum(s1)
- # Find the minimum multiple run length (of unaugmented runs).
- dm = np.delete(d, np.nonzero(d1)[0])
- min_mrl = min(dm) if len(dm) else 0
- # The augmented run lengths are the original run lengths of
- # two or more, expanded to swallow the adjacent single runs.
- # Augmentation can result in non-integer run lengths.
- z = []
- u = 0
- for span_d, span_s1 in zip(np.split(np.asfarray(d), b1), s1):
- s = span_d[:len(span_d) - span_s1]
- if len(s):
- s[0] += u
- s[-1] += 0.5 * span_s1
- z.append(s)
- u = 0.5 * span_s1
- else:
- u += span_s1
- arls = np.array(list(itertools.chain.from_iterable(z)))
- if len(arls):
- arls[-1] += u
- # The multiple-run indices each reference the start
- # of a run of duplicate entries before augmentation.
- mrixs = np.delete(e, np.nonzero(d1)[0])[:-1]
- # (The final edge, representing the image extent, is ommited.)
- result = {
- 'num_singles': num_singles,
- 'max_srl': max_srl,
- 'min_mrl': min_mrl,
- 'mr_indices': mrixs,
- 'mr_lengths': dm,
- 'ar_lengths': arls,
- }
- return result
- def multiruns_scores(metrics):
- scores = []
- rm = metrics
- mrixs = rm['mr_indices']
- mrls = rm['mr_lengths']
- arls = rm['ar_lengths']
- h = int(round(sum(arls)))
- mindivs = len(arls)
- maxdivs = min(h // 2, math.ceil(5 * h / (min(arls))))
- ares = np.pad(np.cumsum(arls), [(1, 0)])
- singles = np.ones(h, dtype=np.bool)
- singles[list(itertools.chain.from_iterable(
- [range(ix, mrl) for ix, mrl in zip(mrixs, mrixs + mrls)]
- ))] = False
- for ndivs in range(mindivs, maxdivs + 1):
- m = ndivs * ares / h
- sum_e2 = np.linalg.norm(m - np.rint(m))
- sr_clx_score = 0
- for i in range(ndivs):
- x = int(round(h * (i + 0.5) / ndivs))
- err = singles[x] if x < h else 1
- sr_clx_score += err
- res_score = 0.001 * ndivs / maxdivs
- edge_score = round(sum_e2, 4)
- score = sr_clx_score + res_score + edge_score
- scores.append((score, ndivs))
- return scores
- def descale_array(M):
- h = M.shape[0]
- w = M.shape[1]
- rowedges = [0]
- for y in range(h):
- if y + 1 >= h or np.any(np.not_equal(M[y + 1], M[y])):
- rowedges.append(y + 1)
- re = np.array(rowedges)
- coledges = [0]
- for x in range(w):
- if x + 1 >= w or np.any(np.not_equal(M[:, x + 1], M[:, x])):
- coledges.append(x + 1)
- ce = np.array(coledges)
- rm = runs_metrics(re)
- cm = runs_metrics(ce)
- row_ns = rm['num_singles']
- row_max_srl = rm['max_srl']
- row_min_mrl = rm['min_mrl']
- col_ns = cm['num_singles']
- col_max_srl = cm['max_srl']
- col_min_mrl = cm['min_mrl']
- mpr = 2 * row_ns < h and row_max_srl < row_min_mrl
- mpc = 2 * col_ns < w and col_max_srl < col_min_mrl
- if mpr and mpc:
- # The image appears to be scaled.
- # Analyse the muliple-run lengths to determine
- # the best sampling positions.
- rss = sorted(multiruns_scores(rm))[:5]
- css = sorted(multiruns_scores(cm))[:5]
- # Of the best in each axis, favour the combinations
- # which preserve the aspect ratio.
- aspect = w / h
- scores = []
- for rs, nr in rss:
- for cs, nc in css:
- a = nc / nr
- ascore = abs(math.log(a / aspect))
- score = rs + cs + ascore
- scores.append((score, nr, nc))
- best, nr, nc = sorted(scores)[0]
- # Select single rows and columns.
- rixs = np.int32(np.minimum(h - 1,
- np.rint(h * (np.arange(nr) + 0.5) / nr)))
- cixs = np.int32(np.minimum(w - 1,
- np.rint(w * (np.arange(nc) + 0.5) / nc)))
- M = M[rixs][:, cixs]
- #Image.fromarray(M).save("xxx.png")
- return M
- def load_image_as_array(filename, bilevel=False):
- im = Image.open(filename)
- if bilevel:
- if im.mode == 'P':
- # Avoid the annoying warning about a transparent colour
- # in a paletted image.
- im = im.convert('LA')
- if im.mode != 'L':
- im = im.convert('L')
- M = np.asarray(im)
- M = bilevel_array(M)
- else:
- M = np.asarray(im)
- return M
- def descale_trimmed_array(M, trimmed=None):
- if trimmed is None:
- trimmed = dict(top=0, bottom=0, left=0, right=0)
- h0, w0 = M.shape[:2]
- M1 = descale_array(M)
- h1, w1 = M1.shape[:2]
- margins = {
- 'top': int(round(trimmed['top'] * h1 / h0)),
- 'bottom': int(round(trimmed['bottom'] * h1 / h0)),
- 'left': int(round(trimmed['left'] * w1 / w0)),
- 'right': int(round(trimmed['right'] * w1 / w0)),
- }
- return M1, margins
- def pad_array(M, margins, padvalue):
- if not isinstance(margins, dict):
- x = margins
- margins = dict(top=x, bottom=x, left=x, right=x)
- pad2d = np.expand_dims(np.asarray(padvalue), axis=(0, 1))
- padcol = np.repeat(pad2d, M.shape[0], axis=0)
- padl = np.repeat(padcol, margins['left'], axis=1)
- padr = np.repeat(padcol, margins['right'], axis=1)
- M = np.hstack((padl, M, padr))
- padrow = np.repeat(pad2d, M.shape[1], axis=1)
- padt = np.repeat(padrow, margins['top'], axis=0)
- padb = np.repeat(padrow, margins['bottom'], axis=0)
- M = np.vstack((padt, M, padb))
- return M
- def get_arguments():
- cmd = os.path.basename(sys.argv[0])
- parser = argparse.ArgumentParser(
- prog=cmd,
- add_help=False,
- description="Reduces a nearest-neighbour scaled image."
- )
- parser.add_argument(
- "-h", "--help",
- dest="help", action="store_true",
- help="Display this message and exit.")
- parser.add_argument(
- "-b", "--bilevel",
- dest="bilevel", action="store_true",
- help="Reduce the colours to just black and white.")
- parser.add_argument(
- "-i", "--invert",
- dest="invert", action="store_true",
- help="Invert colours.")
- parser.add_argument(
- "-V", "--version",
- dest="version", action="store_true",
- help="Display version and exit.")
- parser.add_argument(
- "filename", metavar="IMAGE-IN",
- type=str,
- help=("The scaled bitmapped image to reduce."))
- parser.add_argument(
- "outfilename", metavar="IMG-OUT",
- type=str,
- help=("The reduced image file to create."))
- if "-h" in sys.argv or "--help" in sys.argv:
- parser.print_help()
- sys.exit(0)
- if "-V" in sys.argv or "--version" in sys.argv:
- print(VERSION)
- sys.exit(0)
- args = parser.parse_args()
- return args
- def main():
- result = 0
- err_msg = ''
- cmd = os.path.basename(sys.argv[0])
- try:
- args = get_arguments()
- M = load_image_as_array(args.filename, args.bilevel)
- bgpixel = M[0, 0]
- M, trimmed = trim_array(M, bgpixel)
- M, margins = descale_trimmed_array(M, trimmed)
- M = pad_array(M, margins, bgpixel)
- im = Image.fromarray(M)
- if args.bilevel:
- im = im.convert('1')
- im.save(args.outfilename)
- except ArgError as E:
- err_msg = 'Error: ' + str(E)
- result = 2
- except FileError as E:
- err_msg = str(E)
- result = 3
- except CmdError as E:
- err_msg = str(E)
- result = 4
- except DataError as E:
- err_msg = str(E)
- result = 5
- except Exception as E:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- err_lines = traceback.format_exc().splitlines()
- err_msg = 'Unhandled exception:\n' + '\n'.join(err_lines)
- result = 1
- if err_msg != '':
- print(cmd + ': ' + err_msg, file=sys.stderr)
- return result
- if __name__ == '__main__':
- main()
Advertisement