From b648f9755bce22836585924bf8943da1b683f254 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Wed, 15 Aug 2018 03:30:04 -0700 Subject: [PATCH 1/8] Add background/line color support to renderer --- ovgenpy/channel.py | 2 +- ovgenpy/ovgenpy.py | 1 + ovgenpy/renderer.py | 35 ++++++++++++++++++++++++++++++----- ovgenpy/util.py | 9 +++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ovgenpy/channel.py b/ovgenpy/channel.py index 38c325f..35ae84d 100644 --- a/ovgenpy/channel.py +++ b/ovgenpy/channel.py @@ -18,8 +18,8 @@ class ChannelConfig: render_width_ratio: int = 1 ampl_ratio: float = 1.0 # TODO use amplification = None instead? + bg_color: Any = None line_color: Any = None - background_color: Any = None class Channel: diff --git a/ovgenpy/ovgenpy.py b/ovgenpy/ovgenpy.py index a6ac150..9fd3111 100644 --- a/ovgenpy/ovgenpy.py +++ b/ovgenpy/ovgenpy.py @@ -100,6 +100,7 @@ class Ovgen: nframes = int(nframes) + 1 renderer = MatplotlibRenderer(self.cfg.render, self.cfg.layout, self.nchan) + renderer.set_colors(self.cfg.channels) if RENDER_PROFILING: begin = time.perf_counter() diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index 2d58397..f1dfd07 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -1,11 +1,11 @@ -from typing import Optional, List, TYPE_CHECKING, TypeVar, Callable +from typing import Optional, List, TYPE_CHECKING, TypeVar, Callable, Any import matplotlib import numpy as np from ovgenpy.config import register_config from ovgenpy.outputs import RGB_DEPTH -from ovgenpy.util import ceildiv +from ovgenpy.util import ceildiv, coalesce matplotlib.use('agg') from matplotlib import pyplot as plt @@ -15,13 +15,18 @@ if TYPE_CHECKING: from matplotlib.axes import Axes from matplotlib.figure import Figure from matplotlib.lines import Line2D + from ovgenpy.channel import ChannelConfig -@register_config +@register_config(always_dump='init_bg_color init_line_color line_width') class RendererConfig: width: int height: int + init_bg_color: Any = 'black' + init_line_color: Any = 'white' + # line_width: Optional[float] = None # TODO + create_window: bool = False @@ -58,6 +63,9 @@ class MatplotlibRenderer: self.axes: List['Axes'] = None # set by set_layout() self.lines: List['Line2D'] = None # set by render_frame() first call + self.bg_colors: List = [None] * nplots + self.line_colors: List = [None] * nplots + self._set_layout() # mutates self def _set_layout(self) -> None: @@ -85,7 +93,7 @@ class MatplotlibRenderer: # remove Axis from Axes for ax in axes2d.flatten(): - ax.set_axis_off() + ax.set_axis_off() # FIXME ax.get_xaxis().set_visible(False) # Generate arrangement (using nplots, cfg.orientation) self.axes = self.layout.arrange(lambda row, col: axes2d[row, col]) @@ -99,6 +107,16 @@ class MatplotlibRenderer: if self.cfg.create_window: plt.show(block=False) + def set_colors(self, channel_cfgs: List['ChannelConfig']): + if len(channel_cfgs) != self.nplots: + raise ValueError( + f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" + ) + self.bg_colors = [coalesce(cfg.bg_color, self.cfg.init_bg_color) + for cfg in channel_cfgs] + self.line_colors = [coalesce(cfg.line_color, self.cfg.init_line_color) + for cfg in channel_cfgs] + def render_frame(self, datas: List[np.ndarray]) -> None: ndata = len(datas) if self.nplots != ndata: @@ -109,11 +127,18 @@ class MatplotlibRenderer: if self.lines is None: self.lines = [] for idx, data in enumerate(datas): + # Setup colors + bg_color = self.bg_colors[idx] + line_color = self.line_colors[idx] + + # Setup axes ax = self.axes[idx] ax.set_xlim(0, len(data) - 1) ax.set_ylim(-1, 1) + ax.set_facecolor(bg_color) - line = ax.plot(data)[0] + # Plot line + line = ax.plot(data, color=line_color)[0] self.lines.append(line) # Draw waveform data diff --git a/ovgenpy/util.py b/ovgenpy/util.py index bb57953..5be6659 100644 --- a/ovgenpy/util.py +++ b/ovgenpy/util.py @@ -8,6 +8,15 @@ def ceildiv(n, d): return -(-n // d) +def coalesce(*args): + if len(args) == 0: + raise TypeError('coalesce expected 1 argument, got 0') + for arg in args: + if arg is not None: + return arg + return args[-1] + + T = TypeVar('T') # Adapted from https://github.com/numpy/numpy/issues/2269#issuecomment-14436725 From 09680525661579bc7d6064a76e2187a0cfbc7187 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Wed, 15 Aug 2018 17:26:20 -0700 Subject: [PATCH 2/8] Switch from per-channel to global background color Figure-global backgrounds are reasonably fast, while axes-specific background color causes performance drop. --- ovgenpy/channel.py | 1 - ovgenpy/renderer.py | 16 +++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ovgenpy/channel.py b/ovgenpy/channel.py index 35ae84d..7bceb8a 100644 --- a/ovgenpy/channel.py +++ b/ovgenpy/channel.py @@ -18,7 +18,6 @@ class ChannelConfig: render_width_ratio: int = 1 ampl_ratio: float = 1.0 # TODO use amplification = None instead? - bg_color: Any = None line_color: Any = None diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index f1dfd07..b024e5f 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -18,12 +18,13 @@ if TYPE_CHECKING: from ovgenpy.channel import ChannelConfig -@register_config(always_dump='init_bg_color init_line_color line_width') + +@register_config(always_dump='bg_color init_line_color line_width') class RendererConfig: width: int height: int - init_bg_color: Any = 'black' + bg_color: Any = 'black' init_line_color: Any = 'white' # line_width: Optional[float] = None # TODO @@ -63,7 +64,6 @@ class MatplotlibRenderer: self.axes: List['Axes'] = None # set by set_layout() self.lines: List['Line2D'] = None # set by render_frame() first call - self.bg_colors: List = [None] * nplots self.line_colors: List = [None] * nplots self._set_layout() # mutates self @@ -81,7 +81,7 @@ class MatplotlibRenderer: # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots.html if self.fig: raise Exception("I don't currently expect to call set_layout() twice") - plt.close(self.fig) + # plt.close(self.fig) axes2d: np.ndarray['Axes'] self.fig, axes2d = plt.subplots( @@ -93,7 +93,7 @@ class MatplotlibRenderer: # remove Axis from Axes for ax in axes2d.flatten(): - ax.set_axis_off() # FIXME ax.get_xaxis().set_visible(False) + ax.set_axis_off() # Generate arrangement (using nplots, cfg.orientation) self.axes = self.layout.arrange(lambda row, col: axes2d[row, col]) @@ -112,8 +112,6 @@ class MatplotlibRenderer: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) - self.bg_colors = [coalesce(cfg.bg_color, self.cfg.init_bg_color) - for cfg in channel_cfgs] self.line_colors = [coalesce(cfg.line_color, self.cfg.init_line_color) for cfg in channel_cfgs] @@ -125,17 +123,17 @@ class MatplotlibRenderer: # Initialize axes and draw waveform data if self.lines is None: + self.fig.set_facecolor(self.cfg.bg_color) + self.lines = [] for idx, data in enumerate(datas): # Setup colors - bg_color = self.bg_colors[idx] line_color = self.line_colors[idx] # Setup axes ax = self.axes[idx] ax.set_xlim(0, len(data) - 1) ax.set_ylim(-1, 1) - ax.set_facecolor(bg_color) # Plot line line = ax.plot(data, color=line_color)[0] From b0de9be500995141cf829a81422e9024e38b37a3 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Wed, 15 Aug 2018 04:22:27 -0700 Subject: [PATCH 3/8] Switch to blue line by default --- ovgenpy/renderer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index b024e5f..c0452b6 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -18,6 +18,13 @@ if TYPE_CHECKING: from ovgenpy.channel import ChannelConfig +def default_color(): + colors = np.array([int(x, 16) for x in '1f 77 b4'.split()], dtype=float) + colors /= np.amax(colors) + colors **= 1/3 + + return tuple(colors.tolist()) # tolist() converts np.float64 to float + @register_config(always_dump='bg_color init_line_color line_width') class RendererConfig: @@ -25,7 +32,7 @@ class RendererConfig: height: int bg_color: Any = 'black' - init_line_color: Any = 'white' + init_line_color: Any = default_color() # line_width: Optional[float] = None # TODO create_window: bool = False From 5f94be03291a6cf699d7a71808cacad17be9408e Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Wed, 15 Aug 2018 23:05:28 -0700 Subject: [PATCH 4/8] Add test for renderer background/foreground colors --- ovgenpy/renderer.py | 5 ++--- tests/test_renderer.py | 46 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index c0452b6..87deda6 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -119,8 +119,7 @@ class MatplotlibRenderer: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) - self.line_colors = [coalesce(cfg.line_color, self.cfg.init_line_color) - for cfg in channel_cfgs] + self.line_colors = [cfg.line_color for cfg in channel_cfgs] def render_frame(self, datas: List[np.ndarray]) -> None: ndata = len(datas) @@ -135,7 +134,7 @@ class MatplotlibRenderer: self.lines = [] for idx, data in enumerate(datas): # Setup colors - line_color = self.line_colors[idx] + line_color = coalesce(self.line_colors[idx], self.cfg.init_line_color) # Setup axes ax = self.axes[idx] diff --git a/tests/test_renderer.py b/tests/test_renderer.py index cdce799..71699fb 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -1,11 +1,11 @@ -from unittest.mock import patch - +import numpy as np import pytest +from matplotlib.colors import to_rgb +from ovgenpy.outputs import RGB_DEPTH from ovgenpy.renderer import RendererConfig, MatplotlibRenderer, LayoutConfig, \ RendererLayout - WIDTH = 640 HEIGHT = 360 @@ -90,9 +90,43 @@ def test_renderer(): assert r.layout.ncols == 2 assert r.layout.nrows == 8 -# TODO: test get_frame() -def test_colors(): - pass # TODO + +ALL_ZEROS = np.array([0,0]) + +@pytest.mark.parametrize('bg_str,fg_str', [ + ('#000000', '#ffffff'), + ('#ffffff', '#000000'), + ('#0000aa', '#aaaa00'), + ('#aaaa00', '#0000aa'), +]) +def test_colors(bg_str, fg_str): + """ Ensure the rendered background/foreground colors are correct. """ + cfg = RendererConfig( + WIDTH, + HEIGHT, + bg_color=bg_str, + init_line_color=fg_str, + # line_width=5, + ) + lcfg = LayoutConfig() + nplots = 1 + + r = MatplotlibRenderer(cfg, lcfg, nplots) + r.render_frame([ALL_ZEROS]) + frame_colors: np.ndarray = \ + np.frombuffer(r.get_frame(), dtype=np.uint8).reshape((-1, RGB_DEPTH)) + + bg_u8 = [round(c*255) for c in to_rgb(bg_str)] + fg_u8 = [round(c*255) for c in to_rgb(fg_str)] + + # Ensure background is correct + assert (frame_colors[0] == bg_u8).all() + + # Ensure foreground is present + assert np.prod(frame_colors == fg_u8, axis=-1).any() + + assert (np.amax(frame_colors, axis=0) == np.maximum(bg_u8, fg_u8)).all() + assert (np.amin(frame_colors, axis=0) == np.minimum(bg_u8, fg_u8)).all() # TODO (integration test) ensure rendering to output works From 33166f19a24b4c07057de5c17891035d453f2512 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Thu, 16 Aug 2018 01:31:06 -0700 Subject: [PATCH 5/8] Mark MatplotlibRenderer attributes as protected --- ovgenpy/renderer.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index 87deda6..92a78aa 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -67,11 +67,11 @@ class MatplotlibRenderer: self.layout = RendererLayout(lcfg, nplots) # Flat array of nrows*ncols elements, ordered by cfg.rows_first. - self.fig: 'Figure' = None - self.axes: List['Axes'] = None # set by set_layout() - self.lines: List['Line2D'] = None # set by render_frame() first call + self._fig: 'Figure' = None + self._axes: List['Axes'] = None # set by set_layout() + self._lines: List['Line2D'] = None # set by render_frame() first call - self.line_colors: List = [None] * nplots + self._line_colors: List = [None] * nplots self._set_layout() # mutates self @@ -86,12 +86,12 @@ class MatplotlibRenderer: # Create Axes # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots.html - if self.fig: + if self._fig: raise Exception("I don't currently expect to call set_layout() twice") # plt.close(self.fig) axes2d: np.ndarray['Axes'] - self.fig, axes2d = plt.subplots( + self._fig, axes2d = plt.subplots( self.layout.nrows, self.layout.ncols, squeeze=False, # Remove gaps between Axes @@ -103,11 +103,11 @@ class MatplotlibRenderer: ax.set_axis_off() # Generate arrangement (using nplots, cfg.orientation) - self.axes = self.layout.arrange(lambda row, col: axes2d[row, col]) + self._axes = self.layout.arrange(lambda row, col: axes2d[row, col]) # Setup figure geometry - self.fig.set_dpi(self.DPI) - self.fig.set_size_inches( + self._fig.set_dpi(self.DPI) + self._fig.set_size_inches( self.cfg.width / self.DPI, self.cfg.height / self.DPI ) @@ -119,7 +119,7 @@ class MatplotlibRenderer: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) - self.line_colors = [cfg.line_color for cfg in channel_cfgs] + self._line_colors = [cfg.line_color for cfg in channel_cfgs] def render_frame(self, datas: List[np.ndarray]) -> None: ndata = len(datas) @@ -128,35 +128,35 @@ class MatplotlibRenderer: f'incorrect data to plot: {self.nplots} plots but {ndata} datas') # Initialize axes and draw waveform data - if self.lines is None: - self.fig.set_facecolor(self.cfg.bg_color) + if self._lines is None: + self._fig.set_facecolor(self.cfg.bg_color) - self.lines = [] + self._lines = [] for idx, data in enumerate(datas): # Setup colors - line_color = coalesce(self.line_colors[idx], self.cfg.init_line_color) + line_color = coalesce(self._line_colors[idx], self.cfg.init_line_color) # Setup axes - ax = self.axes[idx] + ax = self._axes[idx] ax.set_xlim(0, len(data) - 1) ax.set_ylim(-1, 1) # Plot line line = ax.plot(data, color=line_color)[0] - self.lines.append(line) + self._lines.append(line) # Draw waveform data else: for idx, data in enumerate(datas): - line = self.lines[idx] + line = self._lines[idx] line.set_ydata(data) - self.fig.canvas.draw() - self.fig.canvas.flush_events() + self._fig.canvas.draw() + self._fig.canvas.flush_events() def get_frame(self) -> bytes: """ Returns ndarray of shape w,h,3. """ - canvas = self.fig.canvas + canvas = self._fig.canvas # Agg is the default noninteractive backend except on OSX. # https://matplotlib.org/faq/usage_faq.html From d94d6b235f3928b3b7c2d4f1206984bf578663fa Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Thu, 16 Aug 2018 01:34:38 -0700 Subject: [PATCH 6/8] Add line width support to MatplotlibRenderer --- ovgenpy/renderer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index 92a78aa..c89945e 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -33,7 +33,7 @@ class RendererConfig: bg_color: Any = 'black' init_line_color: Any = default_color() - # line_width: Optional[float] = None # TODO + line_width: Optional[float] = None # TODO create_window: bool = False @@ -130,6 +130,7 @@ class MatplotlibRenderer: # Initialize axes and draw waveform data if self._lines is None: self._fig.set_facecolor(self.cfg.bg_color) + line_width = self.cfg.line_width self._lines = [] for idx, data in enumerate(datas): @@ -142,7 +143,7 @@ class MatplotlibRenderer: ax.set_ylim(-1, 1) # Plot line - line = ax.plot(data, color=line_color)[0] + line = ax.plot(data, color=line_color, linewidth=line_width)[0] self._lines.append(line) # Draw waveform data From 74f47a6525a87f2eb87108d0a66f0124370864a0 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Thu, 16 Aug 2018 01:32:34 -0700 Subject: [PATCH 7/8] Add test for rendering to output --- ovgenpy/outputs.py | 7 +++++-- tests/test_renderer.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ovgenpy/outputs.py b/ovgenpy/outputs.py index 48467f1..86b35f3 100644 --- a/ovgenpy/outputs.py +++ b/ovgenpy/outputs.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Type, List, Union from ovgenpy.config import register_config +from ovgenpy.utils.keyword_dataclasses import field if TYPE_CHECKING: from ovgenpy.ovgenpy import Config @@ -100,15 +101,16 @@ class ProcessOutput(Output): # but results in slightly higher CPU consumption. self._stream.write(frame) - def close(self): + def close(self) -> int: self._stream.close() - self._popen.wait() + return self._popen.wait() # FFmpegOutput @register_config class FFmpegOutputConfig(IOutputConfig): path: str + args: str = '' # Do not use `-movflags faststart`, I get corrupted mp4 files (missing MOOV) video_template: str = '-c:v libx264 -crf 18 -preset superfast' @@ -124,6 +126,7 @@ class FFmpegOutput(ProcessOutput): ffmpeg = _FFmpegCommand([FFMPEG, '-y'], ovgen_cfg) ffmpeg.add_output(cfg) + ffmpeg.templates.append(cfg.args) self.open(ffmpeg.popen([cfg.path], self.bufsize)) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 71699fb..b739a6f 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -130,3 +130,20 @@ def test_colors(bg_str, fg_str): # TODO (integration test) ensure rendering to output works + + +def test_render_output(): + """ Ensure rendering to output does not raise exceptions. """ + + from ovgenpy.ovgenpy import default_config + from ovgenpy.outputs import FFmpegOutput, FFmpegOutputConfig + + cfg = default_config(render=RendererConfig(WIDTH, HEIGHT)) + renderer = MatplotlibRenderer(cfg.render, cfg.layout, nplots=1) + output_cfg = FFmpegOutputConfig('-', '-f nut') + out = FFmpegOutput(cfg, output_cfg) + + renderer.render_frame([ALL_ZEROS]) + out.write_frame(renderer.get_frame()) + + assert out.close() == 0 From 3324ed15688386738490b15a46bb44f96c4557ff Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Fri, 17 Aug 2018 00:00:36 -0700 Subject: [PATCH 8/8] [renderer] Test default colors, and channel line-color overrides --- ovgenpy/renderer.py | 5 +++++ tests/test_renderer.py | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/ovgenpy/renderer.py b/ovgenpy/renderer.py index c89945e..2548384 100644 --- a/ovgenpy/renderer.py +++ b/ovgenpy/renderer.py @@ -119,6 +119,11 @@ class MatplotlibRenderer: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) + + if self._lines is not None: + raise ValueError( + f'cannot set line colors after calling render_frame()' + ) self._line_colors = [cfg.line_color for cfg in channel_cfgs] def render_frame(self, datas: List[np.ndarray]) -> None: diff --git a/tests/test_renderer.py b/tests/test_renderer.py index b739a6f..a6f7b1a 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -2,6 +2,7 @@ import numpy as np import pytest from matplotlib.colors import to_rgb +from ovgenpy.channel import ChannelConfig from ovgenpy.outputs import RGB_DEPTH from ovgenpy.renderer import RendererConfig, MatplotlibRenderer, LayoutConfig, \ RendererLayout @@ -93,25 +94,55 @@ def test_renderer(): ALL_ZEROS = np.array([0,0]) -@pytest.mark.parametrize('bg_str,fg_str', [ +all_colors = pytest.mark.parametrize('bg_str,fg_str', [ ('#000000', '#ffffff'), ('#ffffff', '#000000'), ('#0000aa', '#aaaa00'), ('#aaaa00', '#0000aa'), ]) -def test_colors(bg_str, fg_str): - """ Ensure the rendered background/foreground colors are correct. """ + + +@all_colors +def test_default_colors(bg_str, fg_str): + """ Test the default background/foreground colors. """ cfg = RendererConfig( WIDTH, HEIGHT, bg_color=bg_str, init_line_color=fg_str, - # line_width=5, ) lcfg = LayoutConfig() nplots = 1 r = MatplotlibRenderer(cfg, lcfg, nplots) + verify(r, bg_str, fg_str) + + # Ensure default ChannelConfig(line_color=None) does not override line color + r = MatplotlibRenderer(cfg, lcfg, nplots) + chan = ChannelConfig(wav_path='') + r.set_colors([chan] * nplots) + verify(r, bg_str, fg_str) + + +@all_colors +def test_line_colors(bg_str, fg_str): + """ Test channel-specific line color overrides """ + cfg = RendererConfig( + WIDTH, + HEIGHT, + bg_color=bg_str, + init_line_color='#888888', + ) + lcfg = LayoutConfig() + nplots = 1 + + r = MatplotlibRenderer(cfg, lcfg, nplots) + chan = ChannelConfig(wav_path='', line_color=fg_str) + r.set_colors([chan] * nplots) + verify(r, bg_str, fg_str) + + +def verify(r: MatplotlibRenderer, bg_str, fg_str): r.render_frame([ALL_ZEROS]) frame_colors: np.ndarray = \ np.frombuffer(r.get_frame(), dtype=np.uint8).reshape((-1, RGB_DEPTH))