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.
main
Sofus Albert Høgsbro Rose 2024-04-27 03:09:47 +02:00
parent c63dda2224
commit 4e1eb19a88
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
2 changed files with 55 additions and 36 deletions

View File

@ -1,7 +1,6 @@
"""Declares `ManagedBLImage`.""" """Declares `ManagedBLImage`."""
import io #import time
import time
import typing as typ import typing as typ
import bpy import bpy
@ -238,60 +237,60 @@ class ManagedBLImage(base.ManagedObj):
dpi: int | None = None, dpi: int | None = None,
bl_select: bool = False, bl_select: bool = False,
): ):
times = [time.perf_counter()] # times = [time.perf_counter()]
import matplotlib.pyplot as plt
times.append(time.perf_counter() - times[0])
# Compute Plot Dimensions # Compute Plot Dimensions
aspect_ratio, _dpi, _width_inches, _height_inches, width_px, height_px = ( aspect_ratio, _dpi, _width_inches, _height_inches, width_px, height_px = (
self.gen_image_geometry(width_inches, height_inches, dpi) 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 # Create MPL Figure, Axes, and Compute Figure Geometry
fig, ax = plt.subplots( fig, canvas, ax = image_ops.mpl_fig_canvas_ax(
figsize=[_width_inches, _height_inches], _width_inches, _height_inches, _dpi
dpi=_dpi,
) )
times.append(time.perf_counter() - times[0]) # times.append(['MPL Fig Canvas Axis', time.perf_counter() - times[0]])
ax.set_aspect(aspect_ratio)
times.append(time.perf_counter() - times[0]) ax.clear()
cmp_width_px, cmp_height_px = fig.canvas.get_width_height() # times.append(['Clear Axis', time.perf_counter() - times[0]])
times.append(time.perf_counter() - times[0])
ax.set_aspect('auto') ## Workaround aspect-ratio bugs
times.append(time.perf_counter() - times[0])
# Plot w/User Parameter # Plot w/User Parameter
func_plotter(ax) func_plotter(ax)
times.append(time.perf_counter() - times[0]) # times.append(['Plot!', time.perf_counter() - times[0]])
# Save Figure to BytesIO # Save Figure to BytesIO
with io.BytesIO() as buff: canvas.draw()
fig.savefig(buff, format='raw', dpi=dpi) # times.append(['Draw Pixels', time.perf_counter() - times[0]])
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])
image_data = np.flipud(image_data).astype(np.float32) / 255 canvas_width_px, canvas_height_px = fig.canvas.get_width_height()
times.append(time.perf_counter() - times[0]) # times.append(['Get Canvas Dims', time.perf_counter() - times[0]])
plt.close(fig) 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 # Optimized Write to Blender Image
bl_image = self.bl_image(cmp_width_px, cmp_height_px, 'RGBA', 'uint8') bl_image = self.bl_image(canvas_width_px, canvas_height_px, 'RGBA', 'uint8')
times.append(time.perf_counter() - times[0]) # times.append(['Get BLImage', time.perf_counter() - times[0]])
bl_image.pixels.foreach_set(image_data.ravel()) 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() bl_image.update()
times.append(time.perf_counter() - times[0]) # times.append(['Update BLImage', time.perf_counter() - times[0]])
if bl_select: if bl_select:
self.bl_select() self.bl_select()
times.append(time.perf_counter() - times[0]) # times.append(['Select BLImage', time.perf_counter() - times[0]])
# log.critical('Timing of MPL Plot: %s', str(times))
# log.critical('Timing of MPL Plot')
# for timing in times:
# log.critical(timing)
@bpy.app.handlers.persistent @bpy.app.handlers.persistent

View File

@ -1,6 +1,7 @@
"""Useful image processing operations for use in the addon.""" """Useful image processing operations for use in the addon."""
import enum import enum
import functools
import time import time
import typing as typ import typing as typ
@ -9,10 +10,15 @@ import jax.numpy as jnp
import jaxtyping as jtyp import jaxtyping as jtyp
import matplotlib import matplotlib
import matplotlib.axis as mpl_ax 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 import contracts as ct
from blender_maxwell.utils import logger from blender_maxwell.utils import logger
mplstyle.use('fast') ## TODO: Does this do anything?
log = logger.get(__name__) log = logger.get(__name__)
#################### ####################
@ -110,6 +116,20 @@ def rgba_image_from_2d_map(
return rgba_image_from_2d_map__grayscale(map_2d) 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 # - Plotters
#################### ####################
@ -230,7 +250,7 @@ def plot_heatmap_2d(
y_unit = info.dim_units[y_name] y_unit = info.dim_units[y_name]
heatmap = ax.imshow(data, aspect='auto', interpolation='none') 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_title('Heatmap')
ax.set_xlabel(f'{x_name}' + (f'({x_unit})' if x_unit is not None else '')) 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 '')) ax.set_ylabel(f'{y_name}' + (f'({y_unit})' if y_unit is not None else ''))