mirror of
https://github.com/KrisKennaway/ii-pix.git
synced 2024-06-08 06:29:31 +00:00
Get rid of support for 140px mode, it was only useful as a demo of why
other converters have the wrong basic approach.
This commit is contained in:
parent
d442baf1f1
commit
8cfee55b1d
33
convert.py
33
convert.py
|
@ -40,16 +40,6 @@ def main():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--show-output', action=argparse.BooleanOptionalAction, default=True,
|
'--show-output', action=argparse.BooleanOptionalAction, default=True,
|
||||||
help="Whether to show the output image after conversion.")
|
help="Whether to show the output image after conversion.")
|
||||||
parser.add_argument(
|
|
||||||
'--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 (default: 560)")
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--palette', type=str, choices=list(set(palette_py.PALETTES.keys())),
|
'--palette', type=str, choices=list(set(palette_py.PALETTES.keys())),
|
||||||
default=palette_py.DEFAULT_PALETTE,
|
default=palette_py.DEFAULT_PALETTE,
|
||||||
|
@ -72,19 +62,12 @@ def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
palette = palette_py.PALETTES[args.palette]()
|
palette = palette_py.PALETTES[args.palette]()
|
||||||
if args.resolution == "140":
|
if args.palette == "ntsc":
|
||||||
if args.palette == "ntsc":
|
# TODO: palette depth should be controlled by Palette not Screen
|
||||||
raise argparse.ArgumentError(
|
screen = screen_py.DHGR560NTSCScreen(palette)
|
||||||
"--resolution=140 cannot be combined with --palette=ntsc")
|
|
||||||
screen = screen_py.DHGR140Screen(palette)
|
|
||||||
lookahead = 0
|
|
||||||
else:
|
else:
|
||||||
if args.palette == "ntsc":
|
screen = screen_py.DHGR560Screen(palette)
|
||||||
# TODO: palette depth should be controlled by Palette not Screen
|
lookahead = args.lookahead
|
||||||
screen = screen_py.DHGR560NTSCScreen(palette)
|
|
||||||
else:
|
|
||||||
screen = screen_py.DHGR560Screen(palette)
|
|
||||||
lookahead = args.lookahead
|
|
||||||
|
|
||||||
# Conversion matrix from RGB to CAM16UCS colour values. Indexed by
|
# Conversion matrix from RGB to CAM16UCS colour values. Indexed by
|
||||||
# 24-bit RGB value
|
# 24-bit RGB value
|
||||||
|
@ -93,7 +76,7 @@ def main():
|
||||||
# Open and resize source image
|
# Open and resize source image
|
||||||
image = image_py.open(args.input)
|
image = image_py.open(args.input)
|
||||||
if args.show_input:
|
if args.show_input:
|
||||||
image_py.resize(image, screen.NATIVE_X_RES, screen.NATIVE_Y_RES * 2,
|
image_py.resize(image, screen.X_RES, screen.Y_RES * 2,
|
||||||
srgb_output=True).show()
|
srgb_output=True).show()
|
||||||
rgb = np.array(
|
rgb = np.array(
|
||||||
image_py.resize(image, screen.X_RES, screen.Y_RES,
|
image_py.resize(image, screen.X_RES, screen.Y_RES,
|
||||||
|
@ -114,8 +97,8 @@ def main():
|
||||||
output_rgb = output_screen.bitmap_to_image_rgb(bitmap)
|
output_rgb = output_screen.bitmap_to_image_rgb(bitmap)
|
||||||
out_image = Image.fromarray(image_py.linear_to_srgb(output_rgb).astype(
|
out_image = Image.fromarray(image_py.linear_to_srgb(output_rgb).astype(
|
||||||
np.uint8))
|
np.uint8))
|
||||||
out_image = image_py.resize(out_image, screen.NATIVE_X_RES,
|
out_image = image_py.resize(out_image, screen.X_RES, screen.Y_RES * 2,
|
||||||
screen.NATIVE_Y_RES * 2, srgb_output=True)
|
srgb_output=True)
|
||||||
|
|
||||||
if args.show_output:
|
if args.show_output:
|
||||||
out_image.show()
|
out_image.show()
|
||||||
|
|
103
dither.pyx
103
dither.pyx
|
@ -89,7 +89,7 @@ cdef inline unsigned char shift_pixel_window(
|
||||||
cdef int dither_lookahead(Dither* dither, float[:, :, ::1] palette_cam16, float[:, :, ::1] palette_rgb,
|
cdef int dither_lookahead(Dither* dither, float[:, :, ::1] palette_cam16, float[:, :, ::1] palette_rgb,
|
||||||
float[:, :, ::1] image_rgb, int x, int y, int lookahead, unsigned char last_pixels,
|
float[:, :, ::1] image_rgb, int x, int y, int lookahead, unsigned char last_pixels,
|
||||||
int x_res, float[:,::1] rgb_to_cam16ucs, unsigned char palette_depth) nogil:
|
int x_res, float[:,::1] rgb_to_cam16ucs, unsigned char palette_depth) nogil:
|
||||||
cdef int i, j, k
|
cdef int candidate_pixels, i, j
|
||||||
cdef float[3] quant_error
|
cdef float[3] quant_error
|
||||||
cdef int best
|
cdef int best
|
||||||
cdef float best_error = 2**31-1
|
cdef float best_error = 2**31-1
|
||||||
|
@ -110,41 +110,42 @@ cdef int dither_lookahead(Dither* dither, float[:, :, ::1] palette_cam16, float[
|
||||||
# given pixel (dependent on the state already chosen for pixels to the left), we need to look beyond local minima.
|
# given pixel (dependent on the state already chosen for pixels to the left), we need to look beyond local minima.
|
||||||
# i.e. it might be better to make a sub-optimal choice for this pixel if it allows access to much better pixel
|
# i.e. it might be better to make a sub-optimal choice for this pixel if it allows access to much better pixel
|
||||||
# colours at later positions.
|
# colours at later positions.
|
||||||
for i in range(1 << lookahead):
|
for candidate_pixels in range(1 << lookahead):
|
||||||
# Working copy of input pixels
|
# Working copy of input pixels
|
||||||
for j in range(xxr - x):
|
for i in range(xxr - x):
|
||||||
for k in range(3):
|
for j in range(3):
|
||||||
lah_image_rgb[j * lah_shape2 + k] = image_rgb[y, x+j, k]
|
lah_image_rgb[i * lah_shape2 + j] = image_rgb[y, x+i, j]
|
||||||
|
|
||||||
total_error = 0
|
total_error = 0
|
||||||
for j in range(xxr - x):
|
# Apply dithering to lookahead horizon or edge of screen
|
||||||
xl = dither_bounds_xl(dither, j)
|
for i in range(xxr - x):
|
||||||
xr = dither_bounds_xr(dither, xxr - x, j)
|
xl = dither_bounds_xl(dither, i)
|
||||||
phase = (x + j) % 4
|
xr = dither_bounds_xr(dither, xxr - x, i)
|
||||||
|
phase = (x + i) % 4
|
||||||
|
|
||||||
next_pixels = shift_pixel_window(
|
next_pixels = shift_pixel_window(
|
||||||
last_pixels, next_pixels=i, shift_right_by=j+1, window_width=palette_depth)
|
last_pixels, next_pixels=candidate_pixels, shift_right_by=i+1, window_width=palette_depth)
|
||||||
|
|
||||||
# We don't update the input at position x (since we've already chosen
|
# We don't update the input at position x (since we've already chosen fixed outputs), but we do propagate
|
||||||
# fixed outputs), but we do propagate quantization errors to positions >x
|
# quantization errors to positions >x so we can compensate for how good/bad these choices were. i.e. the
|
||||||
# so we can compensate for how good/bad these choices were. i.e. the
|
# next_pixels choices are fixed, but we can still distribute quantization error from having made these
|
||||||
# options_rgb choices are fixed, but we can still distribute quantization error
|
# choices, in order to compute the total error.
|
||||||
# from having made these choices, in order to compute the total error.
|
for j in range(3):
|
||||||
for k in range(3):
|
quant_error[j] = lah_image_rgb[i * lah_shape2 + j] - palette_rgb[next_pixels, phase, j]
|
||||||
quant_error[k] = lah_image_rgb[j * lah_shape2 + k] - palette_rgb[next_pixels, phase, k]
|
apply_one_line(dither, xl, xr, i, lah_image_rgb, lah_shape2, quant_error)
|
||||||
apply_one_line(dither, xl, xr, j, lah_image_rgb, lah_shape2, quant_error)
|
|
||||||
|
|
||||||
lah_cam16ucs = convert_rgb_to_cam16ucs(
|
lah_cam16ucs = convert_rgb_to_cam16ucs(
|
||||||
rgb_to_cam16ucs, lah_image_rgb[j*lah_shape2], lah_image_rgb[j*lah_shape2+1],
|
rgb_to_cam16ucs, lah_image_rgb[i*lah_shape2], lah_image_rgb[i*lah_shape2+1],
|
||||||
lah_image_rgb[j*lah_shape2+2])
|
lah_image_rgb[i*lah_shape2+2])
|
||||||
total_error += colour_distance_squared(lah_cam16ucs, palette_cam16[next_pixels, phase])
|
total_error += colour_distance_squared(lah_cam16ucs, palette_cam16[next_pixels, phase])
|
||||||
|
|
||||||
if total_error >= best_error:
|
if total_error >= best_error:
|
||||||
|
# No need to continue
|
||||||
break
|
break
|
||||||
|
|
||||||
if total_error < best_error:
|
if total_error < best_error:
|
||||||
best_error = total_error
|
best_error = total_error
|
||||||
best = i
|
best = candidate_pixels
|
||||||
|
|
||||||
free(lah_image_rgb)
|
free(lah_image_rgb)
|
||||||
return best
|
return best
|
||||||
|
@ -215,36 +216,6 @@ cdef void apply(Dither* dither, int x_res, int y_res, int x, int y, float[:,:,::
|
||||||
for k in range(3):
|
for k in range(3):
|
||||||
image[i,j,k] = clip(image[i,j,k] + error_fraction * quant_error[k], 0, 1)
|
image[i,j,k] = clip(image[i,j,k] + error_fraction * quant_error[k], 0, 1)
|
||||||
|
|
||||||
# Compute closest colour from array of candidate n-bit colour palette values.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# pixel_rgb: source RGB colour value to be matched
|
|
||||||
# options_nbit: array of candidate n-bit colour palette values
|
|
||||||
# distances: matrix of (24-bit RGB value, n-bit colour value) perceptual colour differences
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# index of options_nbit entry having lowest distance value
|
|
||||||
#
|
|
||||||
@cython.boundscheck(False)
|
|
||||||
@cython.wraparound(False)
|
|
||||||
cdef unsigned char find_nearest_colour(float[::1] pixel_rgb, unsigned char[::1] options_nbit,
|
|
||||||
unsigned char[:, ::1] distances):
|
|
||||||
|
|
||||||
cdef int best, dist
|
|
||||||
cdef unsigned char bit4
|
|
||||||
cdef int best_dist = 2**8
|
|
||||||
cdef long flat
|
|
||||||
|
|
||||||
for i in range(options_nbit.shape[0]):
|
|
||||||
flat = (<long>pixel_rgb[0] << 16) + (<long>pixel_rgb[1] << 8) + <long>pixel_rgb[2]
|
|
||||||
bit4 = options_nbit[i]
|
|
||||||
dist = distances[flat, bit4]
|
|
||||||
if dist < best_dist:
|
|
||||||
best_dist = dist
|
|
||||||
best = i
|
|
||||||
|
|
||||||
return options_nbit[best]
|
|
||||||
|
|
||||||
|
|
||||||
# Dither a source image
|
# Dither a source image
|
||||||
#
|
#
|
||||||
|
@ -259,7 +230,8 @@ cdef unsigned char find_nearest_colour(float[::1] pixel_rgb, unsigned char[::1]
|
||||||
#
|
#
|
||||||
@cython.boundscheck(False)
|
@cython.boundscheck(False)
|
||||||
@cython.wraparound(False)
|
@cython.wraparound(False)
|
||||||
def dither_image(screen, float[:, :, ::1] image_rgb, dither, int lookahead, unsigned char verbose, float[:,::1] rgb_to_cam16ucs):
|
def dither_image(
|
||||||
|
screen, float[:, :, ::1] image_rgb, dither, int lookahead, unsigned char verbose, float[:,::1] rgb_to_cam16ucs):
|
||||||
cdef int y, x, i, j, k
|
cdef int y, x, i, j, k
|
||||||
# cdef float[3] input_pixel_rgb
|
# cdef float[3] input_pixel_rgb
|
||||||
cdef float[3] quant_error
|
cdef float[3] quant_error
|
||||||
|
@ -307,23 +279,16 @@ def dither_image(screen, float[:, :, ::1] image_rgb, dither, int lookahead, unsi
|
||||||
print("%d/%d" % (y, yres))
|
print("%d/%d" % (y, yres))
|
||||||
output_pixel_nbit = 0
|
output_pixel_nbit = 0
|
||||||
for x in range(xres):
|
for x in range(xres):
|
||||||
#for i in range(3):
|
# Compute all possible 2**N choices of n-bit pixel colours for positions x .. x + lookahead
|
||||||
# input_pixel_rgb[i] = image_rgb[y,x,i]
|
# lookahead_palette_choices_nbit = lookahead_options(lookahead, output_pixel_nbit)
|
||||||
if lookahead:
|
# Apply error diffusion for each of these 2**N choices, and compute which produces the closest match
|
||||||
# Compute all possible 2**N choices of n-bit pixel colours for positions x .. x + lookahead
|
# to the source image over the succeeding N pixels
|
||||||
# lookahead_palette_choices_nbit = lookahead_options(lookahead, output_pixel_nbit)
|
best_next_pixels = dither_lookahead(
|
||||||
# Apply error diffusion for each of these 2**N choices, and compute which produces the closest match
|
&cdither, palette_cam16, palette_rgb, image_rgb, x, y, lookahead, output_pixel_nbit, xres,
|
||||||
# to the source image over the succeeding N pixels
|
rgb_to_cam16ucs, palette_depth)
|
||||||
best_next_pixels = dither_lookahead(
|
# Apply best choice for next 1 pixel
|
||||||
&cdither, palette_cam16, palette_rgb, image_rgb, x, y, lookahead, output_pixel_nbit, xres,
|
output_pixel_nbit = shift_pixel_window(
|
||||||
rgb_to_cam16ucs, palette_depth)
|
output_pixel_nbit, best_next_pixels, shift_right_by=1, window_width=palette_depth)
|
||||||
# Apply best choice for next 1 pixel
|
|
||||||
output_pixel_nbit = shift_pixel_window(
|
|
||||||
output_pixel_nbit, best_next_pixels, shift_right_by=1, window_width=palette_depth)
|
|
||||||
#else:
|
|
||||||
# # Choose the closest colour among the available n-bit palette options
|
|
||||||
# palette_choices_nbit = screen.pixel_palette_options(output_pixel_nbit, x)
|
|
||||||
# output_pixel_nbit = find_nearest_colour(input_pixel_rgb, palette_choices_nbit, distances)
|
|
||||||
|
|
||||||
# Apply error diffusion from chosen output pixel value
|
# Apply error diffusion from chosen output pixel value
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
|
206
screen.py
206
screen.py
|
@ -4,13 +4,9 @@ import numpy as np
|
||||||
import palette as palette_py
|
import palette as palette_py
|
||||||
|
|
||||||
|
|
||||||
class Screen:
|
class DHGRScreen:
|
||||||
X_RES = None
|
X_RES = 560
|
||||||
Y_RES = None
|
Y_RES = 192
|
||||||
X_PIXEL_WIDTH = None
|
|
||||||
|
|
||||||
NATIVE_X_RES = 560
|
|
||||||
NATIVE_Y_RES = 192
|
|
||||||
|
|
||||||
def __init__(self, palette: palette_py.Palette):
|
def __init__(self, palette: palette_py.Palette):
|
||||||
self.main = np.zeros(8192, dtype=np.uint8)
|
self.main = np.zeros(8192, dtype=np.uint8)
|
||||||
|
@ -42,9 +38,9 @@ class Screen:
|
||||||
# PBBBAAAA PDDCCCCB PFEEEEDD PGGGGFFF
|
# PBBBAAAA PDDCCCCB PFEEEEDD PGGGGFFF
|
||||||
# Aux N Main N Aux N+1 Main N+1 (N even)
|
# Aux N Main N Aux N+1 Main N+1 (N even)
|
||||||
main_col = np.zeros(
|
main_col = np.zeros(
|
||||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8)
|
(self.Y_RES, self.X_RES // 14), dtype=np.uint8)
|
||||||
aux_col = np.zeros(
|
aux_col = np.zeros(
|
||||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8)
|
(self.Y_RES, self.X_RES // 14), dtype=np.uint8)
|
||||||
for byte_offset in range(80):
|
for byte_offset in range(80):
|
||||||
column = np.zeros(self.Y_RES, dtype=np.uint8)
|
column = np.zeros(self.Y_RES, dtype=np.uint8)
|
||||||
for bit in range(7):
|
for bit in range(7):
|
||||||
|
@ -69,19 +65,62 @@ class Screen:
|
||||||
window indexed by x % 4, which gives the index into our 16-colour RGB
|
window indexed by x % 4, which gives the index into our 16-colour RGB
|
||||||
palette.
|
palette.
|
||||||
"""
|
"""
|
||||||
image_rgb = np.empty((self.NATIVE_Y_RES, self.NATIVE_X_RES, 3),
|
image_rgb = np.empty((self.Y_RES, self.X_RES, 3),
|
||||||
dtype=np.uint8)
|
dtype=np.uint8)
|
||||||
for y in range(self.Y_RES):
|
for y in range(self.Y_RES):
|
||||||
pixel = [False, False, False, False]
|
pixel = [False, False, False, False]
|
||||||
for x in range(self.NATIVE_X_RES):
|
for x in range(self.X_RES):
|
||||||
pixel[x % 4] = bitmap[y, x]
|
pixel[x % 4] = bitmap[y, x]
|
||||||
dots = self.palette.DOTS_TO_INDEX[tuple(pixel)]
|
dots = self.palette.DOTS_TO_INDEX[tuple(pixel)]
|
||||||
image_rgb[y, x, :] = self.palette.RGB[dots]
|
image_rgb[y, x, :] = self.palette.RGB[dots]
|
||||||
return image_rgb
|
return image_rgb
|
||||||
|
|
||||||
def pixel_palette_options(self, last_pixel_nbit, x: int):
|
|
||||||
"""Returns available colours for given x pos and n-bit colour of x-1"""
|
class DHGR560Screen(DHGRScreen):
|
||||||
raise NotImplementedError
|
"""DHGR screen including colour fringing and 4 pixel chroma bleed."""
|
||||||
|
|
||||||
|
def _image_to_bitmap(self, image_nbit: np.ndarray) -> np.ndarray:
|
||||||
|
bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool)
|
||||||
|
for y in range(self.Y_RES):
|
||||||
|
for x in range(self.X_RES):
|
||||||
|
pixel = image_nbit[y, x]
|
||||||
|
dots = self.palette.DOTS[pixel]
|
||||||
|
phase = x % 4
|
||||||
|
bitmap[y, x] = dots[phase]
|
||||||
|
return bitmap
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: refactor to share implementation with DHGR560Screen
|
||||||
|
class DHGR560NTSCScreen(DHGRScreen):
|
||||||
|
"""DHGR screen including colour fringing and 8 pixel chroma bleed."""
|
||||||
|
|
||||||
|
# XXX image_nbit is MSB (x, ... x-7) LSB pixel values?
|
||||||
|
|
||||||
|
def _image_to_bitmap(self, image_nbit: np.ndarray) -> np.ndarray:
|
||||||
|
bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool)
|
||||||
|
for y in range(self.Y_RES):
|
||||||
|
for x in range(self.X_RES):
|
||||||
|
pixel = image_nbit[y, x]
|
||||||
|
bitmap[y, x] = pixel >> 7
|
||||||
|
return bitmap
|
||||||
|
|
||||||
|
# TODO: unify with parent
|
||||||
|
def bitmap_to_image_rgb(self, bitmap: np.ndarray) -> np.ndarray:
|
||||||
|
"""Convert our 2-bit bitmap image into a RGB image.
|
||||||
|
|
||||||
|
Colour at every pixel is determined by the value of a 8-bit sliding
|
||||||
|
window indexed by x % 4, which gives the index into our 256-colour RGB
|
||||||
|
palette.
|
||||||
|
"""
|
||||||
|
image_rgb = np.empty((self.Y_RES, self.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(self.X_RES):
|
||||||
|
pixel = pixel[1:] + [bitmap[y, x]]
|
||||||
|
dots = self.palette.DOTS_TO_INDEX[tuple(pixel)]
|
||||||
|
image_rgb[y, x, :] = self.palette.RGB[dots, x % 4]
|
||||||
|
return image_rgb
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sin(pos, phase0=0):
|
def _sin(pos, phase0=0):
|
||||||
|
@ -99,145 +138,6 @@ class Screen:
|
||||||
|
|
||||||
return 1 if line[pos] else 0
|
return 1 if line[pos] else 0
|
||||||
|
|
||||||
|
|
||||||
class DHGR140Screen(Screen):
|
|
||||||
"""DHGR screen ignoring colour fringing, i.e. treating as 140x192x16."""
|
|
||||||
|
|
||||||
X_RES = 140
|
|
||||||
Y_RES = 192
|
|
||||||
X_PIXEL_WIDTH = 4
|
|
||||||
|
|
||||||
def _image_to_bitmap(self, image_nbit: np.ndarray) -> np.ndarray:
|
|
||||||
bitmap = np.zeros(
|
|
||||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH), dtype=np.bool)
|
|
||||||
for y in range(self.Y_RES):
|
|
||||||
for x in range(self.X_RES):
|
|
||||||
pixel = image_nbit[y, x]
|
|
||||||
dots = self.palette.DOTS[pixel]
|
|
||||||
bitmap[y, x * self.X_PIXEL_WIDTH:(
|
|
||||||
(x + 1) * self.X_PIXEL_WIDTH)] = dots
|
|
||||||
return bitmap
|
|
||||||
|
|
||||||
def pixel_palette_options(self, last_pixel_nbit, x: int):
|
|
||||||
# All 16 colour choices are available at every x position.
|
|
||||||
return np.array(list(self.palette.RGB.keys()), dtype=np.uint8)
|
|
||||||
|
|
||||||
|
|
||||||
class DHGR560Screen(Screen):
|
|
||||||
"""DHGR screen including colour fringing and 4 pixel chroma bleed."""
|
|
||||||
X_RES = 560
|
|
||||||
Y_RES = 192
|
|
||||||
X_PIXEL_WIDTH = 1
|
|
||||||
|
|
||||||
def _image_to_bitmap(self, image_nbit: np.ndarray) -> np.ndarray:
|
|
||||||
bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool)
|
|
||||||
for y in range(self.Y_RES):
|
|
||||||
for x in range(self.X_RES):
|
|
||||||
pixel = image_nbit[y, x]
|
|
||||||
dots = self.palette.DOTS[pixel]
|
|
||||||
phase = x % 4
|
|
||||||
bitmap[y, x] = dots[phase]
|
|
||||||
return bitmap
|
|
||||||
|
|
||||||
def pixel_palette_options(self, last_pixel_nbit, x: int):
|
|
||||||
last_dots = self.palette.DOTS[last_pixel_nbit][1:] + [None]
|
|
||||||
|
|
||||||
# rearrange into palette order
|
|
||||||
next_dots = [None] * 8
|
|
||||||
for i in range(4):
|
|
||||||
next_dots[(i - x) % 4] = last_dots[i]
|
|
||||||
next_dots[(i - x) % 4 + 4] = last_dots[i + 4]
|
|
||||||
|
|
||||||
# XXX wrong
|
|
||||||
|
|
||||||
assert next_dots[(3 - x) % 4 + 4] is None
|
|
||||||
# print(x, last_dots, next_dots)
|
|
||||||
|
|
||||||
next_dots[(3 - x) % 4 + 4] = False
|
|
||||||
next_pixel_nbit_0 = self.palette.DOTS_TO_INDEX[next_dots]
|
|
||||||
|
|
||||||
next_dots[(3 - x) % 4 + 4] = True
|
|
||||||
next_pixel_nbit_1 = self.palette.DOTS_TO_INDEX[next_dots]
|
|
||||||
return np.array([next_pixel_nbit_0, next_pixel_nbit_1], dtype=np.uint8)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: refactor to share implementation with DHGR560Screen
|
|
||||||
class DHGR560NTSCScreen(Screen):
|
|
||||||
"""DHGR screen including colour fringing and 8 pixel chroma bleed."""
|
|
||||||
X_RES = 560
|
|
||||||
Y_RES = 192
|
|
||||||
X_PIXEL_WIDTH = 1
|
|
||||||
|
|
||||||
def _image_to_bitmap(self, image_nbit: np.ndarray) -> np.ndarray:
|
|
||||||
bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool)
|
|
||||||
for y in range(self.Y_RES):
|
|
||||||
for x in range(self.X_RES):
|
|
||||||
pixel = image_nbit[y, x]
|
|
||||||
#dots = self.palette.DOTS[pixel]
|
|
||||||
#phase = x % 4
|
|
||||||
bitmap[y, x] = pixel >> 7 # dots[4 + phase]
|
|
||||||
return bitmap
|
|
||||||
|
|
||||||
def bitmap_to_image_rgb(self, bitmap: np.ndarray) -> np.ndarray:
|
|
||||||
"""Convert our 2-bit bitmap image into a RGB image.
|
|
||||||
|
|
||||||
Colour at every pixel is determined by the value of a 8-bit sliding
|
|
||||||
window indexed by x % 4, which gives the index into our 256-colour RGB
|
|
||||||
palette.
|
|
||||||
"""
|
|
||||||
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(self.NATIVE_X_RES):
|
|
||||||
# pixel[x % 4] = pixel[x % 4 + 4]
|
|
||||||
# pixel[x % 4 + 4] = bitmap[y, x]
|
|
||||||
pixel = pixel[1:] + [bitmap[y, x]]
|
|
||||||
dots = self.palette.DOTS_TO_INDEX[tuple(pixel)]
|
|
||||||
image_rgb[y, x, :] = self.palette.RGB[dots, x % 4]
|
|
||||||
return image_rgb
|
|
||||||
|
|
||||||
def pixel_palette_options(self, last_pixel_nbit):
|
|
||||||
# # The two available 8-bit pixel colour choices are given by:
|
|
||||||
# # - Rotating the pixel value from the current x % 4 + 4 position to
|
|
||||||
# # x % 4
|
|
||||||
# # - choosing 0 and 1 for the new values of x % 4 + 4
|
|
||||||
# next_dots0 = list(self.palette.DOTS[last_pixel_nbit])
|
|
||||||
# next_dots1 = list(next_dots0)
|
|
||||||
# next_dots0[x % 4] = next_dots0[x % 4 + 4]
|
|
||||||
# next_dots0[x % 4 + 4] = False
|
|
||||||
# next_dots1[x % 4] = next_dots1[x % 4 + 4]
|
|
||||||
# next_dots1[x % 4 + 4] = True
|
|
||||||
# pixel_nbit_0 = self.palette.DOTS_TO_INDEX[tuple(next_dots0)]
|
|
||||||
# pixel_nbit_1 = self.palette.DOTS_TO_INDEX[tuple(next_dots1)]
|
|
||||||
# return np.array([pixel_nbit_0, pixel_nbit_1], dtype=np.uint8)
|
|
||||||
#next_dots = last_dots[1:] # list(self.palette.DOTS[
|
|
||||||
# last_pixel_nbit][1:])
|
|
||||||
#return np.array([
|
|
||||||
# self.palette.DOTS_TO_INDEX[tuple(last_dots + [False])],
|
|
||||||
# self.palette.DOTS_TO_INDEX[tuple(next_dots + [True])]],
|
|
||||||
# dtype=np.uint8)
|
|
||||||
|
|
||||||
return np.array(last_pixel_nbit >> 1, (last_pixel_nbit >> 1) + 1,
|
|
||||||
dtype=np.uint8)
|
|
||||||
|
|
||||||
# # rearrange into palette order
|
|
||||||
# next_dots = [None] * 8
|
|
||||||
# for i in range(4):
|
|
||||||
# next_dots[i] = last_dots[(i - x) % 4]
|
|
||||||
# next_dots[i + 4] = last_dots[(i - x) % 4 + 4]
|
|
||||||
#
|
|
||||||
# assert next_dots[(3 + x) % 4 + 4] is None
|
|
||||||
# # print(x, last_dots, next_dots)
|
|
||||||
#
|
|
||||||
# next_dots[(3 + x) % 4 + 4] = False
|
|
||||||
# next_pixel_nbit_0 = self.palette.DOTS_TO_INDEX[tuple(next_dots)]
|
|
||||||
#
|
|
||||||
# next_dots[(3 + x) % 4 + 4] = True
|
|
||||||
# next_pixel_nbit_1 = self.palette.DOTS_TO_INDEX[tuple(next_dots)]
|
|
||||||
# return np.array([next_pixel_nbit_0, next_pixel_nbit_1],
|
|
||||||
# dtype=np.uint8)
|
|
||||||
|
|
||||||
def bitmap_to_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
|
def bitmap_to_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
|
||||||
y_width = 12
|
y_width = 12
|
||||||
u_width = 24
|
u_width = 24
|
||||||
|
|
Loading…
Reference in New Issue
Block a user