From 4e1eb19a885e69071274d055721c40b48d79ed4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Sat, 27 Apr 2024 03:09:47 +0200 Subject: [PATCH] feat: Use `canvas.draw()` for plotting. The performance difference isn't as clear cut as hoped. However, the plotting procedure is enormously more straightforward, and performance is more predictable. So it's worth it. We're managing to perfectly reuse figure/canvas/axis, but still hovering at around 70-80ms. Mind you, the tested machine is an older laptop. Still, things feel interactive enough, especially together with the other modifications. To really amp it up, we can look into blitting. It requires alterations to the plotting methodology, but it offers a cached approach to drawing only altered pixels (the bottleneck with `canvas.draw()` is that it needs to render all the pixels, every time). We can also try to lower the resolution if it's too slow. --- .../managed_objs/managed_bl_image.py | 69 +++++++++---------- src/blender_maxwell/utils/image_ops.py | 22 +++++- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py index b97eced..33f5d76 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py @@ -1,7 +1,6 @@ """Declares `ManagedBLImage`.""" -import io -import time +#import time import typing as typ import bpy @@ -238,60 +237,60 @@ class ManagedBLImage(base.ManagedObj): dpi: int | None = None, bl_select: bool = False, ): - times = [time.perf_counter()] - import matplotlib.pyplot as plt + # times = [time.perf_counter()] - times.append(time.perf_counter() - times[0]) # Compute Plot Dimensions aspect_ratio, _dpi, _width_inches, _height_inches, width_px, height_px = ( self.gen_image_geometry(width_inches, height_inches, dpi) ) - times.append(time.perf_counter() - times[0]) + # times.append(['Image Geometry', time.perf_counter() - times[0]]) # Create MPL Figure, Axes, and Compute Figure Geometry - fig, ax = plt.subplots( - figsize=[_width_inches, _height_inches], - dpi=_dpi, + fig, canvas, ax = image_ops.mpl_fig_canvas_ax( + _width_inches, _height_inches, _dpi ) - times.append(time.perf_counter() - times[0]) - ax.set_aspect(aspect_ratio) - times.append(time.perf_counter() - times[0]) - cmp_width_px, cmp_height_px = fig.canvas.get_width_height() - times.append(time.perf_counter() - times[0]) - ax.set_aspect('auto') ## Workaround aspect-ratio bugs - times.append(time.perf_counter() - times[0]) + # times.append(['MPL Fig Canvas Axis', time.perf_counter() - times[0]]) + + ax.clear() + # times.append(['Clear Axis', time.perf_counter() - times[0]]) # Plot w/User Parameter func_plotter(ax) - times.append(time.perf_counter() - times[0]) + # times.append(['Plot!', time.perf_counter() - times[0]]) # Save Figure to BytesIO - with io.BytesIO() as buff: - fig.savefig(buff, format='raw', dpi=dpi) - times.append(time.perf_counter() - times[0]) - buff.seek(0) - image_data = np.frombuffer( - buff.getvalue(), - dtype=np.uint8, - ).reshape([cmp_height_px, cmp_width_px, -1]) - times.append(time.perf_counter() - times[0]) + canvas.draw() + # times.append(['Draw Pixels', time.perf_counter() - times[0]]) - image_data = np.flipud(image_data).astype(np.float32) / 255 - times.append(time.perf_counter() - times[0]) - plt.close(fig) + canvas_width_px, canvas_height_px = fig.canvas.get_width_height() + # times.append(['Get Canvas Dims', time.perf_counter() - times[0]]) + image_data = ( + np.float32( + np.flipud( + np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).reshape( + fig.canvas.get_width_height()[::-1] + (4,) + ) + ) + ) + / 255 + ) + # times.append(['Load Data from Canvas', time.perf_counter() - times[0]]) # Optimized Write to Blender Image - bl_image = self.bl_image(cmp_width_px, cmp_height_px, 'RGBA', 'uint8') - times.append(time.perf_counter() - times[0]) + bl_image = self.bl_image(canvas_width_px, canvas_height_px, 'RGBA', 'uint8') + # times.append(['Get BLImage', time.perf_counter() - times[0]]) bl_image.pixels.foreach_set(image_data.ravel()) - times.append(time.perf_counter() - times[0]) + # times.append(['Set Pixels', time.perf_counter() - times[0]]) bl_image.update() - times.append(time.perf_counter() - times[0]) + # times.append(['Update BLImage', time.perf_counter() - times[0]]) if bl_select: self.bl_select() - times.append(time.perf_counter() - times[0]) - # log.critical('Timing of MPL Plot: %s', str(times)) + # times.append(['Select BLImage', time.perf_counter() - times[0]]) + + # log.critical('Timing of MPL Plot') + # for timing in times: + # log.critical(timing) @bpy.app.handlers.persistent diff --git a/src/blender_maxwell/utils/image_ops.py b/src/blender_maxwell/utils/image_ops.py index 1fdf328..2d7e32d 100644 --- a/src/blender_maxwell/utils/image_ops.py +++ b/src/blender_maxwell/utils/image_ops.py @@ -1,6 +1,7 @@ """Useful image processing operations for use in the addon.""" import enum +import functools import time import typing as typ @@ -9,10 +10,15 @@ import jax.numpy as jnp import jaxtyping as jtyp import matplotlib import matplotlib.axis as mpl_ax +import matplotlib.backends.backend_agg +import matplotlib.figure +import matplotlib.style as mplstyle from blender_maxwell import contracts as ct from blender_maxwell.utils import logger +mplstyle.use('fast') ## TODO: Does this do anything? + log = logger.get(__name__) #################### @@ -110,6 +116,20 @@ def rgba_image_from_2d_map( return rgba_image_from_2d_map__grayscale(map_2d) +#################### +# - MPL Helpers +#################### +@functools.lru_cache(maxsize=16) +def mpl_fig_canvas_ax(width_inches: float, height_inches: float, dpi: int): + fig = matplotlib.figure.Figure(figsize=[width_inches, height_inches], dpi=dpi) + canvas = matplotlib.backends.backend_agg.FigureCanvasAgg(fig) + ax = fig.add_subplot() + + # The Customer is Always Right (in Matters of Taste) + #fig.tight_layout(pad=0) + return (fig, canvas, ax) + + #################### # - Plotters #################### @@ -230,7 +250,7 @@ def plot_heatmap_2d( y_unit = info.dim_units[y_name] heatmap = ax.imshow(data, aspect='auto', interpolation='none') - ax.figure.colorbar(heatmap, ax=ax) + #ax.figure.colorbar(heatmap, ax=ax) ax.set_title('Heatmap') ax.set_xlabel(f'{x_name}' + (f'({x_unit})' if x_unit is not None else '')) ax.set_ylabel(f'{y_name}' + (f'({y_unit})' if y_unit is not None else ''))