Tidy up a bit to prepare for merge

This commit is contained in:
kris 2021-03-15 10:45:33 +00:00
parent d5bd173345
commit 467a0cd196
4 changed files with 103 additions and 96 deletions

View File

@ -27,83 +27,82 @@ def main():
parser.add_argument(
"--lookahead", type=int, default=6,
help=("How many pixels to look ahead to compensate for NTSC colour "
"artifacts. Default: 6"))
"artifacts (default: 6)"))
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)
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. Default: "
"False")
help="Whether to show the input image before conversion.")
parser.add_argument(
'--show_output', action=argparse.BooleanOptionalAction, default=True,
help="Default: True. Whether to show the output image after "
"conversion. Default: True")
help="Whether to show the output image after conversion.")
parser.add_argument(
'--resolution', type=str, choices=("140", "560", "ntsc"), default="560",
'--resolution', type=str, choices=("140", "560"), default="560",
help=("Effective double hi-res resolution to target. '140' treats "
"pixels in groups of 4, with 16 colours that are chosen "
"independently, and ignores NTSC fringing. This is mostly only "
"useful for comparison to other 140px converters. '560' treats "
"each pixel individually, with choice of 2 colours (depending on "
"NTSC colour phase), and looking ahead over next --lookahead "
"pixels to optimize the colour sequence. 'ntsc' additionally "
"simulates the reduced bandwidth of the NTSC chroma signal, and "
"causes colours to bleed over 8 successive pixels instead of 4. "
"Default: 560")
"pixels to optimize the colour sequence (default: 560)")
)
parser.add_argument(
'--palette', type=str, choices=list(
set(palette_py.PALETTES.keys()) - {"ntsc"}),
'--palette', type=str, choices=list(set(palette_py.PALETTES.keys())),
default=palette_py.DEFAULT_PALETTE,
help="RGB colour palette to dither to. Ignored for "
"--resolution=ntsc. 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.")
help="RGB colour palette to use when --show_output (default: "
"value of --palette)")
args = parser.parse_args()
if args.resolution == "ntsc":
palette = palette_py.PALETTES["ntsc"]()
screen = screen_py.DHGR560NTSCScreen(palette)
lookahead = args.lookahead
palette = palette_py.PALETTES[args.palette]()
if args.resolution == "140":
if args.palette == "ntsc":
raise argparse.ArgumentError(
"--resolution=140 cannot be combined with --palette=ntsc")
screen = screen_py.DHGR140Screen(palette)
lookahead = 0
else:
palette = palette_py.PALETTES[args.palette]()
if args.resolution == "560":
screen = screen_py.DHGR560Screen(palette)
lookahead = args.lookahead
if args.palette == "ntsc":
# TODO: palette depth should be controlled by Palette not Screen
screen = screen_py.DHGR560NTSCScreen(palette)
else:
screen = screen_py.DHGR140Screen(palette)
lookahead = 0
screen = screen_py.DHGR560Screen(palette)
lookahead = args.lookahead
# Open and resize source image
image = image_py.open(args.input)
if args.show_input:
image_py.resize(image, 560, 384, srgb_output=True).show()
image_py.resize(image, screen.NATIVE_X_RES, screen.NATIVE_Y_RES * 2,
srgb_output=True).show()
resized = np.array(image_py.resize(image, screen.X_RES,
screen.Y_RES)).astype(np.float32)
dither = dither_pattern.PATTERNS[args.dither]()
output_4bit, _ = dither_pyx.dither_image(
output_nbit, _ = dither_pyx.dither_image(
screen, resized, dither, lookahead)
bitmap = screen.pack(output_4bit)
bitmap = screen.pack(output_nbit)
# Show output image by rendering in target palette
if args.show_palette:
output_palette = palette_py.PALETTES[args.show_palette]()
else:
output_palette = palette
if args.show_palette == 'ntsc':
output_palette_name = args.show_palette or args.palette
output_palette = palette_py.PALETTES[output_palette_name]()
if output_palette_name == "ntsc":
output_screen = screen_py.DHGR560NTSCScreen(output_palette)
else:
output_screen = screen_py.DHGR560Screen(output_palette)
output_rgb = output_screen.bitmap_to_image_rgb(bitmap)
out_image = Image.fromarray(image_py.linear_to_srgb(output_rgb).astype(
np.uint8))
out_image = image_py.resize(out_image, 560, 384, srgb_output=True)
out_image = image_py.resize(out_image, screen.NATIVE_X_RES,
screen.NATIVE_Y_RES * 2, srgb_output=True)
if args.show_output:
out_image.show()

View File

@ -7,7 +7,7 @@ import screen
def main():
s = screen.DHGR560Screen(palette=None)
s = screen.DHGR560NTSCScreen(palette=None)
bitmap = np.zeros((1, 8), dtype=np.bool)
colours = {}

View File

@ -41,6 +41,7 @@ class Palette:
class ToHgrPalette(Palette):
"""4-bit palette used as default by other DHGR image converters."""
DISTANCES_PATH = "data/distances_tohgr.data"
PALETTE_DEPTH = 4
@ -66,6 +67,7 @@ class ToHgrPalette(Palette):
class OpenEmulatorPalette(Palette):
"""4-bit palette chosen to approximately match OpenEmulator output."""
DISTANCES_PATH = "data/distances_openemulator.data"
PALETTE_DEPTH = 4
@ -91,6 +93,7 @@ class OpenEmulatorPalette(Palette):
class VirtualIIPalette(Palette):
"""4-bit palette exactly matching Virtual II emulator output."""
DISTANCES_PATH = "data/distances_virtualii.data"
PALETTE_DEPTH = 4
@ -115,6 +118,7 @@ class VirtualIIPalette(Palette):
class NTSCPalette(Palette):
"""8-bit NTSC palette computed by averaging chroma signal over 8 pixels."""
DISTANCES_PATH = 'data/distances_ntsc.data'
PALETTE_DEPTH = 8

120
screen.py
View File

@ -12,6 +12,9 @@ class Screen:
Y_RES = None
X_PIXEL_WIDTH = None
NATIVE_X_RES = 560
NATIVE_Y_RES = 192
def __init__(self, palette: palette_py.Palette):
self.main = np.zeros(8192, dtype=np.uint8)
self.aux = np.zeros(8192, dtype=np.uint8)
@ -70,10 +73,11 @@ class Screen:
window indexed by x % 4, which gives the index into our 16-colour RGB
palette.
"""
image_rgb = np.empty((192, 560, 3), dtype=np.uint8)
image_rgb = np.empty((self.NATIVE_Y_RES, self.NATIVE_X_RES, 3),
dtype=np.uint8)
for y in range(self.Y_RES):
pixel = [False, False, False, False]
for x in range(560):
for x in range(self.NATIVE_X_RES):
pixel[x % 4] = bitmap[y, x]
dots = self.palette.DOTS_TO_INDEX[tuple(pixel)]
image_rgb[y, x, :] = self.palette.RGB[dots]
@ -99,60 +103,6 @@ class Screen:
return 1 if line[pos] else 0
def bitmap_to_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
y_width = 12
u_width = 24
v_width = 24
contrast = 1
# TODO: where does this come from? OpenEmulator looks like it should
# use a value of 1.0 by default.
saturation = 2
# Fudge factor to make colours line up with OpenEmulator
# TODO: where does this come from - is it due to the band-pass
# filtering they do?
hue = -0.3
# Apply effect of saturation
yuv_to_rgb = np.array(
((1, 0, 0), (0, saturation, 0), (0, 0, saturation)), dtype=np.float)
# 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),
np.cos(hue)))),
yuv_to_rgb)
# Y'UV to R'G'B' conversion
yuv_to_rgb = np.matmul(np.array(
((1, 0, 1.13983), (1, -0.39465, -.58060), (1, 2.03211, 0))),
yuv_to_rgb)
# Apply effect of contrast
yuv_to_rgb *= contrast
out_rgb = np.empty((bitmap.shape[0], bitmap.shape[1] * 3, 3),
dtype=np.uint8)
for y in range(bitmap.shape[0]):
ysum = 0
usum = 0
vsum = 0
line = np.repeat(bitmap[y], 3)
for x in range(bitmap.shape[1] * 3):
ysum += self._read(line, x) - self._read(line, x - y_width)
usum += self._read(line, x) * self._sin(x) - self._read(
line, x - u_width) * self._sin((x - u_width))
vsum += self._read(line, x) * self._cos(x) - self._read(
line, x - v_width) * self._cos((x - v_width))
rgb = np.matmul(
yuv_to_rgb, np.array(
(ysum / y_width, usum / u_width,
vsum / v_width)).reshape((3, 1))).reshape(3)
r = min(255, max(0, rgb[0] * 255))
g = min(255, max(0, rgb[1] * 255))
b = min(255, max(0, rgb[2] * 255))
out_rgb[y, x, :] = (r, g, b)
return out_rgb
class DHGR140Screen(Screen):
"""DHGR screen ignoring colour fringing, i.e. treating as 140x192x16."""
@ -233,10 +183,10 @@ class DHGR560NTSCScreen(Screen):
window indexed by x % 4, which gives the index into our 256-colour RGB
palette.
"""
image_rgb = np.empty((192, 560, 3), dtype=np.uint8)
image_rgb = np.empty((self.NATIVE_Y_RES, self.NATIVE_X_RES, 3), dtype=np.uint8)
for y in range(self.Y_RES):
pixel = [False, False, False, False, False, False, False, False]
for x in range(560):
for x in range(self.NATIVE_X_RES):
pixel[x % 4] = pixel[x % 4 + 4]
pixel[x % 4 + 4] = bitmap[y, x]
dots = self.palette.DOTS_TO_INDEX[tuple(pixel)]
@ -260,3 +210,57 @@ class DHGR560NTSCScreen(Screen):
np.array([pixel_4bit_0, pixel_4bit_1], dtype=np.uint8),
np.array([self.palette.RGB[pixel_4bit_0],
self.palette.RGB[pixel_4bit_1]], dtype=np.uint8))
def bitmap_to_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
y_width = 12
u_width = 24
v_width = 24
contrast = 1
# TODO: where does this come from? OpenEmulator looks like it should
# use a value of 1.0 by default.
saturation = 2
# Fudge factor to make colours line up with OpenEmulator
# TODO: where does this come from - is it due to the band-pass
# filtering they do?
hue = -0.3
# Apply effect of saturation
yuv_to_rgb = np.array(
((1, 0, 0), (0, saturation, 0), (0, 0, saturation)), dtype=np.float)
# 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),
np.cos(hue)))),
yuv_to_rgb)
# Y'UV to R'G'B' conversion
yuv_to_rgb = np.matmul(np.array(
((1, 0, 1.13983), (1, -0.39465, -.58060), (1, 2.03211, 0))),
yuv_to_rgb)
# Apply effect of contrast
yuv_to_rgb *= contrast
out_rgb = np.empty((bitmap.shape[0], bitmap.shape[1] * 3, 3),
dtype=np.uint8)
for y in range(bitmap.shape[0]):
ysum = 0
usum = 0
vsum = 0
line = np.repeat(bitmap[y], 3)
for x in range(bitmap.shape[1] * 3):
ysum += self._read(line, x) - self._read(line, x - y_width)
usum += self._read(line, x) * self._sin(x) - self._read(
line, x - u_width) * self._sin((x - u_width))
vsum += self._read(line, x) * self._cos(x) - self._read(
line, x - v_width) * self._cos((x - v_width))
rgb = np.matmul(
yuv_to_rgb, np.array(
(ysum / y_width, usum / u_width,
vsum / v_width)).reshape((3, 1))).reshape(3)
r = min(255, max(0, rgb[0] * 255))
g = min(255, max(0, rgb[1] * 255))
b = min(255, max(0, rgb[2] * 255))
out_rgb[y, x, :] = (r, g, b)
return out_rgb