mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-08-31 14:29:14 +00:00
Rename FrameSequencer to FrameGrabber and break out into separate file.
Add a test case that the bmp2dhr output of input filenames containing '.' are handled correctly. Break out video.Mode into video_mode.VideoMode to resolve circular dependency.
This commit is contained in:
parent
fac2089206
commit
33aa4d46c4
144
transcoder/frame_grabber.py
Normal file
144
transcoder/frame_grabber.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
"""Extracts sequence of still images from input video stream."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import skvideo.io
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import screen
|
||||||
|
from video_mode import VideoMode
|
||||||
|
|
||||||
|
|
||||||
|
class FrameGrabber:
|
||||||
|
def __init__(self, mode: VideoMode):
|
||||||
|
self.video_mode = mode
|
||||||
|
self.input_frame_rate = 30
|
||||||
|
|
||||||
|
def frames(self) -> Iterator[screen.MemoryMap]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class FileFrameGrabber(FrameGrabber):
|
||||||
|
def __init__(self, filename, mode: VideoMode):
|
||||||
|
super(FileFrameGrabber, self).__init__(mode)
|
||||||
|
|
||||||
|
self.filename = filename # type: str
|
||||||
|
self._reader = skvideo.io.FFmpegReader(filename)
|
||||||
|
|
||||||
|
# Compute frame rate from input video
|
||||||
|
# TODO: possible to compute time offset for each frame instead?
|
||||||
|
data = skvideo.io.ffprobe(self.filename)['video']
|
||||||
|
rate_data = data['@r_frame_rate'].split("/") # e.g. 12000/1001
|
||||||
|
self.input_frame_rate = float(
|
||||||
|
rate_data[0]) / float(rate_data[1]) # type: float
|
||||||
|
|
||||||
|
def _frame_grabber(self) -> Iterator[Image.Image]:
|
||||||
|
for frame_array in self._reader.nextFrame():
|
||||||
|
yield Image.fromarray(frame_array)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _output_dir(filename) -> str:
|
||||||
|
return ".".join(filename.split(".")[:-1])
|
||||||
|
|
||||||
|
def frames(self) -> Iterator[screen.MemoryMap]:
|
||||||
|
"""Encode frame to HGR using bmp2dhr.
|
||||||
|
|
||||||
|
We do the encoding in a background thread to parallelize.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame_dir = self._output_dir(self.filename)
|
||||||
|
try:
|
||||||
|
os.mkdir(frame_dir)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
q = queue.Queue(maxsize=10)
|
||||||
|
|
||||||
|
def _hgr_decode(_idx, _frame):
|
||||||
|
outfile = "%s/%08dC.BIN" % (frame_dir, _idx)
|
||||||
|
bmpfile = "%s/%08d.bmp" % (frame_dir, _idx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.stat(outfile)
|
||||||
|
except FileNotFoundError:
|
||||||
|
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
||||||
|
_frame.save(bmpfile)
|
||||||
|
|
||||||
|
# TODO: parametrize palette
|
||||||
|
subprocess.call([
|
||||||
|
"/usr/local/bin/bmp2dhr", bmpfile, "hgr",
|
||||||
|
"P5",
|
||||||
|
# "P0", # Kegs32 RGB Color palette(for //gs playback)
|
||||||
|
"D9" # Buckels dither
|
||||||
|
])
|
||||||
|
|
||||||
|
os.remove(bmpfile)
|
||||||
|
|
||||||
|
_main = np.fromfile(outfile, dtype=np.uint8)
|
||||||
|
|
||||||
|
return _main, None
|
||||||
|
|
||||||
|
def _dhgr_decode(_idx, _frame):
|
||||||
|
mainfile = "%s/%08d.BIN" % (frame_dir, _idx)
|
||||||
|
auxfile = "%s/%08d.AUX" % (frame_dir, _idx)
|
||||||
|
|
||||||
|
bmpfile = "%s/%08d.bmp" % (frame_dir, _idx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.stat(mainfile)
|
||||||
|
os.stat(auxfile)
|
||||||
|
except FileNotFoundError:
|
||||||
|
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
||||||
|
_frame.save(bmpfile)
|
||||||
|
|
||||||
|
# TODO: parametrize palette
|
||||||
|
subprocess.call([
|
||||||
|
"/usr/local/bin/bmp2dhr", bmpfile, "dhgr", # "v",
|
||||||
|
"P5", # "P0", # Kegs32 RGB Color palette (for //gs
|
||||||
|
# playback)
|
||||||
|
"A", # Output separate .BIN and .AUX files
|
||||||
|
"D9" # Buckels dither
|
||||||
|
])
|
||||||
|
|
||||||
|
os.remove(bmpfile)
|
||||||
|
|
||||||
|
_main = np.fromfile(mainfile, dtype=np.uint8)
|
||||||
|
_aux = np.fromfile(auxfile, dtype=np.uint8)
|
||||||
|
|
||||||
|
return _main, _aux
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
"""Invoke bmp2dhr to encode input image frames and push to queue."""
|
||||||
|
for _idx, _frame in enumerate(self._frame_grabber()):
|
||||||
|
if self.video_mode == VideoMode.DHGR:
|
||||||
|
res = _dhgr_decode(_idx, _frame)
|
||||||
|
else:
|
||||||
|
res = _hgr_decode(_idx, _frame)
|
||||||
|
q.put(res)
|
||||||
|
|
||||||
|
q.put((None, None))
|
||||||
|
|
||||||
|
t = threading.Thread(target=worker, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
main, aux = q.get()
|
||||||
|
if main is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
main_map = screen.FlatMemoryMap(
|
||||||
|
screen_page=1, data=main).to_memory_map()
|
||||||
|
if aux is None:
|
||||||
|
aux_map = None
|
||||||
|
else:
|
||||||
|
aux_map = screen.FlatMemoryMap(
|
||||||
|
screen_page=1, data=aux).to_memory_map()
|
||||||
|
yield (main_map, aux_map)
|
||||||
|
q.task_done()
|
||||||
|
|
||||||
|
t.join()
|
25
transcoder/frame_grabber_test.py
Normal file
25
transcoder/frame_grabber_test.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frame_grabber
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileFrameGrabber(unittest.TestCase):
|
||||||
|
def test_output_dir(self):
|
||||||
|
self.assertEqual(
|
||||||
|
"/foo/bar",
|
||||||
|
frame_grabber.FileFrameGrabber._output_dir("/foo/bar.mp4")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
"/foo/bar.blee",
|
||||||
|
frame_grabber.FileFrameGrabber._output_dir("/foo/bar.blee.mp4")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
"/foo/bar blee",
|
||||||
|
frame_grabber.FileFrameGrabber._output_dir("/foo/bar blee.mp4")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@ -3,7 +3,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
import movie
|
import movie
|
||||||
import video
|
import video_mode
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Transcode videos to ][Vision format.')
|
description='Transcode videos to ][Vision format.')
|
||||||
@ -25,7 +25,7 @@ parser.add_argument(
|
|||||||
'frame rate, which may give better quality for some videos.'
|
'frame rate, which may give better quality for some videos.'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--video_mode', type=str, choices=video.Mode.__members__.keys(),
|
'--video_mode', type=str, choices=video_mode.VideoMode.__members__.keys(),
|
||||||
help='Video display mode to encode for (HGR/DHGR)'
|
help='Video display mode to encode for (HGR/DHGR)'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ def main(args):
|
|||||||
every_n_video_frames=args.every_n_video_frames,
|
every_n_video_frames=args.every_n_video_frames,
|
||||||
audio_normalization=args.audio_normalization,
|
audio_normalization=args.audio_normalization,
|
||||||
max_bytes_out=1024. * 1024 * args.max_output_mb,
|
max_bytes_out=1024. * 1024 * args.max_output_mb,
|
||||||
video_mode=video.Mode[args.video_mode]
|
video_mode=video_mode.VideoMode[args.video_mode]
|
||||||
)
|
)
|
||||||
|
|
||||||
print("Input frame rate = %f" % m.frame_sequencer.input_frame_rate)
|
print("Input frame rate = %f" % m.frame_grabber.input_frame_rate)
|
||||||
|
|
||||||
if args.output:
|
if args.output:
|
||||||
out_filename = args.output
|
out_filename = args.output
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
from typing import Iterable, Iterator
|
from typing import Iterable, Iterator
|
||||||
|
|
||||||
import audio
|
import audio
|
||||||
|
import frame_grabber
|
||||||
import machine
|
import machine
|
||||||
import opcodes
|
import opcodes
|
||||||
import video
|
import video
|
||||||
|
from video_mode import VideoMode
|
||||||
|
|
||||||
|
|
||||||
class Movie:
|
class Movie:
|
||||||
@ -14,17 +16,17 @@ class Movie:
|
|||||||
every_n_video_frames: int = 1,
|
every_n_video_frames: int = 1,
|
||||||
audio_normalization: float = None,
|
audio_normalization: float = None,
|
||||||
max_bytes_out: int = None,
|
max_bytes_out: int = None,
|
||||||
video_mode: video.Mode = video.Mode.HGR,
|
video_mode: VideoMode = VideoMode.HGR,
|
||||||
):
|
):
|
||||||
self.filename = filename # type: str
|
self.filename = filename # type: str
|
||||||
self.every_n_video_frames = every_n_video_frames # type: int
|
self.every_n_video_frames = every_n_video_frames # type: int
|
||||||
self.max_bytes_out = max_bytes_out # type: int
|
self.max_bytes_out = max_bytes_out # type: int
|
||||||
self.video_mode = video_mode # type: video.Mode
|
self.video_mode = video_mode # type: VideoMode
|
||||||
|
|
||||||
self.audio = audio.Audio(
|
self.audio = audio.Audio(
|
||||||
filename, normalization=audio_normalization) # type: audio.Audio
|
filename, normalization=audio_normalization) # type: audio.Audio
|
||||||
|
|
||||||
self.frame_sequencer = video.FileFrameSequencer(
|
self.frame_grabber = frame_grabber.FileFrameGrabber(
|
||||||
filename, mode=video_mode)
|
filename, mode=video_mode)
|
||||||
self.video = video.Video(
|
self.video = video.Video(
|
||||||
self.frame_sequencer, mode=video_mode,
|
self.frame_sequencer, mode=video_mode,
|
||||||
@ -47,7 +49,7 @@ class Movie:
|
|||||||
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
video_frames = self.frame_sequencer.frames()
|
video_frames = self.frame_grabber.frames()
|
||||||
main_seq = None
|
main_seq = None
|
||||||
aux_seq = None
|
aux_seq = None
|
||||||
|
|
||||||
@ -106,7 +108,7 @@ class Movie:
|
|||||||
if socket_pos >= 2044:
|
if socket_pos >= 2044:
|
||||||
# 2 op_ack address bytes + 2 payload bytes from ACK must
|
# 2 op_ack address bytes + 2 payload bytes from ACK must
|
||||||
# terminate 2K stream frame
|
# terminate 2K stream frame
|
||||||
if self.video_mode == video.Mode.DHGR:
|
if self.video_mode == VideoMode.DHGR:
|
||||||
# Flip-flop between MAIN and AUX banks
|
# Flip-flop between MAIN and AUX banks
|
||||||
self.aux_memory_bank = not self.aux_memory_bank
|
self.aux_memory_bank = not self.aux_memory_bank
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import enum
|
|||||||
from typing import Iterator, Tuple
|
from typing import Iterator, Tuple
|
||||||
|
|
||||||
import symbol_table
|
import symbol_table
|
||||||
|
import video_mode
|
||||||
from machine import Machine
|
from machine import Machine
|
||||||
|
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ class Header(Opcode):
|
|||||||
"""Video header opcode."""
|
"""Video header opcode."""
|
||||||
COMMAND = OpcodeCommand.HEADER
|
COMMAND = OpcodeCommand.HEADER
|
||||||
|
|
||||||
def __init__(self, mode: "video.Mode"):
|
def __init__(self, mode: video_mode.VideoMode):
|
||||||
self.video_mode = mode
|
self.video_mode = mode
|
||||||
|
|
||||||
def __data_eq__(self, other):
|
def __data_eq__(self, other):
|
||||||
|
@ -1,158 +1,16 @@
|
|||||||
"""Encode a sequence of images as an optimized stream of screen changes."""
|
"""Encode a sequence of images as an optimized stream of screen changes."""
|
||||||
|
|
||||||
import enum
|
|
||||||
import functools
|
import functools
|
||||||
import heapq
|
import heapq
|
||||||
import os
|
|
||||||
import queue
|
|
||||||
import random
|
import random
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
from typing import List, Iterator, Tuple
|
from typing import List, Iterator, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import skvideo.io
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
import opcodes
|
import opcodes
|
||||||
import screen
|
import screen
|
||||||
|
from frame_grabber import FrameGrabber
|
||||||
|
from video_mode import VideoMode
|
||||||
class Mode(enum.Enum):
|
|
||||||
HGR = 0
|
|
||||||
DHGR = 1
|
|
||||||
|
|
||||||
|
|
||||||
class FrameSequencer:
|
|
||||||
def __init__(self, mode: Mode):
|
|
||||||
self.video_mode = mode
|
|
||||||
self.input_frame_rate = 30
|
|
||||||
|
|
||||||
def frames(self) -> Iterator[screen.MemoryMap]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class FileFrameSequencer(FrameSequencer):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
filename: str,
|
|
||||||
mode: Mode = Mode.HGR,
|
|
||||||
):
|
|
||||||
super(FileFrameSequencer, self).__init__(mode)
|
|
||||||
|
|
||||||
self.filename = filename # type: str
|
|
||||||
self.mode = mode # type: Mode
|
|
||||||
|
|
||||||
self._reader = skvideo.io.FFmpegReader(filename)
|
|
||||||
|
|
||||||
# Compute frame rate from input video
|
|
||||||
# TODO: possible to compute time offset for each frame instead?
|
|
||||||
data = skvideo.io.ffprobe(self.filename)['video']
|
|
||||||
rate_data = data['@r_frame_rate'].split("/") # e.g. 12000/1001
|
|
||||||
self.input_frame_rate = float(
|
|
||||||
rate_data[0]) / float(rate_data[1]) # type: float
|
|
||||||
|
|
||||||
def _frame_grabber(self) -> Iterator[Image.Image]:
|
|
||||||
for frame_array in self._reader.nextFrame():
|
|
||||||
yield Image.fromarray(frame_array)
|
|
||||||
|
|
||||||
def frames(self) -> Iterator[screen.MemoryMap]:
|
|
||||||
"""Encode frame to HGR using bmp2dhr.
|
|
||||||
|
|
||||||
We do the encoding in a background thread to parallelize.
|
|
||||||
"""
|
|
||||||
|
|
||||||
frame_dir = ".".join(self.filename.split(".")[:-1])
|
|
||||||
try:
|
|
||||||
os.mkdir(frame_dir)
|
|
||||||
except FileExistsError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
q = queue.Queue(maxsize=10)
|
|
||||||
|
|
||||||
def _hgr_decode(_idx, _frame):
|
|
||||||
outfile = "%s/%08dC.BIN" % (frame_dir, _idx)
|
|
||||||
bmpfile = "%s/%08d.bmp" % (frame_dir, _idx)
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.stat(outfile)
|
|
||||||
except FileNotFoundError:
|
|
||||||
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
|
||||||
_frame.save(bmpfile)
|
|
||||||
|
|
||||||
# TODO: parametrize palette
|
|
||||||
subprocess.call([
|
|
||||||
"/usr/local/bin/bmp2dhr", bmpfile, "hgr",
|
|
||||||
"P5",
|
|
||||||
# "P0", # Kegs32 RGB Color palette(for //gs playback)
|
|
||||||
"D9" # Buckels dither
|
|
||||||
])
|
|
||||||
|
|
||||||
os.remove(bmpfile)
|
|
||||||
|
|
||||||
_main = np.fromfile(outfile, dtype=np.uint8)
|
|
||||||
|
|
||||||
return _main, None
|
|
||||||
|
|
||||||
def _dhgr_decode(_idx, _frame):
|
|
||||||
mainfile = "%s/%08d.BIN" % (frame_dir, _idx)
|
|
||||||
auxfile = "%s/%08d.AUX" % (frame_dir, _idx)
|
|
||||||
|
|
||||||
bmpfile = "%s/%08d.bmp" % (frame_dir, _idx)
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.stat(mainfile)
|
|
||||||
os.stat(auxfile)
|
|
||||||
except FileNotFoundError:
|
|
||||||
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
|
||||||
_frame.save(bmpfile)
|
|
||||||
|
|
||||||
# TODO: parametrize palette
|
|
||||||
subprocess.call([
|
|
||||||
"/usr/local/bin/bmp2dhr", bmpfile, "dhgr", # "v",
|
|
||||||
"P5", # "P0", # Kegs32 RGB Color palette (for //gs
|
|
||||||
# playback)
|
|
||||||
"A", # Output separate .BIN and .AUX files
|
|
||||||
"D9" # Buckels dither
|
|
||||||
])
|
|
||||||
|
|
||||||
os.remove(bmpfile)
|
|
||||||
|
|
||||||
_main = np.fromfile(mainfile, dtype=np.uint8)
|
|
||||||
_aux = np.fromfile(auxfile, dtype=np.uint8)
|
|
||||||
|
|
||||||
return _main, _aux
|
|
||||||
|
|
||||||
def worker():
|
|
||||||
"""Invoke bmp2dhr to encode input image frames and push to queue."""
|
|
||||||
for _idx, _frame in enumerate(self._frame_grabber()):
|
|
||||||
if self.video_mode == Mode.DHGR:
|
|
||||||
res = _dhgr_decode(_idx, _frame)
|
|
||||||
else:
|
|
||||||
res = _hgr_decode(_idx, _frame)
|
|
||||||
q.put(res)
|
|
||||||
|
|
||||||
q.put((None, None))
|
|
||||||
|
|
||||||
t = threading.Thread(target=worker, daemon=True)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
main, aux = q.get()
|
|
||||||
if main is None:
|
|
||||||
break
|
|
||||||
|
|
||||||
main_map = screen.FlatMemoryMap(
|
|
||||||
screen_page=1, data=main).to_memory_map()
|
|
||||||
if aux is None:
|
|
||||||
aux_map = None
|
|
||||||
else:
|
|
||||||
aux_map = screen.FlatMemoryMap(
|
|
||||||
screen_page=1, data=aux).to_memory_map()
|
|
||||||
yield (main_map, aux_map)
|
|
||||||
q.task_done()
|
|
||||||
|
|
||||||
t.join()
|
|
||||||
|
|
||||||
|
|
||||||
class Video:
|
class Video:
|
||||||
@ -162,11 +20,11 @@ class Video:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
frame_sequencer: FrameSequencer,
|
frame_grabber: FrameGrabber,
|
||||||
mode: Mode = Mode.HGR
|
mode: VideoMode = VideoMode.HGR
|
||||||
):
|
):
|
||||||
self.mode = mode # type: Mode
|
self.mode = mode # type: VideoMode
|
||||||
self.frame_sequencer = frame_sequencer # type: FrameSequencer
|
self.frame_grabber = frame_grabber # type: FrameGrabber
|
||||||
self.ticks_per_frame = (
|
self.ticks_per_frame = (
|
||||||
self.ticks_per_second / self.input_frame_rate) # type: float
|
self.ticks_per_second / self.input_frame_rate) # type: float
|
||||||
self.frame_number = 0 # type: int
|
self.frame_number = 0 # type: int
|
||||||
|
8
transcoder/video_mode.py
Normal file
8
transcoder/video_mode.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Enum representing video encoding mode."""
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class VideoMode(enum.Enum):
|
||||||
|
HGR = 0
|
||||||
|
DHGR = 1
|
@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frame_grabber
|
||||||
import screen
|
import screen
|
||||||
import video
|
import video
|
||||||
|
import video_mode
|
||||||
|
|
||||||
|
|
||||||
class TestVideo(unittest.TestCase):
|
class TestVideo(unittest.TestCase):
|
||||||
def test_diff_weights(self):
|
def test_diff_weights(self):
|
||||||
fs = video.FrameSequencer(mode=video.Mode.DHGR)
|
fs = frame_grabber.FrameGrabber(mode=video_mode.VideoMode.DHGR)
|
||||||
v = video.Video(fs, mode=video.Mode.DHGR)
|
v = video.Video(fs, mode=video_mode.VideoMode.DHGR)
|
||||||
|
|
||||||
frame = screen.MemoryMap(screen_page=1)
|
frame = screen.MemoryMap(screen_page=1)
|
||||||
frame.page_offset[0, 0] = 0b1111111
|
frame.page_offset[0, 0] = 0b1111111
|
||||||
|
Loading…
Reference in New Issue
Block a user