Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Large image draw performance deterioration in recent releases #27554

Closed
ilopata1 opened this issue Dec 21, 2023 · 18 comments · Fixed by #27562
Closed

[Bug]: Large image draw performance deterioration in recent releases #27554

ilopata1 opened this issue Dec 21, 2023 · 18 comments · Fixed by #27562

Comments

@ilopata1
Copy link

ilopata1 commented Dec 21, 2023

Bug summary

Draw performance for large images has deteriorated significantly between Matplotlib 3.1 and 3.8. The biggest issue seems to arise when upgrading from 3.1. to 3.2

Code for reproduction

import cProfile
from profilehooks import profile
import tkinter as tk
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_tkagg import (FigureCanvas)
from PIL import Image
import time

class Application(tk.Frame):

        SIZE = 16000

        def __init__(self, master=None):
            
            super().__init__(master)
            self.figure = plt.figure('Diagram')
            self.canvas = FigureCanvas(self.figure, self)
            self.ax = plt.axes()
            self.canvas.mpl_connect('button_press_event', self.toggle)
            
            self.border = 0
            random_image = self.image_gen()
            self.image = self.ax.imshow(random_image)
                 
            self.canvas.get_tk_widget().pack(side=tk.TOP,fill=tk.BOTH, expand=1)
            self.pack(side=tk.TOP,fill=tk.BOTH, expand=1)

        @profile(immediate=True)
        def toggle(self, event):
            self.border = 0 if self.border == Application.SIZE /4 else Application.SIZE /4 
            self.ax.set_xlim(self.border, Application.SIZE - self.border)
            self.ax.set_ylim(self.border, Application.SIZE - self.border)
            t1= time.process_time()
            self.canvas.draw()
            print("Draw time:", time.process_time() - t1)

        def image_gen(self):
            black = np.zeros((int(Application.SIZE / 8) , int(Application.SIZE / 8), 3),dtype='uint8')
            white = np.ones((int(Application.SIZE / 8) , int(Application.SIZE / 8), 3),dtype='uint8') * 255
            col1 = np.concatenate([white, black, white, black, white, black, white, black], axis=0)
            col2  = np.concatenate([black, white, black, white, black, white, black, white], axis=0)
            output = np.concatenate([col1, col2, col1, col2,col1, col2, col1, col2], axis=1)
            im = Image.fromarray(output, 'RGB')
            return im
                      
if __name__ == '__main__':
          
    root = tk.Tk()
    app = Application(master=root)
    root.state('zoomed')
    app.mainloop()

Actual outcome

When running Matplotlib 3.8.2

         35152 function calls (34234 primitive calls) in 6.264 seconds

   Ordered by: cumulative time, internal time, call count
   List reduced from 650 to 40 due to restriction <40>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    6.264    6.264 min_code.py:29(toggle)
        1    0.000    0.000    6.255    6.255 backend_tkagg.py:9(draw)
        1    0.000    0.000    6.175    6.175 backend_agg.py:381(draw)
        1    0.000    0.000    6.168    6.168 artist.py:93(draw_wrapper)
    130/1    0.001    0.000    6.168    6.168 artist.py:54(draw_wrapper)
        1    0.000    0.000    6.168    6.168 figure.py:3134(draw)
      2/1    0.000    0.000    6.161    6.161 image.py:113(_draw_list_compositing_images)
        1    0.000    0.000    6.161    6.161 _base.py:3005(draw)
        1    0.000    0.000    6.113    6.113 image.py:623(draw)
        1    0.046    0.046    6.107    6.107 image.py:928(make_image)
        1    0.041    0.041    6.061    6.061 image.py:333(_make_image)
        2    3.877    1.939    3.877    1.939 image.py:215(_rgb_to_rgba)
        2    0.000    0.000    2.142    1.071 image.py:160(_resample)
        2    2.141    1.071    2.141    1.071 {built-in method matplotlib._image.resample}
        1    0.000    0.000    0.080    0.080 backend_tkagg.py:13(blit)
        1    0.000    0.000    0.080    0.080 _backend_tk.py:72(blit)
      2/1    0.000    0.000    0.080    0.080 {method 'call' of '_tkinter.tkapp' objects}
        1    0.000    0.000    0.080    0.080 _backend_tk.py:58(_blit)
        1    0.080    0.080    0.080    0.080 {built-in method matplotlib.backends._tkagg.blit}
        2    0.000    0.000    0.042    0.021 axis.py:1379(draw)
       18    0.000    0.000    0.018    0.001 axis.py:287(draw)
       39    0.001    0.000    0.013    0.000 text.py:915(get_window_extent)
        4    0.000    0.000    0.013    0.003 axis.py:1311(_get_ticklabel_bboxes)
        4    0.000    0.000    0.013    0.003 axis.py:1315(<listcomp>)
       54    0.002    0.000    0.013    0.000 text.py:358(_get_layout)
       43    0.001    0.000    0.012    0.000 text.py:734(draw)
        6    0.000    0.000    0.010    0.002 patches.py:579(draw)
        6    0.000    0.000    0.010    0.002 axis.py:1270(_update_ticks)
        6    0.000    0.000    0.010    0.002 patches.py:530(_draw_paths_with_artist_properties)
        6    0.000    0.000    0.009    0.001 backend_agg.py:95(draw_path)
        6    0.009    0.001    0.009    0.001 {method 'draw_path' of 'matplotlib.backends._backend_agg.RendererAgg' objects}
        1    0.000    0.000    0.009    0.009 {built-in method builtins.print}
        4    0.000    0.000    0.009    0.002 run.py:448(write)
        5    0.000    0.000    0.008    0.002 rpc.py:216(remotecall)
        2    0.000    0.000    0.008    0.004 axis.py:2143(_get_tick_boxes_siblings)
        5    0.000    0.000    0.008    0.002 rpc.py:246(asyncreturn)
        5    0.000    0.000    0.008    0.002 rpc.py:290(getresponse)
        5    0.000    0.000    0.008    0.002 rpc.py:306(_getresponse)
        5    0.000    0.000    0.007    0.001 threading.py:280(wait)
        4    0.000    0.000    0.007    0.002 rpc.py:606(__call__)

When running Matplotlib 3.1.3

         29643 function calls (28812 primitive calls) in 3.848 seconds

   Ordered by: cumulative time, internal time, call count
   List reduced from 661 to 40 due to restriction <40>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    3.848    3.848 min_code.py:29(toggle)
        1    0.000    0.000    3.814    3.814 backend_tkagg.py:8(draw)
        1    0.000    0.000    3.552    3.552 backend_agg.py:382(draw)
    130/1    0.001    0.000    3.547    3.547 artist.py:30(draw_wrapper)
        1    0.000    0.000    3.547    3.547 figure.py:1661(draw)
      2/1    0.000    0.000    3.540    3.540 image.py:117(_draw_list_compositing_images)
        1    0.000    0.000    3.540    3.540 _base.py:2573(draw)
        1    0.000    0.000    3.494    3.494 image.py:595(draw)
        1    0.091    0.091    3.490    3.490 image.py:872(make_image)
        1    0.002    0.002    3.399    3.399 image.py:255(_make_image)
        2    3.192    1.596    3.192    1.596 image.py:163(_rgb_to_rgba)
        1    0.000    0.000    0.240    0.240 _backend_tk.py:58(blit)
        1    0.235    0.235    0.235    0.235 {built-in method matplotlib.backends._tkagg.blit}
        2    0.204    0.102    0.205    0.102 {built-in method matplotlib._image.resample}
        2    0.000    0.000    0.040    0.020 axis.py:1195(draw)
        1    0.000    0.000    0.034    0.034 {built-in method builtins.print}
        4    0.000    0.000    0.034    0.008 run.py:354(write)
        5    0.000    0.000    0.033    0.007 rpc.py:217(remotecall)
        5    0.000    0.000    0.032    0.006 rpc.py:247(asyncreturn)
        5    0.000    0.000    0.032    0.006 rpc.py:291(getresponse)
        5    0.000    0.000    0.032    0.006 rpc.py:307(_getresponse)
        5    0.000    0.000    0.032    0.006 threading.py:263(wait)
       10    0.032    0.003    0.032    0.003 {method 'acquire' of '_thread.lock' objects}
        2    0.026    0.013    0.026    0.013 {method 'call' of '_tkinter.tkapp' objects}
        4    0.000    0.000    0.025    0.006 rpc.py:560(__getattr__)
        1    0.000    0.000    0.025    0.025 rpc.py:578(__getmethods)
        1    0.000    0.000    0.021    0.021 __init__.py:1178(update_idletasks)
       18    0.000    0.000    0.015    0.001 axis.py:289(draw)
        4    0.000    0.000    0.015    0.004 axis.py:1074(_update_ticks)
        8    0.000    0.000    0.011    0.001 ticker.py:2079(__call__)
        6    0.000    0.000    0.011    0.002 patches.py:563(draw)
        8    0.000    0.000    0.011    0.001 ticker.py:2083(tick_values)
        8    0.000    0.000    0.011    0.001 ticker.py:2019(_raw_ticks)
        6    0.000    0.000    0.010    0.002 backend_agg.py:119(draw_path)
        6    0.010    0.002    0.010    0.002 {method 'draw_path' of 'matplotlib.backends._backend_agg.RendererAgg' objects}
        8    0.000    0.000    0.009    0.001 axis.py:56(__init__)
        4    0.000    0.000    0.009    0.002 rpc.py:607(__call__)
       43    0.001    0.000    0.009    0.000 text.py:655(draw)
       39    0.000    0.000    0.008    0.000 text.py:852(get_window_extent)
        4    0.000    0.000    0.008    0.002 axis.py:1147(_get_tick_bboxes)

Expected outcome

I would hope that performance remained reasonable consistent or improved between versions.

Additional information

I have only been able to trace this as far as _make_image. The issue seems to persist regardless of interpolation method.

Operating system

Windows 11 64-bit

Matplotlib Version

3.2 upward

Matplotlib Backend

TkAgg

Python version

3.6 for Matplotlib 3.1.3; 3.9 for Matplotlib 3.8.2

Jupyter version

n/A

Installation

conda

@rcomer
Copy link
Member

rcomer commented Dec 21, 2023

I know you stated that

The issue seems to persist regardless of interpolation method.

But I just want to double check whether you have tried the "nearest" interpolation method, as that was the default prior to v3.2
https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.2.0.html#default-image-interpolation

@ilopata1
Copy link
Author

I have tried both and it does not appear to make a material difference for me.

@jklymak
Copy link
Member

jklymak commented Dec 21, 2023

Can you make a reproducible example that doesn't rely on having tk installed?

@ilopata1
Copy link
Author

As requested below. It actually appears that _rgb_to_rgba may be the culprit

import cProfile
from profilehooks import profile
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import time


SIZE = 16000

         
@profile(immediate=True)
def toggle(event):
        global ax, fig , border
        border = 0 if border == SIZE /4 else SIZE /4 
        ax.set_xlim(border, SIZE - border)
        ax.set_ylim(border, SIZE - border)
        t1= time.process_time()
        fig.canvas.draw()
        print("Draw time:", time.process_time() - t1)

def image_gen():
    black = np.zeros((int(SIZE / 8) , int(SIZE / 8), 3),dtype='uint8')
    white = np.ones((int(SIZE / 8) , int(SIZE / 8), 3),dtype='uint8') * 255
    col1 = np.concatenate([white, black, white, black, white, black, white, black], axis=0)
    col2  = np.concatenate([black, white, black, white, black, white, black, white], axis=0)
    output = np.concatenate([col1, col2, col1, col2,col1, col2, col1, col2], axis=1)
    im = Image.fromarray(output, 'RGB')
    return im
              

          
fig, ax = plt.subplots()
fig.canvas.mpl_connect('button_press_event', toggle)    
border = 0
random_image = image_gen()
image = ax.imshow(random_image, interpolation='none')
plt.show()

@ilopata1
Copy link
Author

As a further reference point, executing the same code with matplotlib 2.2.2 gives the results below:

        22416 function calls (21710 primitive calls) in 1.865 seconds

   Ordered by: cumulative time, internal time, call count
   List reduced from 656 to 40 due to restriction <40>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    1.865    1.865 min_code.py:12(toggle)
        1    0.000    0.000    1.816    1.816 backend_agg.py:421(draw)
     76/1    0.001    0.000    1.813    1.813 artist.py:47(draw_wrapper)
        1    0.000    0.000    1.813    1.813 figure.py:1438(draw)
      2/1    0.000    0.000    1.809    1.809 image.py:123(_draw_list_compositing_images)
        1    0.000    0.000    1.809    1.809 _base.py:2528(draw)
        1    0.000    0.000    1.785    1.785 image.py:569(draw)
        1    0.034    0.034    1.780    1.780 image.py:832(make_image)
        1    0.000    0.000    1.746    1.746 image.py:275(_make_image)
        1    1.031    1.031    1.031    1.031 image.py:169(_rgb_to_rgba)
        1    0.714    0.714    0.714    0.714 {built-in method matplotlib._image.resample}
        1    0.000    0.000    0.049    0.049 {built-in method builtins.print}
        4    0.000    0.000    0.049    0.012 run.py:354(write)
        4    0.000    0.000    0.048    0.012 rpc.py:607(__call__)
        4    0.000    0.000    0.048    0.012 rpc.py:217(remotecall)
        4    0.000    0.000    0.048    0.012 rpc.py:247(asyncreturn)
        4    0.000    0.000    0.048    0.012 rpc.py:291(getresponse)
        4    0.000    0.000    0.048    0.012 rpc.py:307(_getresponse)
        4    0.000    0.000    0.048    0.012 threading.py:263(wait)
        8    0.048    0.006    0.048    0.006 {method 'acquire' of '_thread.lock' objects}
        2    0.000    0.000    0.017    0.008 axis.py:1182(draw)
        6    0.000    0.000    0.009    0.002 patches.py:507(draw)
       18    0.000    0.000    0.008    0.000 axis.py:289(draw)
        6    0.000    0.000    0.008    0.001 backend_agg.py:142(draw_path)
        6    0.008    0.001    0.008    0.001 {method 'draw_path' of 'matplotlib.backends._backend_agg.RendererAgg' objects}
        4    0.000    0.000    0.006    0.001 axis.py:1020(_update_ticks)
        1    0.005    0.005    0.005    0.005 {method 'draw_image' of 'matplotlib.backends._backend_agg.RendererAgg' objects}
       25    0.000    0.000    0.004    0.000 text.py:691(draw)
       40    0.000    0.000    0.004    0.000 axis.py:967(iter_ticks)
       18    0.000    0.000    0.004    0.000 lines.py:731(draw)
        4    0.000    0.000    0.004    0.001 ticker.py:1943(__call__)
        4    0.000    0.000    0.004    0.001 ticker.py:1947(tick_values)
        4    0.000    0.000    0.003    0.001 ticker.py:1896(_raw_ticks)
        1    0.000    0.000    0.003    0.003 backend_agg.py:442(get_renderer)
        1    0.000    0.000    0.003    0.003 backend_agg.py:300(clear)
        1    0.003    0.003    0.003    0.003 {method 'clear' of 'matplotlib.backends._backend_agg.RendererAgg' objects}
       18    0.000    0.000    0.003    0.000 backend_agg.py:190(draw_text)
        4    0.000    0.000    0.003    0.001 axis.py:74(__init__)
  503/417    0.001    0.000    0.002    0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}
        4    0.000    0.000    0.002    0.000 axis.py:1119(_get_tick_bboxes)

So, on my machine:
2.2.2->1.865 seconds
3.1.3->3.848 seconds
3.2.8->6.264 seconds

@jklymak
Copy link
Member

jklymak commented Dec 22, 2023

This is still hard for me to debug because it has a mouse click, which zooms on my machine, changing the size of the canvas. Does this require the interactive component?

@ilopata1
Copy link
Author

Here is a version that is not interactive. For me this takes about 1 second to execute on version 2.2.2 and about 2.7 seconds on version 3.8.2.

import cProfile
from profilehooks import profile
import matplotlib
matplotlib.use('agg')       
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import time


SIZE = 16000


@profile(immediate=True)
def zoom():
        global ax, fig , border
        border = 0 if border == SIZE /4 else SIZE /4 
        ax.set_xlim(border, SIZE - border)
        ax.set_ylim(border, SIZE - border)
        t1= time.process_time()
        fig.canvas.draw()
        print("Draw time:", time.process_time() - t1)

def image_gen():
    black = np.zeros((int(SIZE / 8) , int(SIZE / 8), 3),dtype='uint8')
    white = np.ones((int(SIZE / 8) , int(SIZE / 8), 3),dtype='uint8') * 255
    col1 = np.concatenate([white, black, white, black, white, black, white, black], axis=0)
    col2  = np.concatenate([black, white, black, white, black, white, black, white], axis=0)
    output = np.concatenate([col1, col2, col1, col2,col1, col2, col1, col2], axis=1)
    im = Image.fromarray(output, 'RGB')
    return im
              

   
fig, ax = plt.subplots()
border = 0
random_image = image_gen()
image = ax.imshow(random_image, interpolation='nearest')
zoom()
zoom()

@QuLogic
Copy link
Member

QuLogic commented Dec 22, 2023

line_profiler does point to _rgb_to_rgba as you've found:

Total time: 3.64252 s
File: /home/elliott/code/matplotlib/lib/matplotlib/image.py
Function: _rgb_to_rgba at line 217

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   217                                           @profile
   218                                           def _rgb_to_rgba(A):
   219                                               """
   220                                               Convert an RGB image to RGBA, as required by the image resample C++
   221                                               extension.
   222                                               """
   223         2         41.9     21.0      0.0      rgba = np.zeros((A.shape[0], A.shape[1], 4), dtype=A.dtype)
   224         2    3446705.2    2e+06     94.6      rgba[:, :, :3] = A
   225         2         14.3      7.1      0.0      if rgba.dtype == np.uint8:
   226         2     195758.7  97879.4      5.4          rgba[:, :, 3] = 255
   227                                               else:
   228                                                   rgba[:, :, 3] = 1.0
   229         2          1.9      0.9      0.0      return rgba

There are two calls to it in _make_image:

   560         1          0.9      0.9      0.0                  if A.shape[2] == 3:
   561         1    1840651.4    2e+06     48.5                      A = _rgb_to_rgba(A)
   562         1          4.0      4.0      0.0                  alpha = self._get_scalar_alpha()
   563         2     113653.9  56827.0      3.0                  output_alpha = _resample(  # resample alpha channel
   564         1          4.0      4.0      0.0                      self, A[..., 3], out_shape, t, alpha=alpha)
   565         2      36536.1  18268.0      1.0                  output = _resample(  # resample rgb channels
   566         1    1801965.4    2e+06     47.5                      self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
   567         1         64.7     64.7      0.0                  output[..., 3] = output_alpha  # recombine rgb and alpha
   568

The comments seem to indicate this is because the C++ extension needs it that way. But the first call to _resample is with just the alpha channel, which in this case is just 255, so that seems a waste (but there might be a real alpha channel in other cases.) The second call to _resample uses _rgb_to_rgba(A[..., :3]); in other words, it is using an opaque (255/1.0) alpha channel, not the original one. The other calls to _resample are with A.ndim == 2, aka 2D/non-RGB(A) data, in the earlier half of the if above this point of _make_image.

So if I've gone through all cases correctly, there is no Python code that needs to call the resampler with RGBA data instead of RGB; it's just that way because it used to be that way?

@QuLogic
Copy link
Member

QuLogic commented Dec 22, 2023

We should be able to drop the first call with:

diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py
index 8b672e5ad2..249f58bcb2 100644
--- a/lib/matplotlib/image.py
+++ b/lib/matplotlib/image.py
@@ -555,11 +555,14 @@ class _ImageBase(martist.Artist, cm.ScalarMappable):
                 if A.ndim == 2:  # _interpolation_stage == 'rgba'
                     self.norm.autoscale_None(A)
                     A = self.to_rgba(A)
-                if A.shape[2] == 3:
-                    A = _rgb_to_rgba(A)
                 alpha = self._get_scalar_alpha()
-                output_alpha = _resample(  # resample alpha channel
-                    self, A[..., 3], out_shape, t, alpha=alpha)
+                if A.shape[2] == 3:
+                    # No need to resample alpha or make a full array; NumPy will expand
+                    # this out when it's assigned to the alpha channel below.
+                    output_alpha = 255 if A.dtype == np.uint8 else 1.0
+                else:
+                    output_alpha = _resample(  # resample alpha channel
+                        self, A[..., 3], out_shape, t, alpha=alpha)
                 output = _resample(  # resample rgb channels
                     self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
                 output[..., 3] = output_alpha  # recombine rgb and alpha

which cuts the time in half again. Removing the second one would require some spelunking into the image extension, though I'm not sure it's possible as it also cuts in Agg as well.

@ilopata1
Copy link
Author

@QuLogic Thanks for working on this.

Your patch gives me the following error:

File "..........\lib\site-packages\matplotlib\image.py", line 207, in _resample
    _image.resample(data, out, transform,
ValueError: If 3D, input and output arrays must be RGBA with shape (M, N, 4); got trailing dimensions of 3 and 3 respectively

@ilopata1
Copy link
Author

Sorry. Retract that. My error in applying the diff. That does indeed help

@ilopata1
Copy link
Author

However, even with that patch, version 3.8.2 is, still 2.5 times slower than 2.2.2.

@oscargus
Copy link
Contributor

FWIW, _rgb_to_rgba has not been touched since it was added in 2.0. Hence, the different run-times is a bit strange (unless there are different underlying datatypes or NumPy has done something drastic...).

The second major time consumer seems to be _image.resample, but that code seems mainly untouched as well, primarily a bit of type refactoring for 3.8.

Anyway, for 3.9, the wrapper code is moved to pybind11, which hopefully should not degrade the performance further if nothing else...

@ilopata1
Copy link
Author

@oscasgus I had similarly compared the code bases and did not see major differences. I have also done some testing with NumPy 1.19 vs 1.26, which are the relevant versions for my installs, and could not uncover a cause there either.

I am little suspicious that it may have to do with memory allocation. I have profiled using memory_profiler for 2.2.2 and 3.8.2 as shown in these images
2 2 2
3 8 2

Here is the line level trace for 2.2.2

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    15 2501.785 MiB 2501.785 MiB           1   @profile
    16                                         def zoom():
    17                                                 global ax, fig , border
    18 2501.785 MiB    0.000 MiB           1           border = 0 if border == SIZE /4 else SIZE /4
    19 2501.785 MiB    0.000 MiB           1           ax.set_xlim(border, SIZE - border)
    20 2501.785 MiB    0.000 MiB           1           ax.set_ylim(border, SIZE - border)
    21 2501.785 MiB    0.000 MiB           1           t1= time.process_time()
    22 1772.469 MiB -729.316 MiB           1           fig.canvas.draw()
    23 1772.473 MiB    0.004 MiB           1           print("Draw time:", time.process_time() - t1)

And the line level trace for 3.8,2

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    15 1772.359 MiB 1772.359 MiB           1   @profile
    16                                         def zoom():
    17                                                 global ax, fig , border
    18 1772.359 MiB    0.000 MiB           1           border = 0 if border == SIZE /4 else SIZE /4
    19 1772.359 MiB    0.000 MiB           1           ax.set_xlim(border, SIZE - border)
    20 1772.359 MiB    0.000 MiB           1           ax.set_ylim(border, SIZE - border)
    21 1772.359 MiB    0.000 MiB           1           t1= time.process_time()
    22 1776.109 MiB    3.750 MiB           1           fig.canvas.draw()
    23 1776.113 MiB    0.004 MiB           1           print("Draw time:", time.process_time() - t1)

It appears to me that with version 2.2.2, more memory is allocated to the image artist before the draw is called and that this memory is released after the draw completes. With 3.8.2 it appears that the artist has less memory allocated before the draw and that it has to allocate more during the draw process, which therefore takes more time.

Hoping this might provide some additional clues?

@ilopata1
Copy link
Author

I am pursuing threads that I don't really understand, so I hope this is not distracting from the core issue, but one thing that interests me is that in Issue 4567 it is suggested that no copy of the image data is made and yet in image.py at line 686 a call is made to safe_masked_invalid that overrides copy=False to make a copy.

While this seems like this may be an issue, even removing this check completely does not seem to have any effect on the issue I reported here, but I thought I should raise it in case it sheds any light on the matter.

@WeatherGod
Copy link
Member

WeatherGod commented Dec 22, 2023 via email

@jklymak
Copy link
Member

jklymak commented Dec 22, 2023

Thanks a lot for the reproducible example.

If I change the call to
image = ax.imshow(random_image, interpolation='nearest', interpolation_stage='rgba')
I still get pretty slow speeds (3s on my machine per draw). (I changed it to this because the code is much simpler).

If I remove the song and dance with alpha from

alpha = self._get_scalar_alpha()
output_alpha = _resample( # resample alpha channel
self, A[..., 3], out_shape, t, alpha=alpha)
output = _resample( # resample rgb channels
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
output[..., 3] = output_alpha # recombine rgb and alpha

then the speed is much faster at 1.4 s or so, which is what I get for 2.2.2. So I think QuLogic's idea above is sound, and thats probably the best we can do.

@ilopata1
Copy link
Author

Thank you all for you interest, help and support. I think we can consider this closed. My only outstanding question is whether QuLogic's code might get merged or whether I will need to maintain this as a patch for my own use?

QuLogic added a commit to QuLogic/matplotlib that referenced this issue Dec 23, 2023
This is probably the best we can do to fix matplotlib#27554 at this time, without
delving into re-writing the Agg resampler (which, as the Python code
notes, does require RGBA.)
QuLogic added a commit to QuLogic/matplotlib that referenced this issue Dec 23, 2023
This is probably the best we can do to fix matplotlib#27554 at this time, without
delving into re-writing the Agg resampler (which, as the Python code
notes, does require RGBA.)
QuLogic added a commit to QuLogic/matplotlib that referenced this issue Jan 3, 2024
This is probably the best we can do to fix matplotlib#27554 at this time, without
delving into re-writing the Agg resampler (which, as the Python code
notes, does require RGBA.)
@QuLogic QuLogic added this to the v3.9.0 milestone Jan 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants