From fe76ef596a49ef5d183352babb056618cbfcc3d4 Mon Sep 17 00:00:00 2001 From: Nick Downing Date: Fri, 19 Jul 2024 17:31:32 +1000 Subject: [PATCH] Improve /shape/shape_mono_to_color.py to include unfringing based on a decoding table, delete /shape/shape_unfringe.py and some of the shapeN.png intermediate files, simplify /shape/shape_color_to_mono.py, need to redo the shape_(d)hgr.png --- .gitignore | 3 +- shape/Makefile | 18 ++--- shape/shape_color_to_mono.py | 145 ++++++++++------------------------- shape/shape_mono_to_color.py | 77 +++++++++++++++---- shape/shape_unfringe.py | 129 ------------------------------- 5 files changed, 113 insertions(+), 259 deletions(-) delete mode 100755 shape/shape_unfringe.py diff --git a/.gitignore b/.gitignore index 16a09d5..e830bcc 100644 --- a/.gitignore +++ b/.gitignore @@ -41,12 +41,11 @@ /shape/shape0a.png /shape/shape0b.png /shape/shape0c.png +/shape/shape0d.png /shape/shape1.png /shape/shape2.png /shape/shape3.png /shape/shape4.png -/shape/shape5.png -/shape/shape6.png /shape/shape_data.inc /shape/shape_index.inc /shape/sky_blazer_shape.png diff --git a/shape/Makefile b/shape/Makefile index 04966e4..6829d9a 100644 --- a/shape/Makefile +++ b/shape/Makefile @@ -14,12 +14,11 @@ shape0.png \ shape0a.png \ shape0b.png \ shape0c.png \ +shape0d.png \ shape1.png \ shape2.png \ shape3.png \ shape4.png \ -shape5.png \ -shape6.png \ sky_blazer_shape.png dhgr_pixel_shape_index.inc: pixel.txt dhgr_shape.txt shape0c.png @@ -60,13 +59,7 @@ shape2.png: shape0.png shape3.png: shape2.png ./shape_round.py $< $@ -shape4.png: shape2.png - ./shape_unfringe.py $< $@ - -shape5.png: shape4.png - ./shape_round.py $< $@ - -shape6.png: shape5.png +shape4.png: shape3.png ./shape_enhance.py $< $@ shape0a.png: shape_hgr.png @@ -78,6 +71,9 @@ shape0b.png: shape0.png shape0a.png shape0c.png: shape_dhgr.png ./shape_color_to_mono.py $< $@ +shape0d.png: shape0.png shape0c.png + gmic $^ -blend xor -o $@ + star_blazer0.ihx: star_blazer0.a2bin star_blazer0_segments.txt ../utils/a2_load.py --segments=star_blazer0_segments.txt 0x17d1 $< $@ @@ -127,8 +123,8 @@ pixel_shape_index.inc \ pixel_shape_data.inc \ shape_index.inc \ shape_data.inc \ -shape0[abc].png \ -shape[0-6].png \ +shape0[abcd].png \ +shape[0-4].png \ sky_blazer_shape.png \ Sky\ Blazer\ \(4am\ and\ san\ inc\ crack\).dsk \ Sky\ Blazer\ \(4am\ and\ san\ inc\ crack\).nib diff --git a/shape/shape_color_to_mono.py b/shape/shape_color_to_mono.py index 646e9bc..b279495 100755 --- a/shape/shape_color_to_mono.py +++ b/shape/shape_color_to_mono.py @@ -34,17 +34,14 @@ PALETTE = numpy.array( ) hgr = False -mono = False while len(sys.argv) >= 2: if sys.argv[1] == '--hgr': hgr = True - elif sys.argv[1] == '--mono': - mono = True else: break del sys.argv[1] if len(sys.argv) < 3: - print(f'usage: {sys.argv[0]:s} [--hgr] [--mono] in.png out.png') + print(f'usage: {sys.argv[0]:s} [--hgr] in.png out.png') sys.exit(EXIT_FAILURE) in_png = sys.argv[1] out_png = sys.argv[2] @@ -84,112 +81,54 @@ for i in range(0x100): continue shape = shape[:ys, :xs] - if ys & 1: # it has been rounded (half pixel shift to right and down) - # take only odd lines, then double - shape = numpy.copy(shape[:-1, :]) - shape[::2, :] = shape[1::2, :] + # temporary, for shape_(d)hgr.png + if (ys & 1) == 1: + shape = shape[1:, :] ys -= 1 - - # note: we need extra thres to deal with white propagating - # onto a narrow gap from both sides, extra widens gap again - if mono: - assert xs & 1 - window = 1 - thres = 2 - elif xs & 1: - # this works to encode shape3.png (fringed and rounded) - #shape = shape[:, 1:-1] - #xs -= 2 - #window = 3 - #thres = 3 - # but instead treat as mono, since the new mono rounding - # fortuitously leaves an odd width, so we can auto-detect - window = 1 - thres = 2 - else: # it has been unfringed (half pixel shift to right) - # it should be window = 6 here, but that would be too lossy, - # so trim an extra pixel from previous steps and do normally - shape = shape[:, 2:-2] - xs -= 4 - window = 2 - thres = 3 - elif mono: - assert xs & 1 - window = 1 - thres = 1 - elif (xs & 1) == 0: - shape = shape[:, 1:-1] - xs -= 2 - window = 2 - thres = 2 - else: # it has been unfringed (half pixel shift to right) + if (xs & 3) == 0: + assert False + elif (xs & 3) == 1: shape = shape[:, 1:-1] xs -= 2 - window = 3 - thres = 3 + elif (xs & 3) == 2: + shape = shape[:, 1:-2] + xs -= 3 + + # only look at even lines, they will be duplicated again at the end + shape = shape[::2] + ys >>= 1 + + # remove 2 pixels that were added by shape_colour_to_mono.py decoder + assert xs >= 2 + shape = shape[:, 1:-1] + xs -= 2 + assert (xs & 3) == 1 + # take a single bit from each 4-bit colour value, according to x % 4 shape = ( ( - shape[:, :, numpy.newaxis] >> - numpy.arange( - 4, - dtype = numpy.int32 - )[numpy.newaxis, numpy.newaxis, :] + shape >> (numpy.arange(xs, dtype = numpy.int32) & 3)[numpy.newaxis, :] ) & 1 - ).astype(numpy.uint8) - - if xs >= window + hgr: - shape1 = numpy.zeros((ys, xs + 1 - window), numpy.uint8) - for j in range(4): - for k in range(window): - # since xs might not be a multiple of 4 (usually it is), - # calculate how many elements will be picked up by j::4 - w = (xs + 4 - window - j) >> 2 - shape1[:, j::4] += shape[:, j + k:j + k + (w << 2):4, j] - shape = (shape1[::2, :] + shape1[1::2, :]) >= thres - xs += 1 - window - ys >>= 1 - - # if hgr we will only use every other pixel - # extend window by one extra pixel to include effect of skipped pixels - # and then try even or odd pixels to find best match to the dhgr line - if hgr: - shape1 = ( - shape1[::2, :-1] + - shape1[1::2, 1:] + - shape1[::2, :-1] + - shape1[1::2, 1:] - ) >= thres * 2 - - hibit0 = numpy.zeros((ys, xs), bool) - hibit0[:, :-1:2] = shape1[:, ::2] - hibit0[:, 1:-1:2] = shape1[:, ::2] - - hibit1 = numpy.zeros((ys, xs), bool) - hibit1[:, 1::2] = shape1[:, 1::2] - hibit1[:, 2::2] = shape1[:, 1::2] - - for j in range(ys): - #if ( - # numpy.any(hibit0[j, :] != shape[j, :]) and - # numpy.any(hibit1[j, :] != shape[j, :]) - #): - # print('shape', shape[j, :]) - # print('shape1', shape1[j, :]) - # print('hibit0', hibit0[j, :]) - # print('hibit1', hibit1[j, :]) - # assert False - shape[j, :] = ( - hibit1[j, :] - if ( - numpy.sum(hibit1[j, :] ^ shape[j, :]) < - numpy.sum(hibit0[j, :] ^ shape[j, :]) - ) else - hibit0[j, :] # preferred in case of tie - ) - - image_out[y:y + ys * 2:2, x:x + xs] = shape * 0xf - image_out[y + 1:y + ys * 2 + 1:2, x:x + xs] = shape * 0xf + ).astype(bool) + + if hgr: + # for each line, replace even pixels with odd pixels biased left or right + # biasing is done by creating a padded version and dropping first or last + assert (xs & 1) == 1 + odd_pixels = numpy.zeros((ys, (xs >> 1) + 2), bool) + odd_pixels[:, 1:-1] = shape[:, 1:-1:2] + for i in range(ys): + shape[i, ::2] = ( + odd_pixels[i, :-1] + if ( + numpy.sum(shape[i, ::2] ^ odd_pixels[i, :-1]) < + numpy.sum(shape[i, ::2] ^ odd_pixels[i, 1:]) + ) else + odd_pixels[i, 1:] + ) + + image_out[y:y + ys * 2:2, x:x + xs] = shape * 0xf + image_out[y + 1:y + 1 + ys * 2:2, x:x + xs] = shape * 0xf image_out_pil = PIL.Image.new( 'P', diff --git a/shape/shape_mono_to_color.py b/shape/shape_mono_to_color.py index f11dc37..b798f98 100755 --- a/shape/shape_mono_to_color.py +++ b/shape/shape_mono_to_color.py @@ -33,6 +33,51 @@ PALETTE = numpy.array( numpy.uint8 ) +# decoder uses a majority voting scheme with tie breaking by parity +def parity(nibble): + nibble = (nibble & 3) ^ (nibble >> 2) + nibble = (nibble & 1) ^ (nibble >> 1) + return nibble + +decode = [] +for i in range(0x80): + candidates = [ + i & 0xf, + (i & 0xe) | ((i >> 4) & 1), + (i & 0xc) | ((i >> 4) & 3), + (i & 8) | ((i >> 4) & 7) + ] + if candidates[1] == candidates[2]: + result = candidates[1] + elif candidates[0] == candidates[1] and candidates[2] != candidates[3]: + result = candidates[0] + elif candidates[0] != candidates[1] and candidates[2] == candidates[3]: + result = candidates[3] + elif candidates[1] == 0 or candidates[1] == 0xf: + # exceptional case: 1100001 -- don't treat as transition from 0001 to 0110 + # exceptional case: 0011110 -- don't treat as transition from 1110 to 1001 + result = candidates[1] + elif candidates[2] == 0 or candidates[2] == 0xf: + # exceptional case: 1000011 -- don't treat as transition from 0011 to 0100 + # exceptional case: 0111100 -- don't treat as transition from 1100 to 1011 + result = candidates[2] + else: + # we assume it is a fringed transition from candidate 0 to candidate 3 + # on the left are more of candidate 0, on the right more of candidate 3 + # so to ensure a clean transition, we just have to choose candidate 0 or 3 + p0 = parity(candidates[0]) + p3 = parity(candidates[3]) + if p0 < p3: + result = candidates[0] + elif p3 < p0: + result = candidates[3] + else: + assert False + # actual colour indicated by "result" should be rotated left by (x + n) % 4, + # as we don't have a circular rotate, repeat the lower 3 bits for right shift + decode.append(result | ((result & 7) << 4)) +decode = numpy.array(decode, numpy.uint8) + if len(sys.argv) < 3: print(f'usage: {sys.argv[0]:s} in.png out.png') sys.exit(EXIT_FAILURE) @@ -74,21 +119,25 @@ for i in range(0x100): continue shape = shape[:ys, :xs] - shape1 = numpy.zeros((ys, xs + 3, 4), bool) - for j in range(4): - for k in range(4): - # since xs might not be a multiple of 4 (in fact isn't), - # calculate how many elements will be picked up by j::4 - w = (xs + 3 - j) >> 2 - shape1[:, j + k:j + k + (w << 2):4, j] = shape[:, j::4] - shape = shape1 - xs += 3 + # decoding uses a 7-bit sliding window, so add 4 spare bits each side, + # the windowing process will remove 6 so eventually we're adding 2 pixels + shape1 = numpy.zeros((ys, xs + 8), bool) + shape1[:, 4:-4] = shape + xs += 2 - shape = numpy.bitwise_or.reduce( - shape << - numpy.arange(4, dtype = numpy.int32)[numpy.newaxis, numpy.newaxis, :], - 2 - ).astype(numpy.uint8) + shape = ( + decode[ + numpy.bitwise_or.reduce( + numpy.stack( + [shape1[:, i:xs + i] for i in range(7)], + 2 + ).astype(numpy.uint8) << + numpy.arange(7, dtype = numpy.int32)[numpy.newaxis, numpy.newaxis, :], + 2 + ).reshape((ys * xs,)) + ].reshape((ys, xs)) >> + (-numpy.arange(xs, dtype = numpy.int32) & 3)[numpy.newaxis, :] + ) & 0xf image_out[y:y + ys, x:x + xs] = shape diff --git a/shape/shape_unfringe.py b/shape/shape_unfringe.py deleted file mode 100755 index 2019dff..0000000 --- a/shape/shape_unfringe.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 - -import numpy -import sys -import PIL.Image - -EXIT_SUCCESS = 0 -EXIT_FAILURE = 1 - -PITCH_X = 256 -PITCH_Y = 64 - -# see palette.py -PALETTE = numpy.array( - [ - [0x00, 0x00, 0x00], - [0xbc, 0x00, 0x89], - [0x00, 0x00, 0xbc], - [0xbc, 0x00, 0xe1], - [0x00, 0xbc, 0x89], - [0x80, 0x80, 0x80], #[0xbc, 0xbc, 0xbc], - [0x00, 0xbc, 0xe1], - [0xbc, 0xbc, 0xff], - [0xbc, 0xbc, 0x00], - [0xff, 0xbc, 0x89], - [0xc0, 0xc0, 0xc0], #[0xbc, 0xbc, 0xbc], - [0xff, 0xbc, 0xe1], - [0xbc, 0xff, 0x89], - [0xff, 0xff, 0xbc], - [0xbc, 0xff, 0xe1], - [0xff, 0xff, 0xff], - ], - numpy.uint8 -) - -if len(sys.argv) < 3: - print(f'usage: {sys.argv[0]:s} in.png out.png') - sys.exit(EXIT_FAILURE) -in_png = sys.argv[1] -out_png = sys.argv[2] - -image_in_pil = PIL.Image.open(in_png) -assert image_in_pil.mode == 'P' -image_in = numpy.frombuffer( - image_in_pil.tobytes(), - dtype = numpy.uint8 -).reshape((image_in_pil.size[1], image_in_pil.size[0])) - -image_out = numpy.zeros((PITCH_Y * 32, PITCH_X * 8), numpy.uint8) -assert image_out.shape == image_in.shape - -for i in range(0x100): - j = i & 7 - k = i >> 3 - x = j * PITCH_X - y = k * PITCH_Y - bg = 0xa if (j ^ k) & 1 else 5 - image_out[y:y + PITCH_Y, x:x + PITCH_X] = bg - - shape = image_in[y:y + PITCH_Y, x:x + PITCH_X] - xs = shape.shape[1] - while xs: - if numpy.any(shape[:, xs - 1] != bg): - break - xs -= 1 - else: - continue - ys = shape.shape[0] - while ys: - if numpy.any(shape[ys - 1, :] != bg): - break - ys -= 1 - else: - continue - shape = shape[:ys, :xs] - - # spread even colours to right by 1 pixel - # temporarily pad image on either side by 1 pixel first - # then remove the left padding after (it spreads into the right padding) - shape1 = numpy.zeros((ys, xs + 2), numpy.uint8) - shape1[:, 1:-1] = shape - shape = shape1 - xs += 2 - - parity = shape[:, :-1] - parity = numpy.bitwise_xor(parity & 3, parity >> 2) - parity = numpy.bitwise_xor(parity & 1, parity >> 1) - mask = parity == 0 - - shape[:, 1:][mask] = shape[:, :-1][mask] - shape = shape[:, 1:] - xs -= 1 - - shape1 = numpy.zeros((ys, xs + 4), numpy.uint8) - shape1[:, 2:-2] = shape - - # black is now separated from white by 2 even pixels - # spread the black and white into this gap from either side - black = shape1 == 0 - white = shape1 == 0xf - mask = numpy.logical_and(black[:, :-3], white[:, 3:]) - shape[mask[:, 1:]] = 0 - shape[mask[:, :-1]] = 0xf - mask = numpy.logical_and(white[:, :-3], black[:, 3:]) - shape[mask[:, 1:]] = 0xf - shape[mask[:, :-1]] = 0 - - # greys are now separated from black or white by 1 odd pixel - # spread the black and white into this gap from either side - grey = numpy.logical_or(shape1 == 5, shape1 == 0xa) - mask = numpy.logical_and(black[:, :-3], grey[:, 2:-1]) - shape[mask[:, 1:]] = 0 - mask = numpy.logical_and(grey[:, 1:-2], black[:, 3:]) - shape[mask[:, :-1]] = 0 - mask = numpy.logical_and(white[:, :-3], grey[:, 2:-1]) - shape[mask[:, 1:]] = 0xf - mask = numpy.logical_and(grey[:, 1:-2], white[:, 3:]) - shape[mask[:, :-1]] = 0xf - - image_out[y:y + ys, x:x + xs] = shape - -image_out_pil = PIL.Image.new( - 'P', - (image_out.shape[1], image_out.shape[0]), - None -) -image_out_pil.frombytes(image_out.tobytes()) -image_out_pil.putpalette(list(PALETTE.reshape((0x30,)))) -image_out_pil.save(out_png) -- 2.34.1