Skip to content

Commit

Permalink
Merge branch 'master' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreRaybaut committed Oct 1, 2024
2 parents ec6c8c4 + 81b28a0 commit b8c2aaa
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 61 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@

## Version 2.6.3 ##

🧯 In this release, test coverage is 79%.

🛠️ Bug fixes:

* [Issue #25](https://github.com/PlotPyStack/PlotPy/issues/25) - `OverflowError` with Contrast Adjustment panel for constant images
* When updating image parameters (`ImageParam`) from the associated item object:
* If `xmin`, `xmax`, `ymin`, `ymax` attributes are not yet set (i.e. `None`), do not update them with the image data bounds
* Previous behavior was to update them with the image data bounds, which was leading to breaking the automatic bounds update when the image data is updated
* [Issue #24](https://github.com/PlotPyStack/PlotPy/issues/24) - Colormap: side effect on image axes when changing the colormap
* [Issue #23](https://github.com/PlotPyStack/PlotPy/issues/23) - Windows: Image `_scaler` engine performance regression
* PySide6 compatibility issues:
* Fixed deprecated call to `QMouseEvent` in `tests/unit/utils.py`
* Added workaround for `QPolygonF` shape point slicing
Expand Down
33 changes: 16 additions & 17 deletions plotpy/items/shape/range.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import qwt.scale_map
from qtpy.QtCore import QPointF, QRectF
from qtpy.QtGui import QPainter
from qwt import QwtSymbol

from plotpy.plot import BasePlot
from plotpy.styles.base import ItemParameters


Expand Down Expand Up @@ -137,34 +139,31 @@ def draw(
yMap: Y axis scale map
canvasRect: Canvas rectangle
"""
plot = self.plot()
plot: BasePlot = self.plot()
if not plot:
return
if self.selected:
pen = self.sel_pen
sym = self.sel_symbol
pen: QG.QPen = self.sel_pen
sym: QwtSymbol = self.sel_symbol
else:
pen = self.pen
sym = self.symbol
pen: QG.QPen = self.pen
sym: QwtSymbol = self.symbol

rct = plot.canvas().contentsRect()
rct2 = QC.QRectF(rct)
rct2.setLeft(xMap.transform(self._min))
rct2.setRight(xMap.transform(self._max))
rct = QC.QRectF(plot.canvas().contentsRect())
rct.setLeft(xMap.transform(self._min))
rct.setRight(xMap.transform(self._max))

painter.fillRect(rct2, self.brush)
painter.fillRect(rct, self.brush)
painter.setPen(pen)
painter.drawLine(rct2.topLeft(), rct2.bottomLeft())
painter.drawLine(rct2.topRight(), rct2.bottomRight())
painter.drawLine(rct.topLeft(), rct.bottomLeft())
painter.drawLine(rct.topRight(), rct.bottomRight())

dash = QG.QPen(pen)
dash.setStyle(QC.Qt.DashLine)
dash.setWidth(1)
painter.setPen(dash)

center_x = int(rct2.center().x())
top = int(rct2.top())
bottom = int(rct2.bottom())
painter.drawLine(center_x, top, center_x, bottom)
cx = rct.center().x()
painter.drawLine(QC.QPointF(cx, rct.top()), QC.QPointF(cx, rct.bottom()))

painter.setPen(pen)
x0, x1, y = self.get_handles_pos()
Expand Down
20 changes: 4 additions & 16 deletions plotpy/styles/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,12 @@ class ImageParam(RawImageParam):
help=_("Locked images are not movable with the mouse"),
)
_xdata = BeginGroup(_("Image placement along X-axis"))
xmin = FloatItem(_("x|min"), default=None)
xmax = FloatItem(_("x|max"), default=None)
xmin = FloatItem(_("x|min"), default=None, check=False)
xmax = FloatItem(_("x|max"), default=None, check=False)
_end_xdata = EndGroup(_("Image placement along X-axis"))
_ydata = BeginGroup(_("Image placement along Y-axis"))
ymin = FloatItem(_("y|min"), default=None)
ymax = FloatItem(_("y|max"), default=None)
ymin = FloatItem(_("y|min"), default=None, check=False)
ymax = FloatItem(_("y|max"), default=None, check=False)
_end_ydata = EndGroup(_("Image placement along Y-axis"))

def update_param(self, item: ImageItem) -> None:
Expand All @@ -366,21 +366,9 @@ def update_param(self, item: ImageItem) -> None:
"""
super().update_param(item)
self.xmin = item.xmin
if self.xmin is None:
self.xmin = 0.0
self.ymin = item.ymin
if self.ymin is None:
self.ymin = 0.0
if item.is_empty():
shape = (0, 0)
else:
shape = item.data.shape
self.xmax = item.xmax
if self.xmax is None:
self.xmax = float(shape[1])
self.ymax = item.ymax
if self.ymax is None:
self.ymax = float(shape[0])

def update_item(self, item: ImageItem) -> None:
"""Update the given image item from the parameters.
Expand Down
35 changes: 23 additions & 12 deletions plotpy/tests/features/test_image_data_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import pytest
from guidata.qthelpers import exec_dialog, qt_app_context
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW

from plotpy.builder import make
Expand All @@ -31,39 +32,41 @@
from plotpy.tools import LockLUTRangeTool


def get_data() -> np.ndarray:
def get_data(variable_size: bool) -> np.ndarray:
"""Compute 2D Gaussian data and add a narrower Gaussian on top with a random
position and amplitude."""
size = np.random.randint(50, 200) if variable_size else 100
dtype = np.uint16
amp = np.iinfo(dtype).max * 0.3
data = ptd.gen_2d_gaussian(100, dtype, sigma=10.0, x0=0.0, y0=0.0, amp=amp)
data = ptd.gen_2d_gaussian(size, dtype, sigma=10.0, x0=0.0, y0=0.0, amp=amp)
# Choose a random position: x0, y0 have to be in the range [-10.0, 10.0]
x0 = np.random.uniform(-10.0, 10.0)
y0 = np.random.uniform(-10.0, 10.0)
# Choose a random amplitude: a has to be in the range [0.1, 0.5]
a = np.random.uniform(0.1, 0.7) * np.iinfo(dtype).max
# Add the narrower Gaussian on top
data += ptd.gen_2d_gaussian(100, dtype, sigma=4.0, x0=x0, y0=y0, amp=a)
data += ptd.gen_2d_gaussian(size, dtype, sigma=4.0, x0=x0, y0=y0, amp=a)
return data


class ImageUpdateDialog(PlotDialog):
"""Dialog box for image update"""

def __init__(self, title):
def __init__(self, title: str, variable_size: bool = False) -> None:
self.variable_size = variable_size
options = PlotOptions(title="-", show_contrast=True, type="image")
super().__init__(title=title, toolbar=True, edit=False, options=options)
self.resize(600, 600)
self.timer = QC.QTimer()
self.item = make.image(get_data(), interpolation="nearest")
self.item = make.image(get_data(self.variable_size), interpolation="nearest")
self.item.set_lut_range((15000, 28000))
plot = self.get_plot()
plot.add_item(self.item)
plot.set_active_item(self.item, select=False)
self.counter = 0
self.keep_lut_cb: QW.QCheckBox | None = None

def populate_plot_layout(self):
def populate_plot_layout(self) -> None:
"""Populate the plot layout"""
start_btn = QW.QPushButton("Start image update")
start_btn.clicked.connect(self.start_image_update)
Expand All @@ -74,9 +77,17 @@ def populate_plot_layout(self):
self.keep_lut_cb = QW.QCheckBox()
self.keep_lut_cb.setChecked(False)
self.add_widget(self.keep_lut_cb, 0, 2)
variable_size_cb = QW.QCheckBox("Variable size")
variable_size_cb.setChecked(self.variable_size)
variable_size_cb.stateChanged.connect(self.toggle_variable_size)
self.add_widget(variable_size_cb, 0, 3)
self.add_widget(self.plot_widget, 1, 0, 1, 0)

def register_tools(self):
def toggle_variable_size(self, state: int) -> None:
"""Toggle the variable size of the image"""
self.variable_size = state == QC.Qt.Checked

def register_tools(self) -> None:
"""Reimplement to connect the Keep LUT range checkbox to the item"""
mgr = self.get_manager()
keep_lut_tool = mgr.add_tool(LockLUTRangeTool)
Expand All @@ -87,18 +98,18 @@ def register_tools(self):
self.keep_lut_cb.setToolTip(keep_lut_tool.action.toolTip())
self.keep_lut_cb.stateChanged.connect(keep_lut_tool.activate)

def start_image_update(self):
def start_image_update(self) -> None:
"""Start updating the image"""
self.timer.timeout.connect(self.update_image)
self.timer.start(500)

def stop_image_update(self):
def stop_image_update(self) -> None:
"""Stop updating the image"""
self.timer.stop()

def update_image(self):
def update_image(self) -> None:
"""Update the image"""
data = get_data()
data = get_data(self.variable_size)
self.counter += 1
plot = self.get_plot()

Expand All @@ -111,7 +122,7 @@ def update_image(self):
plot.set_title(f"Image update {self.counter:03d}")
plot.replot()

def closeEvent(self, event):
def closeEvent(self, event: QG.QCloseEvent) -> None:
"""Reimplement closeEvent to stop the timer before closing the dialog"""
self.timer.stop()
super().closeEvent(event)
Expand Down
12 changes: 5 additions & 7 deletions plotpy/tests/items/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ def get_font_array(sz: int, chars: str = DEFAULT_CHARS) -> np.ndarray | None:
metric = pnt.fontMetrics()
rct = metric.boundingRect(chars)
pnt.end()
h = rct.height()
w = rct.width()
h, w = rct.height(), rct.width()
img = QG.QImage(w, h, QG.QImage.Format_ARGB32)
paint = QG.QPainter()
paint.begin(img)
Expand All @@ -62,12 +61,11 @@ def get_font_array(sz: int, chars: str = DEFAULT_CHARS) -> np.ndarray | None:
paint.drawText(0, paint.fontMetrics().ascent(), chars)
paint.end()
try:
data = img.bits().asstring(img.sizeInBytes())
data = img.bits().asstring(h * w * 4)
except AttributeError:
data = img.bits()
npy = np.frombuffer(data, np.uint8)
npy.shape = img.height(), img.bytesPerLine() // 4, 4
return npy[:, :, 0]
npy: np.ndarray = np.frombuffer(data, np.uint8)
return npy.reshape(h, w, 4)[:, :, 0]


def write_text_on_array(
Expand Down Expand Up @@ -119,7 +117,7 @@ def make_items(N: int) -> list[TrImageItem]:
s = float((info.max - info.min))
a1 = s * (data - m) / (M - m)
img = np.array(a1 + info.min, dtype)
write_text_on_array(img, 0, 0, int(N / 15.0), str(dtype))
write_text_on_array(img, 0, 0, int(N / 15.0), dtype.__name__)
items.append(make.trimage(img, colormap="jet"))
nc = int(np.sqrt(len(items)) + 1.0)
maxy, x, y = 0, 0, 0
Expand Down
16 changes: 7 additions & 9 deletions plotpy/tools/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,18 +642,17 @@ def activate_cmap(self, cmap: str | EditableColormap) -> None:
self.update_plot(self._active_colormap.name)
self.update_status(plot)

def update_plot(self, cmap: str) -> None:
def update_plot(self, cmap_name: str) -> None:
"""Update the plot with the given colormap.
Args:
cmap: Colormap name
cmap_name: Colormap name
"""
plot: BasePlot = self.get_active_plot()
items = get_selected_images(plot, IColormapImageItemType)
for item in items:
param: BaseImageParam = item.param
param.colormap = cmap
param.update_item(item)
cmap = item.get_color_map()
item.set_color_map(cmap_name, cmap.invert)
plot.SIG_ITEM_PARAMETERS_CHANGED.emit(item)
plot.invalidate()

Expand Down Expand Up @@ -705,10 +704,9 @@ def activate_command(self, plot: BasePlot, checked: bool) -> None:
if self._active_colormap is not None and plot is not None:
items = get_selected_images(plot, IColormapImageItemType)
for item in items:
param: BaseImageParam = item.param
param.invert_colormap = checked
param.update_item(item)
plot.SIG_ITEM_PARAMETERS_CHANGED.emit(item)
cmap = item.get_color_map()
item.set_color_map(cmap.name, invert=checked)
plot.SIG_ITEM_PARAMETERS_CHANGED.emit(item)
plot.invalidate()
self.update_status(plot)

Expand Down

0 comments on commit b8c2aaa

Please sign in to comment.