diff --git a/convert.py b/convert.py index 3dad419..20957c9 100644 --- a/convert.py +++ b/convert.py @@ -3,8 +3,8 @@ import argparse import numpy as np - -import convert_shr +import convert_dhr as convert_dhr_py +import convert_shr as convert_shr_py import dither_pattern import image as image_py import palette as palette_py @@ -15,21 +15,10 @@ import screen as screen_py # - support LR/DLR # - support HGR - -def main(): - parser = argparse.ArgumentParser() +def add_common_args(parser): parser.add_argument("input", type=str, help="Input image file to process.") parser.add_argument("output", type=str, help="Output file for converted " "Apple II image.") - parser.add_argument( - "--lookahead", type=int, default=8, - help=("How many pixels to look ahead to compensate for NTSC colour " - "artifacts (default: 8)")) - parser.add_argument( - '--dither', type=str, choices=list(dither_pattern.PATTERNS.keys()), - default=dither_pattern.DEFAULT_PATTERN, - help="Error distribution pattern to apply when dithering (default: " - + dither_pattern.DEFAULT_PATTERN + ")") parser.add_argument( '--show-input', action=argparse.BooleanOptionalAction, default=False, help="Whether to show the input image before conversion.") @@ -37,17 +26,10 @@ def main(): '--show-output', action=argparse.BooleanOptionalAction, default=True, help="Whether to show the output image after conversion.") parser.add_argument( - '--palette', type=str, choices=list(set(palette_py.PALETTES.keys())), - default=palette_py.DEFAULT_PALETTE, - help='RGB colour palette to dither to. "ntsc" blends colours over 8 ' - 'pixels and gives better image quality on targets that ' - 'use/emulate NTSC, but can be substantially slower. Other ' - 'palettes determine colours based on 4 pixel sequences ' - '(default: ' + palette_py.DEFAULT_PALETTE + ")") - parser.add_argument( - '--show-palette', type=str, choices=list(palette_py.PALETTES.keys()), - help="RGB colour palette to use when --show_output (default: " - "value of --palette)") + '--save-preview', action=argparse.BooleanOptionalAction, default=True, + help='Whether to save a .PNG rendering of the output image (default: ' + 'True)' + ) parser.add_argument( '--verbose', action=argparse.BooleanOptionalAction, default=False, help="Show progress during conversion") @@ -55,56 +37,86 @@ def main(): '--gamma-correct', type=float, default=2.4, help='Gamma-correct image by this value (default: 2.4)' ) - parser.add_argument( + + +def main(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + + dhr_parser = subparsers.add_parser("dhr") + add_common_args(dhr_parser) + + def validate_lookahead(arg: int) -> int: + try: + int_arg = int(arg) + if int_arg < 1: + return int_arg + except Exception: + raise argparse.ArgumentTypeError("--lookahead must be at least 1") + dhr_parser.add_argument( + "--lookahead", type=validate_lookahead, default=8, + help=("How many pixels to look ahead to compensate for NTSC colour " + "artifacts (default: 8)")) + dhr_parser.add_argument( + '--dither', type=str, choices=list(dither_pattern.PATTERNS.keys()), + default=dither_pattern.DEFAULT_PATTERN, + help="Error distribution pattern to apply when dithering (default: " + + dither_pattern.DEFAULT_PATTERN + ")") + dhr_parser.add_argument( + '--palette', type=str, choices=list(set(palette_py.PALETTES.keys())), + default=palette_py.DEFAULT_PALETTE, + help='RGB colour palette to dither to. "ntsc" blends colours over 8 ' + 'pixels and gives better image quality on targets that ' + 'use/emulate NTSC, but can be substantially slower. Other ' + 'palettes determine colours based on 4 pixel sequences ' + '(default: ' + palette_py.DEFAULT_PALETTE + ")") + dhr_parser.add_argument( + '--show-palette', type=str, choices=list(palette_py.PALETTES.keys()), + help="RGB colour palette to use when --show_output (default: " + "value of --palette)") + dhr_parser.set_defaults(func=convert_dhr) + + shr_parser = subparsers.add_parser("shr") + add_common_args(shr_parser) + shr_parser.add_argument( '--fixed-colours', type=int, default=0, help='How many colours to fix as identical across all 16 SHR palettes ' '(default: 0)' ) - parser.add_argument( - '--save-preview', action=argparse.BooleanOptionalAction, default=True, - help='Whether to save a .PNG rendering of the output image (default: ' - 'True)' - ) - parser.add_argument( + shr_parser.add_argument( '--show-final-score', action=argparse.BooleanOptionalAction, default=False, help='Whether to output the final image quality score ' '(default: False)' ) + shr_parser.set_defaults(func=convert_shr) args = parser.parse_args() - if args.lookahead < 1: - parser.error('--lookahead must be at least 1') + args.func(args) - # palette = palette_py.PALETTES[args.palette]() - screen = screen_py.SHR320Screen() - - # Conversion matrix from RGB to CAM16UCS colour values. Indexed by - # 24-bit RGB value - rgb24_to_cam16ucs = np.load("data/rgb24_to_cam16ucs.npy") +def prepare_image(image_filename: str, show_input: bool, screen, + gamma_correct: float) -> np.ndarray: # Open and resize source image - image = image_py.open(args.input) - if args.show_input: + image = image_py.open(image_filename) + if show_input: image_py.resize(image, screen.X_RES, screen.Y_RES, srgb_output=False).show() rgb = np.array( image_py.resize(image, screen.X_RES, screen.Y_RES, - gamma=args.gamma_correct)).astype(np.float32) / 255 + gamma=gamma_correct)).astype(np.float32) / 255 + return rgb - convert_shr.convert(screen, rgb, args) - # dither = dither_pattern.PATTERNS[args.dither]() - # bitmap = dither_dhr_pyx.dither_image( - # screen, rgb, dither, args.lookahead, args.verbose, rgb24_to_cam16ucs) +def convert_dhr(args): + palette = palette_py.PALETTES[args.palette]() + screen = screen_py.DHGRScreen(palette) + rgb = prepare_image(args.input, args.show_input, screen, args.gamma_correct) + convert_dhr_py.convert(screen, rgb, args) - # Show output image by rendering in target palette - # output_palette_name = args.show_palette or args.palette - # output_palette = palette_py.PALETTES[output_palette_name]() - # output_screen = screen_py.DHGRScreen(output_palette) - # if output_palette_name == "ntsc": - # output_srgb = output_screen.bitmap_to_image_ntsc(bitmap) - # else: - # output_srgb = image_py.linear_to_srgb( - # output_screen.bitmap_to_image_rgb(bitmap)).astype(np.uint8) + +def convert_shr(args): + screen = screen_py.SHR320Screen() + rgb = prepare_image(args.input, args.show_input, screen, args.gamma_correct) + convert_shr_py.convert(screen, rgb, args) if __name__ == "__main__": diff --git a/convert_dhr.py b/convert_dhr.py new file mode 100644 index 0000000..c769bdf --- /dev/null +++ b/convert_dhr.py @@ -0,0 +1,46 @@ +import os.path + +from PIL import Image +import numpy as np + +import dither_dhr as dither_dhr_pyx +import dither_pattern +import palette as palette_py +import screen as screen_py +import image as image_py + + +def convert(screen, rgb: np.ndarray, args): + # Conversion matrix from RGB to CAM16UCS colour values. Indexed by + # 24-bit RGB value + rgb24_to_cam16ucs = np.load("data/rgb24_to_cam16ucs.npy") + + dither = dither_pattern.PATTERNS[args.dither]() + bitmap = dither_dhr_pyx.dither_image( + screen, rgb, dither, args.lookahead, args.verbose, rgb24_to_cam16ucs) + + # Show output image by rendering in target palette + output_palette_name = args.show_palette or args.palette + output_palette = palette_py.PALETTES[output_palette_name]() + output_screen = screen_py.DHGRScreen(output_palette) + if output_palette_name == "ntsc": + output_srgb = output_screen.bitmap_to_image_ntsc(bitmap) + else: + output_srgb = image_py.linear_to_srgb( + output_screen.bitmap_to_image_rgb(bitmap)).astype(np.uint8) + out_image = image_py.resize( + Image.fromarray(output_srgb), screen.X_RES, screen.Y_RES * 2, + srgb_output=True) + + if args.show_output: + out_image.show() + + if args.save_preview: + # Save Double hi-res image + outfile = os.path.join( + os.path.splitext(args.output)[0] + "-preview.png") + out_image.save(outfile, "PNG") + screen.pack(bitmap) + with open(args.output, "wb") as f: + f.write(bytes(screen.aux)) + f.write(bytes(screen.main))