Skip to content

Commit 6529b3f

Browse files
Merge pull request #152 from haesleinhuepf/histogram
Add interactive histogram
2 parents 86a8c7e + f0aa7f7 commit 6529b3f

File tree

6 files changed

+326
-2
lines changed

6 files changed

+326
-2
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ stackview.display_range(image)
7474

7575
![img.png](https://raw.githubusercontent.com/haesleinhuepf/stackview/main/docs/images/demo_display_range.gif)
7676

77+
### Interactive histogram
7778

79+
```
80+
stackview.histogram(image)
81+
```
82+
83+
![img.png](https://raw.githubusercontent.com/haesleinhuepf/stackview/main/docs/images/histogram.gif)
7884

7985
### Static insight views
8086

docs/histogram.ipynb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "94e6bb0c-c0ff-445a-abb4-f80e77f607e6",
6+
"metadata": {},
7+
"source": [
8+
"# Interactive histogram\n",
9+
"In this notebook we will create a histogram shown next to an image. By dragging the mouse in the image, one can create a new histogram of the intensities in the specified region."
10+
]
11+
},
12+
{
13+
"cell_type": "code",
14+
"execution_count": 1,
15+
"id": "f85a9c99-dd9b-4adf-8fde-1e74c942694b",
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"import numpy as np\n",
20+
"import stackview \n",
21+
"from skimage.io import imread\n",
22+
"\n",
23+
"image = imread(\"data/Haase_MRT_tfl3d1.tif\")"
24+
]
25+
},
26+
{
27+
"cell_type": "code",
28+
"execution_count": 2,
29+
"id": "28034b4b-2393-4930-ad80-60b6c38b549d",
30+
"metadata": {},
31+
"outputs": [
32+
{
33+
"data": {
34+
"application/vnd.jupyter.widget-view+json": {
35+
"model_id": "8e4ef033adff4193a5bef60299f7b41a",
36+
"version_major": 2,
37+
"version_minor": 0
38+
},
39+
"text/plain": [
40+
"HBox(children=(VBox(children=(HBox(children=(VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=2…"
41+
]
42+
},
43+
"execution_count": 2,
44+
"metadata": {},
45+
"output_type": "execute_result"
46+
}
47+
],
48+
"source": [
49+
"stackview.histogram(image, zoom_factor=1.5)"
50+
]
51+
},
52+
{
53+
"cell_type": "code",
54+
"execution_count": null,
55+
"id": "661d2ad6-c80c-4357-84ba-4e530a8f62e5",
56+
"metadata": {},
57+
"outputs": [],
58+
"source": []
59+
},
60+
{
61+
"cell_type": "code",
62+
"execution_count": null,
63+
"id": "fa383f8c-767f-4003-ab8e-b10bec4463b5",
64+
"metadata": {},
65+
"outputs": [],
66+
"source": []
67+
}
68+
],
69+
"metadata": {
70+
"kernelspec": {
71+
"display_name": "Python 3 (ipykernel)",
72+
"language": "python",
73+
"name": "python3"
74+
},
75+
"language_info": {
76+
"codemirror_mode": {
77+
"name": "ipython",
78+
"version": 3
79+
},
80+
"file_extension": ".py",
81+
"mimetype": "text/x-python",
82+
"name": "python",
83+
"nbconvert_exporter": "python",
84+
"pygments_lexer": "ipython3",
85+
"version": "3.11.11"
86+
}
87+
},
88+
"nbformat": 4,
89+
"nbformat_minor": 5
90+
}

docs/images/histogram.gif

2.87 MB
Loading

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="stackview",
8-
version="0.14.3",
8+
version="0.15.0",
99
author="Robert Haase",
1010
author_email="[email protected]",
1111
description="Interactive image stack viewing in jupyter notebooks",

stackview/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.14.3"
1+
__version__ = "0.15.0"
22

33
from ._static_view import jupyter_displayable_output, insight
44
from ._utilities import merge_rgb
@@ -24,4 +24,5 @@
2424
from ._sliceplot import sliceplot
2525
from ._wordcloudplot import wordcloudplot
2626
from ._add_bounding_boxes import add_bounding_boxes
27+
from ._histogram import histogram
2728

stackview/_histogram.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import warnings
2+
3+
def histogram(
4+
image,
5+
slice_number: int = None,
6+
alpha: float = 0.5,
7+
continuous_update: bool = True,
8+
slider_text: str = "[{}]",
9+
zoom_factor: float = 1.0,
10+
zoom_spline_order: int = 0,
11+
colormap:str = None,
12+
display_min:float = None,
13+
display_max:float = None
14+
):
15+
"""Shows an image with a slider to go through a stack plus a label with the current mouse position and intensity at that position.
16+
17+
Parameters
18+
----------
19+
image : image
20+
Image shown
21+
labels: label image
22+
Labels which can be manually modified to draw annotations
23+
slice_number : int, optional
24+
Slice-position in the stack
25+
alpha : float, optional
26+
Alpha blending value for the labels on top of the image
27+
continuous_update : bool, optional
28+
Update the image while dragging the mouse on the slider, default: False
29+
zoom_factor: float, optional
30+
Allows showing the image larger (> 1) or smaller (<1)
31+
zoom_spline_order: int, optional
32+
Spline order used for interpolation (default=0, nearest-neighbor)
33+
colormap: str, optional
34+
Matplotlib colormap name or "pure_green", "pure_magenta", ...
35+
display_min: float, optional
36+
Lower bound of properly shown intensities
37+
display_max: float, optional
38+
Upper bound of properly shown intensities
39+
40+
Returns
41+
-------
42+
An ipywidget with an image display, a slider and a label showing mouse position and intensity.
43+
"""
44+
from ._utilities import _no_resize
45+
from ._image_widget import _img_to_rgb
46+
import ipywidgets
47+
from ipyevents import Event
48+
from ._slice_viewer import _SliceViewer
49+
import numpy as np
50+
import matplotlib.pyplot as plt
51+
from ._grid import grid
52+
53+
if 'cupy.ndarray' in str(type(image)):
54+
image = image.get()
55+
56+
if slice_number is None:
57+
slice_number = int(image.shape[0] / 2)
58+
59+
total_min = float(image.min())
60+
total_max = float(image.max())
61+
62+
# Image view
63+
viewer = _SliceViewer(image,
64+
zoom_factor=zoom_factor,
65+
zoom_spline_order=zoom_spline_order,
66+
colormap=colormap,
67+
display_min=display_min,
68+
display_max=display_max,
69+
slider_text=slider_text)
70+
view = viewer.view
71+
# setup user interface for changing the slice
72+
slice_slider = viewer.slice_slider
73+
74+
former_drawn_position = {'state':None,
75+
'start_x': 0,
76+
'start_y': 0,
77+
'end_x': image.shape[-1],
78+
'end_y': image.shape[-2],
79+
}
80+
81+
def create_histogram_plot(image, x, y, width, height):
82+
#return np.random.normal(50, y, (100, 100))
83+
cropped_image = image[..., y:y+height, x:x+width]
84+
85+
#histogram = np.histogram(cropped_image, bins=256, range=(0, 255))
86+
# plot histogram and store histogram as numpy RGB array
87+
plt.figure(figsize=(1.8, 1.4))
88+
# measure how long this takes
89+
90+
plt.hist(cropped_image.flatten(), bins=32)
91+
plt.xlim(total_min, total_max)
92+
plt.yticks([])
93+
plt.xticks([total_min, int((total_min + total_max) / 2), total_max])
94+
plt.tight_layout()
95+
96+
from io import BytesIO
97+
from PIL import Image
98+
99+
with BytesIO() as file_obj:
100+
plt.savefig(file_obj, format='png')
101+
plt.close() # supress plot output
102+
file_obj.seek(0)
103+
104+
# Open the image using PIL's Image.open() which accepts a file-like object
105+
histogram_image = Image.open(file_obj)
106+
107+
# Convert the PIL image to a numpy array
108+
return np.array(histogram_image)[...,:3]
109+
110+
histogram_image = create_histogram_plot(image, 0, 0, image.shape[-2], image.shape[-1])
111+
histogram_viewer = _SliceViewer(histogram_image)
112+
113+
width = image.shape[-1]
114+
height = image.shape[-2]
115+
layout = layout=ipywidgets.Layout(display="flex", max_height="25px")
116+
slice_lbl = ipywidgets.Label(f"(..., 0:{height}, 0:{width}", layout=layout)
117+
dtype_lbl = ipywidgets.Label(str(image.dtype), layout=layout)
118+
min_intensity_lbl = ipywidgets.Label("", layout=layout)
119+
max_intensity_lbl = ipywidgets.Label("", layout=layout)
120+
121+
layout = ipywidgets.Layout(display="flex", justify_content="flex-end", min_width="50px", max_height="25px")
122+
123+
table = grid([
124+
[ipywidgets.Label("slice", layout=layout), slice_lbl],
125+
[ipywidgets.Label("dtype", layout=layout), dtype_lbl],
126+
[ipywidgets.Label("min", layout=layout), min_intensity_lbl],
127+
[ipywidgets.Label("max", layout=layout), max_intensity_lbl],
128+
])
129+
130+
# event handler when the user changed the slider:
131+
def update_display(event=None):
132+
slice_image1 = viewer.get_view_slice()
133+
134+
rgb_image1 = _img_to_rgb(slice_image1, colormap=colormap, display_min=display_min, display_max=display_max)
135+
from ._add_bounding_boxes import add_bounding_boxes
136+
bb = None
137+
if former_drawn_position['state'] is not None:
138+
bb = {
139+
'x': min(former_drawn_position['start_x'], former_drawn_position['end_x']),
140+
'y': min(former_drawn_position['start_y'], former_drawn_position['end_y']),
141+
'width': abs(former_drawn_position['start_x'] - former_drawn_position['end_x']),
142+
'height': abs(former_drawn_position['start_y'] - former_drawn_position['end_y'])
143+
}
144+
annotated_image = add_bounding_boxes(rgb_image1, [bb])
145+
slice_lbl.value = f"(..., {bb['y']}:{bb['y']+bb['height']}, {bb['x']}:{bb['x']+bb['width']})"
146+
else:
147+
annotated_image = rgb_image1
148+
149+
if former_drawn_position['state'] == "mouse-up" and bb is not None:
150+
h_image = create_histogram_plot(slice_image1, bb['x'], bb['y'], bb['width'], bb['height'])
151+
histogram_viewer.view.data = h_image
152+
former_drawn_position['state'] = None
153+
min_intensity_lbl.value = str(np.min(slice_image1))
154+
max_intensity_lbl.value = str(np.max(slice_image1))
155+
156+
view.data = annotated_image
157+
158+
159+
# user interface for histogram
160+
tool_box = ipywidgets.VBox([
161+
table,
162+
histogram_viewer.view
163+
])
164+
165+
event_handler = Event(source=view, watched_events=['mousemove'])
166+
167+
if slice_slider is not None:
168+
# connect user interface with event
169+
result = _no_resize(ipywidgets.HBox([
170+
ipywidgets.VBox([_no_resize(view), slice_slider]),
171+
tool_box
172+
]))
173+
else:
174+
result = _no_resize(ipywidgets.VBox([
175+
ipywidgets.HBox([_no_resize(view), tool_box]),
176+
]))
177+
178+
# event handler for when something was drawn
179+
def update_display_while_drawing(event):
180+
181+
# get position from event
182+
relative_position_x = event['relativeX'] / zoom_factor
183+
relative_position_y = event['relativeY'] / zoom_factor
184+
absolute_position_x = int(relative_position_x)
185+
absolute_position_y = int(relative_position_y)
186+
187+
188+
if event['buttons'] == 0:
189+
if former_drawn_position['state'] == 'mouse-down':
190+
# not clicked
191+
former_drawn_position['state'] = 'mouse-up'
192+
update_display()
193+
return
194+
195+
# compare position and last known position. If equal, don't update / redraw
196+
if former_drawn_position["end_x"] == absolute_position_x and former_drawn_position["end_y"] == absolute_position_y:
197+
return
198+
199+
if former_drawn_position['state'] is None:
200+
# mouse-down event
201+
former_drawn_position['state'] = 'mouse-down'
202+
former_drawn_position['start_x'] = absolute_position_x
203+
former_drawn_position['start_y'] = absolute_position_y
204+
205+
former_drawn_position['end_x'] = absolute_position_x
206+
former_drawn_position['end_y'] = absolute_position_y
207+
208+
x = min(former_drawn_position['start_x'], former_drawn_position['end_x'])
209+
y = min(former_drawn_position['start_y'], former_drawn_position['end_y'])
210+
w = abs(former_drawn_position['start_x'] - former_drawn_position['end_x'])
211+
h = abs(former_drawn_position['start_y'] - former_drawn_position['end_y'])
212+
213+
update_display()
214+
215+
# draw everything once
216+
update_display()
217+
218+
# connect events
219+
event_handler.on_dom_event(update_display_while_drawing)
220+
221+
def viewer_update(e=None):
222+
former_drawn_position['state'] = 'mouse-up'
223+
update_display()
224+
viewer.observe(viewer_update)
225+
226+
result.update = update_display
227+
return result

0 commit comments

Comments
 (0)