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`."""
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

View File

@ -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 ''))