Skip to content

Commit 56d5006

Browse files
committed
Fix contrast adjustment for high dynamic range float images
Fix #46
1 parent 6fc54bd commit 56d5006

File tree

3 files changed

+51
-13
lines changed

3 files changed

+51
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
🛠️ Bug fixes:
1212

1313
* [Issue #44](https://github.com/PlotPyStack/PlotPy/issues/44) - Incorrect calculation method for "∑(y)" in `CurveStatsTool`: replaced `spt.trapezoid` with `np.sum`, which is more consistent with the summation operation
14+
* [Issue #46](https://github.com/PlotPyStack/PlotPy/issues/46) - Contrast adjustment with 'Eliminate outliers' failed for float images with high dynamic range
1415

1516
Other changes:
1617

plotpy/lutrange.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,47 @@
2929
def hist_range_threshold(
3030
hist: np.ndarray, bin_edges: np.ndarray, percent: float
3131
) -> tuple[float, float]:
32-
"""Return the range corresponding to the given percent of the histogram
32+
"""Return the range corresponding to the central `percent` of the histogram mass.
33+
This can be used to eliminate outliers symmetrically, e.g. for contrast adjustment.
34+
35+
Notes:
36+
- If the histogram comes from an image with integer values (e.g. 0-255),
37+
the first bin is assumed to correspond to value 0 and will be ignored.
38+
- For floating-point images, all bins are considered.
3339
3440
Args:
35-
hist (numpy.ndarray): The histogram
36-
bin_edges (numpy.ndarray): The bin edges
37-
percent (float): The percent of the histogram
41+
hist: The histogram (length N)
42+
bin_edges: The bin edges (length N+1)
43+
percent: Percent of histogram mass to retain (between 0 and 100)
3844
3945
Returns:
40-
tuple[float, float]: The range
46+
A tuple containing the minimum and maximum values of the range
47+
corresponding to the central `percent` of the histogram mass.
4148
"""
42-
hist = np.concatenate((hist, [0]))
43-
hist = hist[1:]
44-
bin_edges = bin_edges[1:]
49+
if not (0 <= percent <= 100):
50+
raise ValueError("percent must be in (0, 100]")
51+
52+
hist_len = len(hist)
53+
i_offset = 0
54+
55+
# 1. Remove the first bin (typically corresponding to 0), only for integer images
56+
if np.issubdtype(bin_edges.dtype, np.integer):
57+
hist = hist[1:]
58+
i_offset = 1
59+
60+
# 3. Threshold: keep `percent`% of the mass → remove (1 - percent)% symmetrically
4561
threshold = 0.5 * percent / 100 * hist.sum()
4662

47-
i_bin_min = np.cumsum(hist).searchsorted(threshold)
48-
i_bin_max = -1 - np.cumsum(np.flipud(hist)).searchsorted(threshold)
63+
# 4. Find index where left cumulative sum exceeds threshold
64+
i_bin_min = max(np.cumsum(hist).searchsorted(threshold) - i_offset, 0)
65+
66+
# 5. Find index where right cumulative sum exceeds threshold
67+
i_bin_max = hist_len - np.searchsorted(np.cumsum(np.flipud(hist)), threshold)
68+
69+
# 6. Return bounds as [bin_edges[i_min], bin_edges[i_max + 1]]
70+
vmin, vmax = bin_edges[i_bin_min], bin_edges[i_bin_max]
4971

50-
return bin_edges[i_bin_min], bin_edges[i_bin_max]
72+
return vmin, vmax
5173

5274

5375
def lut_range_threshold(

plotpy/tests/features/test_contrast.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
import os
1111
import os.path as osp
1212

13+
import numpy as np
1314
from guidata.env import execenv
1415
from guidata.qthelpers import qt_app_context
1516

1617
from plotpy.builder import make
1718
from plotpy.tests import get_path
18-
from plotpy.tests.data import gen_image1
19+
from plotpy.tests.data import gen_image1, gen_image4
1920

2021

2122
def __create_dialog_with_contrast(item):
@@ -72,6 +73,20 @@ def test_contrast2():
7273
plot.set_active_item(item2)
7374

7475

76+
def test_contrast3():
77+
"""Contrast test 3
78+
79+
Test if level histogram works properly when the image has a really high dynamic
80+
range (the validation is not automatic)
81+
"""
82+
with qt_app_context(exec_loop=True):
83+
data = gen_image4(512, 512)
84+
data = np.fft.fftshift(np.fft.fft2(data)).real
85+
item = make.image(data, colormap="viridis", eliminate_outliers=2.0)
86+
win = __create_dialog_with_contrast(item)
87+
88+
7589
if __name__ == "__main__":
76-
# test_contrast1()
90+
test_contrast1()
7791
test_contrast2()
92+
test_contrast3()

0 commit comments

Comments
 (0)