mirror of
https://github.com/KrisKennaway/ii-pix.git
synced 2024-06-07 15:46:54 +00:00
Unify DHR and HGR implementation in screen.py
This commit is contained in:
parent
f019823505
commit
b0aa38fe06
315
screen.py
315
screen.py
|
@ -125,41 +125,124 @@ class DHGRScreen:
|
||||||
self.main[addr:addr + 40] = main_col[y, :]
|
self.main[addr:addr + 40] = main_col[y, :]
|
||||||
return
|
return
|
||||||
|
|
||||||
class HGRScreen:
|
|
||||||
|
class NTSCScreen:
|
||||||
|
NTSC_PHASE_SHIFT = None
|
||||||
|
|
||||||
|
def _sin(self, pos):
|
||||||
|
x = pos % 12 + self.NTSC_PHASE_SHIFT * 3
|
||||||
|
return np.sin(x * 2 * np.pi / 12)
|
||||||
|
|
||||||
|
def _cos(self, pos):
|
||||||
|
x = pos % 12 + self.NTSC_PHASE_SHIFT * 3
|
||||||
|
return np.cos(x * 2 * np.pi / 12)
|
||||||
|
|
||||||
|
def _read(self, line, pos):
|
||||||
|
if pos < 0:
|
||||||
|
return 0
|
||||||
|
return 1 if line[pos] else 0
|
||||||
|
|
||||||
|
def bitmap_to_image_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
|
||||||
|
y_width = 12
|
||||||
|
u_width = 24
|
||||||
|
v_width = 24
|
||||||
|
|
||||||
|
contrast = 1
|
||||||
|
# TODO: This is necessary to match OpenEmulator. I think it is because
|
||||||
|
# they introduce an extra (unexplained) factor of 2 when applying the
|
||||||
|
# Chebyshev/Lanczos filtering to the u and v components.
|
||||||
|
saturation = 2
|
||||||
|
# TODO: this phase shift is necessary to match OpenEmulator. I'm not
|
||||||
|
# sure where it comes from - e.g. it doesn't match the phaseInfo
|
||||||
|
# calculation for the signal phase at the start of the visible region.
|
||||||
|
hue = 0.2 * (2 * np.pi)
|
||||||
|
|
||||||
|
# Apply effect of saturation
|
||||||
|
yuv_to_rgb = np.array(
|
||||||
|
((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),
|
||||||
|
np.cos(hue)))),
|
||||||
|
yuv_to_rgb)
|
||||||
|
# Y'UV to R'G'B' conversion
|
||||||
|
yuv_to_rgb = np.matmul(np.array(
|
||||||
|
((1, 0, 1.139883), (1, -0.394642, -.5806227), (1, 2.032062, 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
|
||||||
|
|
||||||
|
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 an n-bit sliding
|
||||||
|
window and x % 4, which give the index into our RGB palette.
|
||||||
|
"""
|
||||||
|
image_rgb = np.empty((self.Y_RES, self.X_RES, 3), dtype=np.uint8)
|
||||||
|
for y in range(self.Y_RES):
|
||||||
|
bitmap_window = [False] * self.palette.PALETTE_DEPTH
|
||||||
|
for x in range(self.X_RES):
|
||||||
|
# Maintain a sliding window of pixels of width PALETTE_DEPTH
|
||||||
|
bitmap_window = bitmap_window[1:] + [bitmap[y, x]]
|
||||||
|
|
||||||
|
image_rgb[y, x, :] = self.palette.RGB[
|
||||||
|
self.palette.bitmap_to_idx(
|
||||||
|
# Mapping from bit pattern to colour is rotated by
|
||||||
|
# NTSC phase shift
|
||||||
|
np.roll(
|
||||||
|
np.array(bitmap_window, dtype=bool),
|
||||||
|
self.NTSC_PHASE_SHIFT
|
||||||
|
)
|
||||||
|
), x % 4]
|
||||||
|
return image_rgb
|
||||||
|
|
||||||
|
class DHGRNTSCScreen(DHGRScreen, NTSCScreen):
|
||||||
|
def __init__(self, palette: palette_py.Palette):
|
||||||
|
self.palette = palette
|
||||||
|
super(DHGRNTSCScreen, self).__init__()
|
||||||
|
|
||||||
|
NTSC_PHASE_SHIFT = 0
|
||||||
|
|
||||||
|
|
||||||
|
class HGRScreen(NTSCScreen):
|
||||||
X_RES = 560
|
X_RES = 560
|
||||||
Y_RES = 192
|
Y_RES = 192
|
||||||
|
|
||||||
MODE = Mode.HI_RES
|
MODE = Mode.HI_RES
|
||||||
|
|
||||||
|
NTSC_PHASE_SHIFT = 3
|
||||||
|
|
||||||
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)
|
||||||
self.palette = palette
|
self.palette = palette
|
||||||
super(HGRScreen, self).__init__()
|
super(HGRScreen, self).__init__()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def y_to_base_addr(y: int) -> int:
|
|
||||||
"""Maps y coordinate to screen memory base address."""
|
|
||||||
a = y // 64
|
|
||||||
d = y - 64 * a
|
|
||||||
b = d // 8
|
|
||||||
c = d - 8 * b
|
|
||||||
|
|
||||||
return 1024 * c + 128 * b + 40 * a
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def compute_fat_pixels(screen_byte, last_pixels):
|
|
||||||
result = 0
|
|
||||||
for i in range(7):
|
|
||||||
bit = (screen_byte >> i) & 0b1
|
|
||||||
fat_bit = bit << 1 | bit
|
|
||||||
result |= fat_bit << (2 * i)
|
|
||||||
if screen_byte & 0x80:
|
|
||||||
# Palette bit shifts to the right
|
|
||||||
result <<= 1
|
|
||||||
result |= (last_pixels >> 7)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def pack_bytes(self, linear_bytemap: np.ndarray):
|
def pack_bytes(self, linear_bytemap: np.ndarray):
|
||||||
"""Packs an image into memory format (8K main)."""
|
"""Packs an image into memory format (8K main)."""
|
||||||
|
|
||||||
|
@ -167,185 +250,3 @@ class HGRScreen:
|
||||||
addr = self.y_to_base_addr(y)
|
addr = self.y_to_base_addr(y)
|
||||||
self.main[addr:addr + 40] = linear_bytemap[y, :]
|
self.main[addr:addr + 40] = linear_bytemap[y, :]
|
||||||
return
|
return
|
||||||
|
|
||||||
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 an n-bit sliding
|
|
||||||
window and x % 4, which give the index into our RGB palette.
|
|
||||||
"""
|
|
||||||
image_rgb = np.empty((self.Y_RES, self.X_RES, 3), dtype=np.uint8)
|
|
||||||
for y in range(self.Y_RES):
|
|
||||||
bitmap_window = [False] * self.palette.PALETTE_DEPTH
|
|
||||||
for x in range(self.X_RES):
|
|
||||||
# Maintain a sliding window of pixels of width PALETTE_DEPTH
|
|
||||||
bitmap_window = bitmap_window[1:] + [bitmap[y, x]]
|
|
||||||
image_rgb[y, x, :] = self.palette.RGB[
|
|
||||||
self.palette.bitmap_to_idx(
|
|
||||||
np.array(bitmap_window, dtype=bool)), x % 4]
|
|
||||||
return image_rgb
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _sin(pos, phase0=9):
|
|
||||||
x = pos % 12 + phase0
|
|
||||||
return np.sin(x * 2 * np.pi / 12)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _cos(pos, phase0=9):
|
|
||||||
x = pos % 12 + phase0
|
|
||||||
return np.cos(x * 2 * np.pi / 12)
|
|
||||||
|
|
||||||
def _read(self, line, pos):
|
|
||||||
if pos < 0:
|
|
||||||
return 0
|
|
||||||
return 1 if line[pos] else 0
|
|
||||||
|
|
||||||
def bitmap_to_image_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
|
|
||||||
y_width = 12
|
|
||||||
u_width = 24
|
|
||||||
v_width = 24
|
|
||||||
|
|
||||||
contrast = 1
|
|
||||||
# TODO: This is necessary to match OpenEmulator. I think it is because
|
|
||||||
# they introduce an extra (unexplained) factor of 2 when applying the
|
|
||||||
# Chebyshev/Lanczos filtering to the u and v components.
|
|
||||||
saturation = 2
|
|
||||||
# TODO: this phase shift is necessary to match OpenEmulator. I'm not
|
|
||||||
# sure where it comes from - e.g. it doesn't match the phaseInfo
|
|
||||||
# calculation for the signal phase at the start of the visible region.
|
|
||||||
hue = 0.2 * (2 * np.pi)
|
|
||||||
|
|
||||||
# Apply effect of saturation
|
|
||||||
yuv_to_rgb = np.array(
|
|
||||||
((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),
|
|
||||||
np.cos(hue)))),
|
|
||||||
yuv_to_rgb)
|
|
||||||
# Y'UV to R'G'B' conversion
|
|
||||||
yuv_to_rgb = np.matmul(np.array(
|
|
||||||
((1, 0, 1.139883), (1, -0.394642, -.5806227), (1, 2.032062, 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 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.
|
|
||||||
|
|
||||||
Colour at every pixel is determined by the value of an n-bit sliding
|
|
||||||
window and x % 4, which give the index into our RGB palette.
|
|
||||||
"""
|
|
||||||
image_rgb = np.empty((self.Y_RES, self.X_RES, 3), dtype=np.uint8)
|
|
||||||
for y in range(self.Y_RES):
|
|
||||||
bitmap_window = [False] * self.palette.PALETTE_DEPTH
|
|
||||||
for x in range(self.X_RES):
|
|
||||||
# Maintain a sliding window of pixels of width PALETTE_DEPTH
|
|
||||||
bitmap_window = bitmap_window[1:] + [bitmap[y, x]]
|
|
||||||
image_rgb[y, x, :] = self.palette.RGB[
|
|
||||||
self.palette.bitmap_to_idx(
|
|
||||||
np.array(bitmap_window, dtype=bool)), x % 4]
|
|
||||||
return image_rgb
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _sin(pos, phase0=0):
|
|
||||||
x = pos % 12 + phase0
|
|
||||||
return np.sin(x * 2 * np.pi / 12)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _cos(pos, phase0=0):
|
|
||||||
x = pos % 12 + phase0
|
|
||||||
return np.cos(x * 2 * np.pi / 12)
|
|
||||||
|
|
||||||
def _read(self, line, pos):
|
|
||||||
if pos < 0:
|
|
||||||
return 0
|
|
||||||
return 1 if line[pos] else 0
|
|
||||||
|
|
||||||
def bitmap_to_image_ntsc(self, bitmap: np.ndarray) -> np.ndarray:
|
|
||||||
y_width = 12
|
|
||||||
u_width = 24
|
|
||||||
v_width = 24
|
|
||||||
|
|
||||||
contrast = 1
|
|
||||||
# TODO: This is necessary to match OpenEmulator. I think it is because
|
|
||||||
# they introduce an extra (unexplained) factor of 2 when applying the
|
|
||||||
# Chebyshev/Lanczos filtering to the u and v components.
|
|
||||||
saturation = 2
|
|
||||||
# TODO: this phase shift is necessary to match OpenEmulator. I'm not
|
|
||||||
# sure where it comes from - e.g. it doesn't match the phaseInfo
|
|
||||||
# calculation for the signal phase at the start of the visible region.
|
|
||||||
hue = 0.2 * (2 * np.pi)
|
|
||||||
|
|
||||||
# Apply effect of saturation
|
|
||||||
yuv_to_rgb = np.array(
|
|
||||||
((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),
|
|
||||||
np.cos(hue)))),
|
|
||||||
yuv_to_rgb)
|
|
||||||
# Y'UV to R'G'B' conversion
|
|
||||||
yuv_to_rgb = np.matmul(np.array(
|
|
||||||
((1, 0, 1.139883), (1, -0.394642, -.5806227), (1, 2.032062, 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
Block a user