kopia lustrzana https://github.com/corrscope/corrscope
Merge pull request #22 from nyanpasu64/renderer-layout
Add renderer layout orientation field, add class LayoutConfigpull/357/head
commit
596e9812c7
|
@ -5,7 +5,7 @@ from typing import Optional, List
|
|||
from ovgenpy import outputs
|
||||
from ovgenpy.channel import Channel, ChannelConfig
|
||||
from ovgenpy.config import register_config
|
||||
from ovgenpy.renderer import MatplotlibRenderer, RendererConfig
|
||||
from ovgenpy.renderer import MatplotlibRenderer, RendererConfig, LayoutConfig
|
||||
from ovgenpy.triggers import ITriggerConfig, CorrelationTriggerConfig
|
||||
from ovgenpy.utils import keyword_dataclasses as dc
|
||||
from ovgenpy.utils.keyword_dataclasses import field
|
||||
|
@ -30,10 +30,10 @@ class Config:
|
|||
trigger: ITriggerConfig # Maybe overriden per Wave
|
||||
|
||||
amplification: float
|
||||
layout: LayoutConfig
|
||||
render: RendererConfig
|
||||
|
||||
outputs: List[outputs.IOutputConfig]
|
||||
create_window: bool = False
|
||||
|
||||
@property
|
||||
def render_width_s(self) -> float:
|
||||
|
@ -61,10 +61,8 @@ def default_config(**kwargs):
|
|||
),
|
||||
|
||||
amplification=1,
|
||||
render=RendererConfig(
|
||||
1280, 720,
|
||||
ncols=1
|
||||
),
|
||||
layout=LayoutConfig(ncols=1),
|
||||
render=RendererConfig(1280, 720),
|
||||
|
||||
outputs=[
|
||||
# outputs.FFmpegOutputConfig(output),
|
||||
|
@ -97,12 +95,11 @@ class Ovgen:
|
|||
# Calculate number of frames (TODO master file?)
|
||||
render_width_s = self.cfg.render_width_s
|
||||
fps = self.cfg.fps
|
||||
create_window = self.cfg.create_window
|
||||
|
||||
nframes = fps * self.waves[0].get_s()
|
||||
nframes = int(nframes) + 1
|
||||
|
||||
renderer = MatplotlibRenderer(self.cfg.render, self.nchan, create_window)
|
||||
renderer = MatplotlibRenderer(self.cfg.render, self.cfg.layout, self.nchan)
|
||||
|
||||
if RENDER_PROFILING:
|
||||
begin = time.perf_counter()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Optional, List, Tuple, TYPE_CHECKING
|
||||
from typing import Optional, List, TYPE_CHECKING, TypeVar, Callable
|
||||
|
||||
import matplotlib
|
||||
import numpy as np
|
||||
|
@ -22,20 +22,7 @@ class RendererConfig:
|
|||
width: int
|
||||
height: int
|
||||
|
||||
nrows: Optional[int] = None
|
||||
ncols: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.nrows:
|
||||
self.nrows = None
|
||||
if not self.ncols:
|
||||
self.ncols = None
|
||||
|
||||
if self.nrows and self.ncols:
|
||||
raise ValueError('cannot manually assign both nrows and ncols')
|
||||
|
||||
if not self.nrows and not self.ncols:
|
||||
self.ncols = 1
|
||||
create_window: bool = False
|
||||
|
||||
|
||||
class MatplotlibRenderer:
|
||||
|
@ -61,17 +48,10 @@ class MatplotlibRenderer:
|
|||
|
||||
DPI = 96
|
||||
|
||||
def __init__(self, cfg: RendererConfig, nplots: int, create_window: bool):
|
||||
def __init__(self, cfg: RendererConfig, lcfg: 'LayoutConfig', nplots: int):
|
||||
self.cfg = cfg
|
||||
self.nplots = nplots
|
||||
self.create_window = create_window
|
||||
|
||||
# Setup layout
|
||||
# "ncols=1" is good for vertical layouts.
|
||||
# But "nrows=X" is good for left-to-right grids.
|
||||
|
||||
self.nrows = 0
|
||||
self.ncols = 0
|
||||
self.layout = RendererLayout(lcfg, nplots)
|
||||
|
||||
# Flat array of nrows*ncols elements, ordered by cfg.rows_first.
|
||||
self.fig: 'Figure' = None
|
||||
|
@ -89,8 +69,6 @@ class MatplotlibRenderer:
|
|||
Outputs: self.nrows, self.ncols, self.axes
|
||||
"""
|
||||
|
||||
self.nrows, self.ncols = self._calc_layout()
|
||||
|
||||
# Create Axes
|
||||
# https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots.html
|
||||
if self.fig:
|
||||
|
@ -99,7 +77,7 @@ class MatplotlibRenderer:
|
|||
|
||||
axes2d: np.ndarray['Axes']
|
||||
self.fig, axes2d = plt.subplots(
|
||||
self.nrows, self.ncols,
|
||||
self.layout.nrows, self.layout.ncols,
|
||||
squeeze=False,
|
||||
# Remove gaps between Axes
|
||||
gridspec_kw=dict(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
|
||||
|
@ -109,11 +87,8 @@ class MatplotlibRenderer:
|
|||
for ax in axes2d.flatten():
|
||||
ax.set_axis_off()
|
||||
|
||||
# if column major:
|
||||
if self.cfg.ncols:
|
||||
axes2d = axes2d.T
|
||||
|
||||
self.axes: List['Axes'] = axes2d.flatten().tolist()[:self.nplots]
|
||||
# Generate arrangement (using nplots, cfg.orientation)
|
||||
self.axes = self.layout.arrange(lambda row, col: axes2d[row, col])
|
||||
|
||||
# Setup figure geometry
|
||||
self.fig.set_dpi(self.DPI)
|
||||
|
@ -121,29 +96,9 @@ class MatplotlibRenderer:
|
|||
self.cfg.width / self.DPI,
|
||||
self.cfg.height / self.DPI
|
||||
)
|
||||
if self.create_window:
|
||||
if self.cfg.create_window:
|
||||
plt.show(block=False)
|
||||
|
||||
def _calc_layout(self) -> Tuple[int, int]:
|
||||
"""
|
||||
Inputs: self.cfg, self.waves
|
||||
:return: (nrows, ncols)
|
||||
"""
|
||||
cfg = self.cfg
|
||||
|
||||
if cfg.nrows:
|
||||
nrows = cfg.nrows
|
||||
if nrows is None:
|
||||
raise ValueError('invalid cfg: rows_first is True and nrows is None')
|
||||
ncols = ceildiv(self.nplots, nrows)
|
||||
else:
|
||||
ncols = cfg.ncols
|
||||
if ncols is None:
|
||||
raise ValueError('invalid cfg: rows_first is False and ncols is None')
|
||||
nrows = ceildiv(self.nplots, ncols)
|
||||
|
||||
return nrows, ncols
|
||||
|
||||
def render_frame(self, datas: List[np.ndarray]) -> None:
|
||||
ndata = len(datas)
|
||||
if self.nplots != ndata:
|
||||
|
@ -189,3 +144,82 @@ class MatplotlibRenderer:
|
|||
assert buffer_rgb.size == w * h * RGB_DEPTH
|
||||
|
||||
return buffer_rgb
|
||||
|
||||
|
||||
@register_config
|
||||
class LayoutConfig:
|
||||
nrows: Optional[int] = None
|
||||
ncols: Optional[int] = None
|
||||
orientation: str = 'h'
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.nrows:
|
||||
self.nrows = None
|
||||
if not self.ncols:
|
||||
self.ncols = None
|
||||
|
||||
if self.nrows and self.ncols:
|
||||
raise ValueError('cannot manually assign both nrows and ncols')
|
||||
|
||||
if not self.nrows and not self.ncols:
|
||||
self.ncols = 1
|
||||
|
||||
|
||||
Region = TypeVar('Region')
|
||||
RegionFactory = Callable[[int, int], Region] # f(row, column) -> Region
|
||||
|
||||
|
||||
class RendererLayout:
|
||||
VALID_ORIENTATIONS = ['h', 'v']
|
||||
|
||||
def __init__(self, cfg: LayoutConfig, nplots: int):
|
||||
self.cfg = cfg
|
||||
self.nplots = nplots
|
||||
|
||||
# Setup layout
|
||||
self.nrows, self.ncols = self._calc_layout()
|
||||
|
||||
self.orientation = cfg.orientation
|
||||
if self.orientation not in self.VALID_ORIENTATIONS:
|
||||
raise ValueError(f'Invalid orientation {self.orientation} not in '
|
||||
f'{self.VALID_ORIENTATIONS}')
|
||||
|
||||
def _calc_layout(self):
|
||||
"""
|
||||
Inputs: self.cfg, self.waves
|
||||
:return: (nrows, ncols)
|
||||
"""
|
||||
cfg = self.cfg
|
||||
|
||||
if cfg.nrows:
|
||||
nrows = cfg.nrows
|
||||
if nrows is None:
|
||||
raise ValueError('invalid cfg: rows_first is True and nrows is None')
|
||||
ncols = ceildiv(self.nplots, nrows)
|
||||
else:
|
||||
ncols = cfg.ncols
|
||||
if ncols is None:
|
||||
raise ValueError('invalid cfg: rows_first is False and ncols is None')
|
||||
nrows = ceildiv(self.nplots, ncols)
|
||||
|
||||
return nrows, ncols
|
||||
|
||||
def arrange(self, region_factory: RegionFactory) -> List[Region]:
|
||||
""" Generates an array of regions.
|
||||
|
||||
index, row, column are fed into region_factory in a row-major order [row][col].
|
||||
The results are possibly reshaped into column-major order [col][row].
|
||||
"""
|
||||
nspaces = self.nrows * self.ncols
|
||||
inds = np.arange(nspaces)
|
||||
rows, cols = np.unravel_index(inds, (self.nrows, self.ncols))
|
||||
|
||||
row_col = np.array([rows, cols]).T
|
||||
regions = np.array([region_factory(*rc) for rc in row_col]) # type: np.ndarray[Region]
|
||||
regions2d = regions.reshape((self.nrows, self.ncols)) # type: np.ndarray[Region]
|
||||
|
||||
# if column major:
|
||||
if self.orientation == 'v':
|
||||
regions2d = regions2d.T
|
||||
|
||||
return regions2d.flatten()[:self.nplots].tolist()
|
||||
|
|
|
@ -2,25 +2,28 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from ovgenpy.renderer import RendererConfig, MatplotlibRenderer
|
||||
from ovgenpy.renderer import RendererConfig, MatplotlibRenderer, LayoutConfig, \
|
||||
RendererLayout
|
||||
|
||||
|
||||
WIDTH = 640
|
||||
HEIGHT = 360
|
||||
|
||||
|
||||
def test_config():
|
||||
with pytest.raises(ValueError):
|
||||
RendererConfig(WIDTH, HEIGHT, nrows=1, ncols=1)
|
||||
LayoutConfig(nrows=1, ncols=1)
|
||||
|
||||
one_col = RendererConfig(WIDTH, HEIGHT, ncols=1)
|
||||
one_col = LayoutConfig(ncols=1)
|
||||
assert one_col
|
||||
|
||||
one_row = RendererConfig(WIDTH, HEIGHT, nrows=1)
|
||||
one_row = LayoutConfig(nrows=1)
|
||||
assert one_row
|
||||
|
||||
default = RendererConfig(WIDTH, HEIGHT)
|
||||
assert default.ncols == 1 # Should default to single-column layout
|
||||
default = LayoutConfig()
|
||||
assert default.ncols == 1 # Should default to single-column layout
|
||||
assert default.nrows is None
|
||||
assert default.orientation == 'h'
|
||||
|
||||
|
||||
def test_renderer():
|
||||
|
@ -33,15 +36,59 @@ def test_renderer():
|
|||
"""
|
||||
|
||||
# 2 columns
|
||||
cfg = RendererConfig(WIDTH, HEIGHT, ncols=2)
|
||||
nplots = 16
|
||||
cfg = RendererConfig(WIDTH, HEIGHT)
|
||||
lcfg = LayoutConfig(ncols=2)
|
||||
nplots = 15
|
||||
|
||||
r = MatplotlibRenderer(cfg, nplots, create_window=False)
|
||||
r = MatplotlibRenderer(cfg, lcfg, nplots)
|
||||
|
||||
# 2 columns, 8 rows
|
||||
assert r.ncols == 2
|
||||
assert r.nrows == 8
|
||||
assert r.layout.ncols == 2
|
||||
assert r.layout.nrows == 8
|
||||
|
||||
|
||||
@pytest.mark.parametrize('lcfg', [
|
||||
LayoutConfig(ncols=2),
|
||||
LayoutConfig(nrows=8),
|
||||
])
|
||||
def test_layout(lcfg):
|
||||
nplots = 15
|
||||
layout = RendererLayout(lcfg, nplots)
|
||||
|
||||
assert layout.ncols == 2
|
||||
assert layout.nrows == 8
|
||||
|
||||
# holy shit, passing tuples into a numpy array breaks things spectacularly, and it's
|
||||
# painfully difficult to stuff tuples into 1D array.
|
||||
# http://wesmckinney.com/blog/performance-quirk-making-a-1d-object-ndarray-of-tuples/
|
||||
regions = layout.arrange(lambda row, col: str((row, col)))
|
||||
assert len(regions) == nplots
|
||||
|
||||
assert regions[0] == '(0, 0)'
|
||||
assert regions[1] == '(0, 1)'
|
||||
assert regions[2] == '(1, 0)'
|
||||
m = nplots - 1
|
||||
assert regions[m] == str((m // 2, m % 2))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('lcfg', [
|
||||
LayoutConfig(ncols=3, orientation='v'),
|
||||
LayoutConfig(nrows=3, orientation='v'),
|
||||
])
|
||||
def test_layout(lcfg):
|
||||
nplots = 7
|
||||
layout = RendererLayout(lcfg, nplots)
|
||||
|
||||
assert layout.ncols == 3
|
||||
assert layout.nrows == 3
|
||||
|
||||
regions = layout.arrange(lambda row, col: str((row, col)))
|
||||
assert len(regions) == nplots
|
||||
|
||||
assert regions[0] == '(0, 0)'
|
||||
assert regions[2] == '(2, 0)'
|
||||
assert regions[3] == '(0, 1)'
|
||||
assert regions[6] == '(0, 2)'
|
||||
|
||||
# TODO: test get_frame()
|
||||
# (integration test) ensure rendering to output works
|
||||
|
|
Ładowanie…
Reference in New Issue