diff --git a/README.md b/README.md index 199348b..5f761fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ][-pix 2.0 +# ][-pix 2.1 ][-pix is an image conversion utility targeting Apple II graphics modes, currently Double Hi-Res (enhanced //e, //c, //gs) and Super Hi-Res (//gs). @@ -39,11 +39,11 @@ To convert an image, the basic command is: python convert.py [] ``` where -* `mode` is `dhr` for Double Hi-Res (560x192), or `shr` for Super Hi-Res (320x200) +* `mode` is `dhr` for Double Hi-Res Colour (560x192), `dhr_mono` for Double Hi-Res Mono (560x192), or `shr` for Super Hi-Res (320x200) * `input` is the source image file to convert (e.g. `my-image.jpg`) * `output` is the output filename to produce (e.g. `my-image.dhr`) -The following flags are supported in both `dhr` and `shr` modes: +The following flags are supported in all modes: * `--show-input` Whether to show the input image before conversion. (default: False) * `--show-output` Whether to show the output image after conversion. (default: True) @@ -158,6 +158,11 @@ python convert.py shr examples/shr/rabbit-kitten-original.png examples/shr/rabbi # Version history +## v2.1 (2023-01-21) + +* Added support for DHGR mono conversions +* Fixed compatibility with python 3.10 + ## v2.0 (2022-07-16) * Added support for Super Hi-Res 320x200 image conversions diff --git a/convert.py b/convert.py index d7b15a7..5ed13a2 100644 --- a/convert.py +++ b/convert.py @@ -79,6 +79,10 @@ def main(): "value of --palette)") dhr_parser.set_defaults(func=convert_dhr) + dhr_mono_parser = subparsers.add_parser("dhr_mono") + add_common_args(dhr_mono_parser) + dhr_mono_parser.set_defaults(func=convert_dhr_mono) + shr_parser = subparsers.add_parser("shr") add_common_args(shr_parser) shr_parser.add_argument( @@ -106,25 +110,29 @@ def prepare_image(image_filename: str, show_input: bool, screen, # Open and resize source image 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=gamma_correct)).astype(np.float32) / 255 - return rgb - + image_py.resize(image, screen.X_RES, screen.Y_RES * 2, + srgb_output=True).show() + return image_py.resize(image, screen.X_RES, screen.Y_RES, + gamma=gamma_correct) 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) + screen = screen_py.DHGRNTSCScreen(palette) + image = prepare_image(args.input, args.show_input, screen, + args.gamma_correct) + convert_dhr_py.convert(screen, image, args) + + +def convert_dhr_mono(args): + screen = screen_py.DHGRScreen() + image = prepare_image(args.input, args.show_input, screen, args.gamma_correct) + convert_dhr_py.convert_mono(screen, image, args) 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) + image = prepare_image(args.input, args.show_input, screen, args.gamma_correct) + convert_shr_py.convert(screen, image, args) if __name__ == "__main__": diff --git a/convert_dhr.py b/convert_dhr.py index 3ccf252..9ba7801 100644 --- a/convert_dhr.py +++ b/convert_dhr.py @@ -10,7 +10,27 @@ import screen as screen_py import image as image_py -def convert(screen: screen_py.DHGRScreen, rgb: np.ndarray, args): +def _output(out_image: Image, args): + 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") + + +def _write(screen: screen_py.DHGRScreen, bitmap: np.ndarray, args): + screen.pack(bitmap) + with open(args.output, "wb") as f: + f.write(bytes(screen.aux)) + f.write(bytes(screen.main)) + + +def convert(screen: screen_py.DHGRNTSCScreen, image: Image, args): + rgb = np.array(image).astype(np.float32) / 255 + # Conversion matrix from RGB to CAM16UCS colour values. Indexed by # 24-bit RGB value base_dir = os.path.dirname(__file__) @@ -24,7 +44,7 @@ def convert(screen: screen_py.DHGRScreen, rgb: np.ndarray, 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) + output_screen = screen_py.DHGRNTSCScreen(output_palette) if output_palette_name == "ntsc": output_srgb = output_screen.bitmap_to_image_ntsc(bitmap) else: @@ -34,15 +54,15 @@ def convert(screen: screen_py.DHGRScreen, rgb: np.ndarray, args): Image.fromarray(output_srgb), screen.X_RES, screen.Y_RES * 2, srgb_output=True) - if args.show_output: - out_image.show() + _output(out_image, args) + _write(screen, bitmap, args) - 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)) +def convert_mono(screen: screen_py.DHGRScreen, image: Image, args): + image = image.convert("1") + + out_image = Image.fromarray((np.array(image) * 255).astype(np.uint8)) + out_image = image_py.resize( + out_image, screen.X_RES, screen.Y_RES * 2, srgb_output=True) + + _output(out_image, args) + _write(screen, np.array(image).astype(bool), args) diff --git a/convert_shr.py b/convert_shr.py index 0a01f92..12c8bfc 100644 --- a/convert_shr.py +++ b/convert_shr.py @@ -362,7 +362,9 @@ class ClusterPalette: self._palette_lines[palette_idx] = [worst_line] -def convert(screen, rgb: np.ndarray, args): +def convert(screen, image: Image, args): + rgb = np.array(image).astype(np.float32) / 255 + # Conversion matrix from RGB to CAM16UCS colour values. Indexed by # 24-bit RGB value base_dir = os.path.dirname(__file__) diff --git a/dither_pattern.py b/dither_pattern.py index d69095b..8cb8e84 100644 --- a/dither_pattern.py +++ b/dither_pattern.py @@ -11,7 +11,7 @@ class DitherPattern: class NoDither(DitherPattern): """No dithering.""" PATTERN = np.array(((0, 0), (0, 0)), - dtype=np.float32).reshape(2, 2) / np.float(16) + dtype=np.float32).reshape(2, 2) / np.float32(16) ORIGIN = (0, 1) @@ -20,7 +20,7 @@ class FloydSteinbergDither(DitherPattern): # 0 * 7 # 3 5 1 PATTERN = np.array(((0, 0, 7), (3, 5, 1)), - dtype=np.float32).reshape(2, 3) / np.float(16) + dtype=np.float32).reshape(2, 3) / np.float32(16) ORIGIN = (0, 1) @@ -31,7 +31,7 @@ class FloydSteinbergDither2(DitherPattern): PATTERN = np.array( ((0, 0, 0, 0, 0, 7), (3, 5, 1, 0, 0, 0)), - dtype=np.float32).reshape(2, 6) / np.float(16) + dtype=np.float32).reshape(2, 6) / np.float32(16) ORIGIN = (0, 2) diff --git a/dither_shr.pyx b/dither_shr.pyx index aff880d..dc8b61e 100644 --- a/dither_shr.pyx +++ b/dither_shr.pyx @@ -315,13 +315,15 @@ cdef (unsigned char)[:, ::1] _convert_cam16ucs_to_rgb12_iigs(float[:, ::1] point rgb = colour.convert(point_cam, "CAM16UCS", "RGB").astype(np.float32) # TODO: precompute this conversion matrix since it's static. This accounts for about 10% of the CPU time here. - rgb12_iigs = np.clip( - # Convert to Rec.601 R'G'B' - colour.YCbCr_to_RGB( - # Gamma correct and convert Rec.709 R'G'B' to YCbCr - colour.RGB_to_YCbCr( - linear_to_srgb_array(rgb), K=colour.WEIGHTS_YCBCR['ITU-R BT.709']), - K=colour.WEIGHTS_YCBCR['ITU-R BT.601']), 0, 1).astype(np.float32) * 15 + rgb12_iigs = np.ascontiguousarray( + np.clip( + # Convert to Rec.601 R'G'B' + colour.YCbCr_to_RGB( + # Gamma correct and convert Rec.709 R'G'B' to YCbCr + colour.RGB_to_YCbCr( + linear_to_srgb_array(rgb), K=colour.WEIGHTS_YCBCR['ITU-R BT.709']), + K=colour.WEIGHTS_YCBCR['ITU-R BT.601']), 0, 1) + ).astype(np.float32) * 15 return np.round(rgb12_iigs).astype(np.uint8) # Wrapper around _convert_cam16ucs_to_rgb12_iigs to allow calling from python while retaining fast path for cython diff --git a/image.py b/image.py index 41680ea..622b61b 100644 --- a/image.py +++ b/image.py @@ -45,3 +45,6 @@ def resize( np.uint8)) else: return res + +def resize_mono(image: Image, x_res, y_res) -> Image: + return image.resize((x_res, y_res), Image.LANCZOS) diff --git a/precompute_conversion.py b/precompute_conversion.py index 857c715..abb3b9e 100644 --- a/precompute_conversion.py +++ b/precompute_conversion.py @@ -30,7 +30,8 @@ def main(): rgb24_to_cam16ucs = colour.convert(all_rgb24, "RGB", "CAM16UCS").astype( np.float32) del all_rgb24 - np.save("data/rgb24_to_cam16ucs.npy", rgb24_to_cam16ucs) + np.save("data/rgb24_to_cam16ucs.npy", np.ascontiguousarray( + rgb24_to_cam16ucs)) del rgb24_to_cam16ucs print("Precomputing conversion matrix from 12-bit //gs RGB to CAM16UCS " @@ -60,7 +61,8 @@ def main(): rgb12_iigs_to_cam16ucs = colour.convert( rgb12_iigs, "RGB", "CAM16UCS").astype(np.float32) del rgb12_iigs - np.save("data/rgb12_iigs_to_cam16ucs.npy", rgb12_iigs_to_cam16ucs) + np.save("data/rgb12_iigs_to_cam16ucs.npy", np.ascontiguousarray( + rgb12_iigs_to_cam16ucs)) del rgb12_iigs_to_cam16ucs diff --git a/screen.py b/screen.py index 8a51d45..681bbcc 100644 --- a/screen.py +++ b/screen.py @@ -71,10 +71,9 @@ class DHGRScreen: X_RES = 560 Y_RES = 192 - def __init__(self, palette: palette_py.Palette): + def __init__(self): self.main = np.zeros(8192, dtype=np.uint8) self.aux = np.zeros(8192, dtype=np.uint8) - self.palette = palette @staticmethod def y_to_base_addr(y: int) -> int: @@ -112,6 +111,11 @@ class DHGRScreen: self.main[addr:addr + 40] = main_col[y, :] return +class DHGRNTSCScreen(DHGRScreen): + def __init__(self, palette: palette_py.Palette): + self.palette = palette + super(DHGRNTSCScreen, self).__init__() + def bitmap_to_image_rgb(self, bitmap: np.ndarray) -> np.ndarray: """Convert our 2-bit bitmap image into a RGB image. @@ -161,7 +165,8 @@ class DHGRScreen: # Apply effect of saturation yuv_to_rgb = np.array( - ((1, 0, 0), (0, saturation, 0), (0, 0, saturation)), dtype=np.float) + ((1, 0, 0), (0, saturation, 0), (0, 0, saturation)), + dtype=np.float32) # Apply hue phase rotation yuv_to_rgb = np.matmul(np.array( ((1, 0, 0), (0, np.cos(hue), np.sin(hue)), (0, -np.sin(hue),