|
| 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