Tidy up a bit to prepare for merge
This commit is contained in:
parent
d5bd173345
commit
467a0cd196
73
convert.py
73
convert.py
|
@ -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()
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
120
screen.py
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue