Skip to content

Commit 92e3d1c

Browse files
committed
Fixes #5 and #7: contrast panel/levels histogram issues
1 parent 6d46cb7 commit 92e3d1c

File tree

8 files changed

+188
-83
lines changed

8 files changed

+188
-83
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
* [Issue #3](https://github.com/PlotPyStack/PlotPy/issues/3) - `PlotWidget`: `ZeroDivisionError` on resize while ignoring constraints
88
* [Issue #4](https://github.com/PlotPyStack/PlotPy/issues/4) - Average cross section: `RuntimeWarning: Mean of empty slice.`
9+
* [Issue #5](https://github.com/PlotPyStack/PlotPy/issues/5) - Contrast panel: levels histogram is sometimes not updated
910
* [Issue #6](https://github.com/PlotPyStack/PlotPy/issues/6) - 1D Histogram items are not properly drawn
11+
* [Issue #7](https://github.com/PlotPyStack/PlotPy/issues/7) - Contrast panel: histogram may contains zeros periodically due to improper bin sizes
1012

1113
## Version 2.0.1 ##
1214

plotpy/interfaces/items.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -489,20 +489,24 @@ def can_sethistogram(self) -> bool:
489489

490490

491491
class IHistDataSource:
492-
def get_histogram(self, nbins: int) -> tuple[np.ndarray, np.ndarray]:
492+
def get_histogram(
493+
self, nbins: int, drange: tuple[float, float] | None = None
494+
) -> tuple[np.ndarray, np.ndarray]:
493495
"""
494496
Return a tuple (hist, bins) where hist is a list of histogram values
495497
496498
Args:
497-
nbins (int): number of bins
499+
nbins: number of bins
500+
drange: lower and upper range of the bins. If not provided, range is
501+
simply (data.min(), data.max()). Values outside the range are ignored.
498502
499503
Returns:
500-
tuple: (hist, bins)
504+
Tuple (hist, bins)
501505
502506
Example of implementation:
503507
504-
def get_histogram(self, nbins):
508+
def get_histogram(self, nbins, drange=None):
505509
data = self.get_data()
506-
return np.histogram(data, nbins)
510+
return np.histogram(data, bins=nbins, range=drange)
507511
"""
508512
pass

plotpy/items/histogram.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,21 @@ class HistDataSource:
3838
def __init__(self, data: np.ndarray) -> None:
3939
self.data = data
4040

41-
def get_histogram(self, nbins: int) -> tuple[np.ndarray, np.ndarray]:
41+
def get_histogram(
42+
self, nbins: int, drange: tuple[float, float] | None = None
43+
) -> tuple[np.ndarray, np.ndarray]:
4244
"""
4345
Return a tuple (hist, bins) where hist is a list of histogram values
4446
4547
Args:
46-
nbins (int): number of bins
48+
nbins: number of bins
49+
drange: lower and upper range of the bins. If not provided, range is
50+
simply (data.min(), data.max()). Values outside the range are ignored.
4751
4852
Returns:
49-
tuple: (hist, bins)
53+
Tuple (hist, bins)
5054
"""
51-
return np.histogram(self.data, nbins)
55+
return np.histogram(self.data, bins=nbins, range=drange)
5256

5357

5458
assert_interfaces_valid(HistDataSource)
@@ -73,6 +77,7 @@ def __init__(
7377
self.hist_count = None
7478
self.hist_bins = None
7579
self.bins = None
80+
self.bin_range = None
7681
self.old_bins = None
7782
self.source: BaseImageItem | None = None
7883
self.logscale: bool | None = None
@@ -157,13 +162,30 @@ def get_bins(self) -> int | None:
157162
"""
158163
return self.bins
159164

165+
def set_bin_range(self, bin_range: tuple[float, float] | None) -> None:
166+
"""Sets the range of the bins
167+
168+
Args:
169+
bin_range: (min, max) or None for automatic range
170+
"""
171+
self.bin_range = bin_range
172+
self.update_histogram()
173+
174+
def get_bin_range(self) -> tuple[float, float] | None:
175+
"""Returns the range of the bins
176+
177+
Returns:
178+
tuple: (min, max)
179+
"""
180+
return self.bin_range
181+
160182
def compute_histogram(self) -> tuple[np.ndarray, np.ndarray]:
161183
"""Compute histogram data
162184
163185
Returns:
164186
tuple: (hist, bins)
165187
"""
166-
return self.get_hist_source().get_histogram(self.bins)
188+
return self.get_hist_source().get_histogram(self.bins, self.bin_range)
167189

168190
def update_histogram(self) -> None:
169191
"""Update histogram data"""

plotpy/items/image/base.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -946,22 +946,28 @@ def can_sethistogram(self) -> bool:
946946
"""
947947
return False
948948

949-
def get_histogram(self, nbins: int) -> tuple[np.ndarray, np.ndarray]:
949+
def get_histogram(
950+
self, nbins: int, drange: tuple[float, float] | None = None
951+
) -> tuple[np.ndarray, np.ndarray]:
950952
"""
951953
Return a tuple (hist, bins) where hist is a list of histogram values
952954
953955
Args:
954-
nbins (int): number of bins
956+
nbins: number of bins
957+
drange: lower and upper range of the bins. If not provided, range is
958+
simply (data.min(), data.max()). Values outside the range are ignored.
955959
956960
Returns:
957-
tuple: (hist, bins)
961+
Tuple (hist, bins)
958962
"""
959963
if self.data is None:
960964
return [0], [0, 1]
961965
if self.histogram_cache is None or nbins != self.histogram_cache[0].shape[0]:
962966
if True:
963967
# Note: np.histogram does not accept data with NaN
964-
res = np.histogram(self.data[~np.isnan(self.data)], nbins)
968+
res = np.histogram(
969+
self.data[~np.isnan(self.data)], bins=nbins, range=drange
970+
)
965971
else:
966972
# TODO: _histogram is faster, but caching is buggy in this version
967973
_min, _max = get_nan_range(self.data)

plotpy/items/image/misc.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,20 +405,26 @@ def can_sethistogram(self) -> bool:
405405
"""
406406
return True
407407

408-
def get_histogram(self, nbins: int) -> tuple[np.ndarray, np.ndarray]:
408+
def get_histogram(
409+
self, nbins: int, drange: tuple[float, float] | None = None
410+
) -> tuple[np.ndarray, np.ndarray]:
409411
"""
410412
Return a tuple (hist, bins) where hist is a list of histogram values
411413
412414
Args:
413-
nbins (int): number of bins
415+
nbins: number of bins
416+
drange: lower and upper range of the bins. If not provided, range is
417+
simply (data.min(), data.max()). Values outside the range are ignored.
414418
415419
Returns:
416-
tuple: (hist, bins)
420+
Tuple (hist, bins)
417421
"""
418422
if self.data is None:
419423
return [0], [0, 1]
420424
_min, _max = get_nan_range(self.data)
421-
if self.data.dtype in (np.float64, np.float32):
425+
if drange is not None:
426+
bins = np.linspace(drange[0], drange[1], nbins + 1)
427+
elif self.data.dtype in (np.float64, np.float32):
422428
bins = np.unique(
423429
np.array(np.linspace(_min, _max, nbins + 1), dtype=self.data.dtype)
424430
)

plotpy/panels/contrastadjustment.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from typing import TYPE_CHECKING
2121

22+
import numpy as np
2223
from guidata.configtools import get_icon, get_image_layout
2324
from guidata.dataset import DataSet, FloatItem
2425
from guidata.qthelpers import add_actions, create_action
@@ -37,7 +38,7 @@
3738
from plotpy.tools import AntiAliasingTool, BasePlotMenuTool, SelectPointTool, SelectTool
3839

3940
if TYPE_CHECKING: # pragma: no cover
40-
from collections.abc import Callable
41+
from collections.abc import Callable, Generator
4142

4243
from qtpy.QtWidgets import QWidget
4344

@@ -65,14 +66,10 @@ def __init__(self, parent: QWidget = None) -> None:
6566
self.antialiased = False
6667

6768
# a dict of dict : plot -> selected items -> HistogramItem
68-
self._tracked_items: dict[BasePlot, dict[BaseImageItem, CurveItem]] = {}
69+
self._tracked_items: dict[BasePlot, dict[BaseImageItem, HistogramItem]] = {}
6970
self.param = CurveParam(_("Curve"), icon="curve.png")
7071
self.param.read_config(CONF, "histogram", "curve")
7172

72-
self.histparam = HistogramParam(_("Histogram"), icon="histogram.png")
73-
self.histparam.logscale = False
74-
self.histparam.n_bins = 256
75-
7673
self.range = XRangeSelection(0, 1)
7774
self.range_mono_color = self.range.shapeparam.sel_line.color
7875
self.range_multi_color = CONF.get("histogram", "range/multi/color", "red")
@@ -103,11 +100,13 @@ def connect_plot(self, plot: BasePlot) -> None:
103100
plot.SIG_ITEM_REMOVED.connect(self.item_removed)
104101
plot.SIG_ACTIVE_ITEM_CHANGED.connect(self.active_item_changed)
105102

106-
def tracked_items_gen(self) -> tuple[BaseImageItem, CurveItem]:
103+
def tracked_items_gen(
104+
self,
105+
) -> Generator[tuple[BaseImageItem, HistogramItem], None, None]:
107106
"""Generator of tracked items"""
108-
for plot, items in list(self._tracked_items.items()):
109-
for item in list(items.items()):
110-
yield item # tuple item,curve
107+
for _plot, items in list(self._tracked_items.items()):
108+
for item_curve_tuple in list(items.items()):
109+
yield item_curve_tuple # tuple item,curve
111110

112111
def __del_known_items(self, known_items: dict, items: list) -> None:
113112
"""Delete known items
@@ -129,7 +128,9 @@ def selection_changed(self, plot: BasePlot) -> None:
129128
Args:
130129
plot: plot whose selection changed
131130
"""
132-
items = plot.get_selected_items(item_type=IVoiImageItemType)
131+
items: list[BaseImageItem] = plot.get_selected_items(
132+
item_type=IVoiImageItemType
133+
)
133134
known_items = self._tracked_items.setdefault(plot, {})
134135

135136
if items:
@@ -153,13 +154,10 @@ def selection_changed(self, plot: BasePlot) -> None:
153154

154155
for item in items:
155156
if item not in known_items:
156-
imin, imax = item.get_lut_range_full()
157-
delta = int(float(imax) - float(imin))
158-
if delta > 0 and delta < 256:
159-
self.histparam.n_bins = delta
160-
else:
161-
self.histparam.n_bins = 256
162-
curve = HistogramItem(self.param, self.histparam, keep_weakref=True)
157+
histparam = HistogramParam(_("Histogram"), icon="histogram.png")
158+
histparam.logscale = False
159+
histparam.n_bins = 256
160+
curve = HistogramItem(self.param, histparam, keep_weakref=True)
163161
curve.set_hist_source(item)
164162
self.add_item(curve, z=0)
165163
known_items[item] = curve
@@ -170,8 +168,18 @@ def selection_changed(self, plot: BasePlot) -> None:
170168
return
171169
self.param.shade = 1.0 / nb_selected
172170
for item, curve in self.tracked_items_gen():
171+
if np.issubdtype(item.data.dtype, np.integer):
172+
# For integer data, we use the full range of data type
173+
info = np.iinfo(item.data.dtype)
174+
curve.histparam.bin_min = info.min
175+
curve.histparam.bin_max = info.max
176+
curve.histparam.n_bins = min(info.max - info.min + 1, 256)
177+
else:
178+
curve.histparam.bin_min = None
179+
curve.histparam.bin_max = None
180+
curve.histparam.n_bins = 256
173181
self.param.update_item(curve)
174-
self.histparam.update_hist(curve)
182+
curve.histparam.update_hist(curve)
175183

176184
self.active_item_changed(plot)
177185

plotpy/styles/histogram.py

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
# -*- coding: utf-8 -*-
22

3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
37
from guidata.dataset import (
48
BoolItem,
59
ChoiceItem,
610
ColorItem,
711
DataSet,
12+
FloatItem,
813
GetAttrProp,
914
IntItem,
1015
StringItem,
@@ -14,26 +19,39 @@
1419
from plotpy.styles.base import ItemParameters
1520
from plotpy.styles.image import BaseImageParam
1621

22+
if TYPE_CHECKING: # pragma: no cover
23+
from plotpy.items import Histogram2DItem, HistogramItem
24+
1725

1826
class HistogramParam(DataSet):
1927
n_bins = IntItem(_("Bins"), default=100, min=1, help=_("Number of bins"))
28+
bin_min = FloatItem(_("Min"), default=None, help=_("Minimum value"), check=False)
29+
bin_max = FloatItem(_("Max"), default=None, help=_("Maximum value"), check=False)
2030
logscale = BoolItem(_("logarithmic"), _("Y-axis scale"), default=False)
2131

22-
def update_param(self, obj):
23-
"""
32+
def update_param(self, item: HistogramItem) -> None:
33+
"""Update the histogram parameters from the plot item
2434
25-
:param obj:
35+
Args:
36+
item: Histogram item
2637
"""
27-
self.n_bins = obj.get_bins()
28-
self.logscale = obj.get_logscale()
38+
self.n_bins = item.get_bins()
39+
self.bin_min, self.bin_max = item.get_bin_range()
40+
self.logscale = item.get_logscale()
2941

30-
def update_hist(self, hist):
31-
"""
42+
def update_hist(self, item: HistogramItem) -> None:
43+
"""Update the histogram plot item from the parameters
3244
33-
:param hist:
45+
Args:
46+
item: Histogram item
3447
"""
35-
hist.set_bins(self.n_bins)
36-
hist.set_logscale(self.logscale)
48+
if self.bin_min is None or self.bin_max is None:
49+
item.bin_range = None
50+
else:
51+
item.bin_range = (self.bin_min, self.bin_max)
52+
item.bins = self.n_bins
53+
item.logscale = self.logscale
54+
item.update_histogram()
3755

3856

3957
class Histogram2DParam(BaseImageParam):
@@ -79,24 +97,26 @@ class Histogram2DParam(BaseImageParam):
7997
help=_("Background color when no data is present"),
8098
)
8199

82-
def update_param(self, obj):
83-
"""
100+
def update_param(self, item: Histogram2DItem) -> None:
101+
"""Update the histogram parameters from the plot item
84102
85-
:param obj:
103+
Args:
104+
item: 2D Histogram item
86105
"""
87-
super().update_param(obj)
88-
self.logscale = obj.logscale
89-
self.nx_bins, self.ny_bins = obj.nx_bins, obj.ny_bins
106+
super().update_param(item)
107+
self.logscale = item.logscale
108+
self.nx_bins, self.ny_bins = item.nx_bins, item.ny_bins
90109

91-
def update_histogram(self, histogram):
92-
"""
110+
def update_histogram(self, item: Histogram2DItem) -> None:
111+
"""Update the histogram plot item from the parameters
93112
94-
:param histogram:
113+
Args:
114+
item: 2D Histogram item
95115
"""
96-
histogram.logscale = int(self.logscale)
97-
histogram.set_background_color(self.background)
98-
histogram.set_bins(self.nx_bins, self.ny_bins)
99-
self.update_item(histogram)
116+
item.logscale = int(self.logscale)
117+
item.set_background_color(self.background)
118+
item.set_bins(self.nx_bins, self.ny_bins)
119+
self.update_item(item)
100120

101121

102122
class Histogram2DParam_MS(Histogram2DParam):

0 commit comments

Comments
 (0)