From 6791b1111399c477f69be067a0c0295ff22a3a14 Mon Sep 17 00:00:00 2001 From: Sang Woo Kim Date: Thu, 12 Mar 2026 22:55:34 +0900 Subject: [PATCH 1/3] Optimize addImage for image streaming: fast path bypasses full pipeline When updating an existing image with same-shape data and the backend renderer supports direct updates (updateData), bypass the full item dirty/remove/add cycle and update the renderer directly. Fast path conditions: - Backend renderer exists and has updateData method - Data shape is unchanged from previous frame - No alternative/alpha images provided (ImageData) Changes: - ImageData.setData: early fast path before data copy - ImageDataBase.setData: direct renderer update when shape unchanged - PlotWidget.addImage: skip redundant setActiveImage on same image - Fix double copy in ImageData.setData (copy=False to super) --- src/silx/gui/plot/PlotWidget.py | 5 ++-- src/silx/gui/plot/items/image.py | 40 +++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/silx/gui/plot/PlotWidget.py b/src/silx/gui/plot/PlotWidget.py index 6fd34b6159..309fa4a9c5 100755 --- a/src/silx/gui/plot/PlotWidget.py +++ b/src/silx/gui/plot/PlotWidget.py @@ -1496,8 +1496,9 @@ def addImage( else: self._notifyContentChanged(image) - if len(self.getAllImages()) == 1 or image is self.getActiveImage(): - self.setActiveImage(image) + if self.getActiveImage() != image: + if len(self.getAllImages()) == 1: + self.setActiveImage(image) if resetzoom: # We ask for a zoom reset in order to handle the plot scaling diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py index ca137be0dc..653c96aac9 100644 --- a/src/silx/gui/plot/items/image.py +++ b/src/silx/gui/plot/items/image.py @@ -371,6 +371,31 @@ def setData(self, data: numpy.ndarray, copy: bool = True): elif numpy.iscomplexobj(data): _logger.warning("Converting complex image to absolute value to plot it.") data = numpy.absolute(data) + + # Fast path: same shape, backend renderer supports direct update + renderer = self._backendRenderer + if ( + renderer is not None + and hasattr(renderer, 'updateData') + and self._data.shape == data.shape + ): + self._data = data + self._valueDataChanged() + + # Compute clim: fixed range or delegate to renderer + colormap = self.getColormap() + vmin, vmax = colormap.getVMin(), colormap.getVMax() + if vmin is not None and vmax is not None: + renderer.updateData(data, clim=(float(vmin), float(vmax))) + else: + renderer.updateData(data, clim=None) + + plot = self.getPlot() + if plot is not None: + plot._setDirtyPlot() + self.sigItemChanged.emit(ItemChangedType.DATA) + return + super().setData(data) def _updated(self, event=None, checkVisibility=True): @@ -488,6 +513,19 @@ def setData( :param copy: True (Default) to get a copy, False to use internal representation (do not modify!) """ + # Fast path: data-only update, bypass full pipeline + if alternative is None and alpha is None: + data_arr = numpy.asarray(data) + if ( + data_arr.ndim == 2 + and self._backendRenderer is not None + and hasattr(self._backendRenderer, 'updateData') + and self._data is not None + and self._data.shape == data_arr.shape + ): + super().setData(data_arr, copy=copy) + return + data = numpy.array(data, copy=copy or NP_OPTIONAL_COPY) assert data.ndim == 2 @@ -507,7 +545,7 @@ def setData( alpha = numpy.clip(alpha, 0.0, 1.0) self.__alpha = alpha - super().setData(data) + super().setData(data, copy=False) class ImageRgba(ImageBase): From cc41181aad2e852abbba89e17ed9fc8ec0509a64 Mon Sep 17 00:00:00 2001 From: Sang Woo Kim Date: Thu, 12 Mar 2026 23:22:14 +0900 Subject: [PATCH 2/3] Fix formatting for image.py to satisfy CI --- src/silx/gui/plot/items/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py index 653c96aac9..877baa3447 100644 --- a/src/silx/gui/plot/items/image.py +++ b/src/silx/gui/plot/items/image.py @@ -376,7 +376,7 @@ def setData(self, data: numpy.ndarray, copy: bool = True): renderer = self._backendRenderer if ( renderer is not None - and hasattr(renderer, 'updateData') + and hasattr(renderer, "updateData") and self._data.shape == data.shape ): self._data = data @@ -519,7 +519,7 @@ def setData( if ( data_arr.ndim == 2 and self._backendRenderer is not None - and hasattr(self._backendRenderer, 'updateData') + and hasattr(self._backendRenderer, "updateData") and self._data is not None and self._data.shape == data_arr.shape ): From 0b934beb7f05ee1deba78a617cf401030268ee88 Mon Sep 17 00:00:00 2001 From: Sang Woo Kim Date: Thu, 19 Mar 2026 13:41:41 +0900 Subject: [PATCH 3/3] Fix fast path: add safety guards and revert setActiveImage change --- src/silx/gui/plot/PlotWidget.py | 5 ++--- src/silx/gui/plot/items/image.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/silx/gui/plot/PlotWidget.py b/src/silx/gui/plot/PlotWidget.py index 309fa4a9c5..6fd34b6159 100755 --- a/src/silx/gui/plot/PlotWidget.py +++ b/src/silx/gui/plot/PlotWidget.py @@ -1496,9 +1496,8 @@ def addImage( else: self._notifyContentChanged(image) - if self.getActiveImage() != image: - if len(self.getAllImages()) == 1: - self.setActiveImage(image) + if len(self.getAllImages()) == 1 or image is self.getActiveImage(): + self.setActiveImage(image) if resetzoom: # We ask for a zoom reset in order to handle the plot scaling diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py index 877baa3447..978f454b98 100644 --- a/src/silx/gui/plot/items/image.py +++ b/src/silx/gui/plot/items/image.py @@ -372,11 +372,15 @@ def setData(self, data: numpy.ndarray, copy: bool = True): _logger.warning("Converting complex image to absolute value to plot it.") data = numpy.absolute(data) - # Fast path: same shape, backend renderer supports direct update + # Fast path: update renderer directly without full rebuild. + # Only safe when item has no pending state changes (origin, scale, etc.) + # and the data shape is unchanged. renderer = self._backendRenderer if ( renderer is not None and hasattr(renderer, "updateData") + and not self._dirty + and self._data is not None and self._data.shape == data.shape ): self._data = data @@ -390,6 +394,9 @@ def setData(self, data: numpy.ndarray, copy: bool = True): else: renderer.updateData(data, clim=None) + # Recompute colormapped display data (applies mask, etc.) + self._setColormappedData(self.getValueData(copy=False), copy=False) + plot = self.getPlot() if plot is not None: plot._setDirtyPlot() @@ -520,6 +527,7 @@ def setData( data_arr.ndim == 2 and self._backendRenderer is not None and hasattr(self._backendRenderer, "updateData") + and not self._dirty and self._data is not None and self._data.shape == data_arr.shape ): @@ -545,7 +553,7 @@ def setData( alpha = numpy.clip(alpha, 0.0, 1.0) self.__alpha = alpha - super().setData(data, copy=False) + super().setData(data) class ImageRgba(ImageBase):