Skip to content

Commit edf546e

Browse files
committed
WIP in case my local computer combusts
I promise to revise history soon 😬
1 parent ab66d3f commit edf546e

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

astrowidgets/bqplot.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import numpy as np
2+
from bqplot import Figure, LinearScale, Axis, ColorScale, PanZoom
3+
from bqplot_image_gl import ImageGL
4+
from bqplot_image_gl.interacts import (MouseInteraction,
5+
keyboard_events, mouse_events)
6+
7+
import ipywidgets as ipw
8+
import traitlets as trait
9+
10+
# Allowed locations for cursor display
11+
ALLOWED_CURSOR_LOCATIONS = ['top', 'bottom', None]
12+
13+
# List of marker names that are for internal use only
14+
RESERVED_MARKER_SET_NAMES = ['all']
15+
16+
17+
class _AstroImage(ipw.VBox):
18+
"""
19+
Encapsulate an image as a bqplot figure inside a box.
20+
21+
bqplot is involved for its pan/zoom capabilities, and it presents as
22+
a box to obscure the usual bqplot properties and methods.
23+
"""
24+
def __init__(self, image_data=None,
25+
display_width=500,
26+
viewer_aspect_ratio=1.0):
27+
super().__init__()
28+
29+
self._viewer_aspect_ratio = viewer_aspect_ratio
30+
31+
self._display_width = display_width
32+
self._display_height = self._viewer_aspect_ratio * self._display_width
33+
34+
35+
layout = ipw.Layout(width=f'{self._display_width}px',
36+
height=f'{self._display_height}px',
37+
justify_content='center')
38+
39+
self._figure_layout = layout
40+
41+
scale_x = LinearScale(min=0, max=1, #self._image_shape[1],
42+
allow_padding=False)
43+
scale_y = LinearScale(min=0, max=1, #self._image_shape[0],
44+
allow_padding=False)
45+
self._scales = {'x': scale_x, 'y': scale_y}
46+
axis_x = Axis(scale=scale_x, visible=False)
47+
axis_y = Axis(scale=scale_y, orientation='vertical', visible=False)
48+
scales_image = {'x': scale_x, 'y': scale_y,
49+
'image': ColorScale(max=1.114, min=2902,
50+
scheme='Greys')}
51+
52+
self._figure = Figure(scales=self._scales, axes=[axis_x, axis_y],
53+
fig_margin=dict(top=0, left=0,
54+
right=0, bottom=0),
55+
layout=layout)
56+
57+
self._image = ImageGL(scales=scales_image)
58+
59+
self._figure.marks = (self._image, )
60+
61+
panzoom = PanZoom(scales={'x': [scales_image['x']],
62+
'y': [scales_image['y']]})
63+
interaction = MouseInteraction(x_scale=scales_image['x'],
64+
y_scale=scales_image['y'],
65+
move_throttle=70, next=panzoom,
66+
events=keyboard_events + mouse_events)
67+
68+
self._figure.interaction = interaction
69+
70+
if image_data:
71+
self.set_data(image_data, reset_view=True)
72+
73+
self.children = (self._figure, )
74+
75+
@property
76+
def data_aspect_ratio(self):
77+
"""
78+
Aspect ratio of the image data, horizontal size over vertical size.
79+
"""
80+
return self._image_shape[0] / self._image_shape[1]
81+
82+
def reset_scale_to_fit_image(self):
83+
wide = self.data_aspect_ratio < 1
84+
tall = self.data_aspect_ratio > 1
85+
square = self.data_aspect_ratio == 1
86+
87+
if wide:
88+
self._scales['x'].min = 0
89+
self._scales['x'].max = self._image_shape[1]
90+
self._set_scale_aspect_ratio_to_match_viewer()
91+
elif tall or square:
92+
self._scales['y'].min = 0
93+
self._scales['y'].max = self._image_shape[0]
94+
self._set_scale_aspect_ratio_to_match_viewer(reset_scale='x')
95+
96+
# Great, now let's center
97+
self.center = (self._image_shape[1]/2,
98+
self._image_shape[0]/2)
99+
100+
101+
def _set_scale_aspect_ratio_to_match_viewer(self,
102+
reset_scale='y'):
103+
# Set the scales so that they match the aspect ratio
104+
# of the viewer, preserving the current image center.
105+
width_x, width_y = self.scale_widths
106+
frozen_width = dict(y=width_x, x=width_y)
107+
scale_aspect = width_x / width_y
108+
figure_x = float(self._figure.layout.width[:-2])
109+
figure_y = float(self._figure.layout.height[:-2])
110+
figure_aspect = figure_x / figure_y
111+
current_center = self.center
112+
if abs(figure_aspect - scale_aspect) > 1e-4:
113+
# Make the scale aspect ratio match the
114+
# figure layout aspect ratio
115+
if reset_scale == 'y':
116+
scale_factor = 1/ figure_aspect
117+
else:
118+
scale_factor = figure_aspect
119+
120+
self._scales[reset_scale].min = 0
121+
self._scales[reset_scale].max = frozen_width[reset_scale] * scale_factor
122+
self.center = current_center
123+
124+
def set_data(self, image_data, reset_view=True):
125+
self._image_shape = image_data.shape
126+
127+
if reset_view:
128+
self.reset_scale_to_fit_image()
129+
130+
# Set the image data and map it to the bqplot figure so that
131+
# cursor location corresponds to the underlying array index.
132+
self._image.image = image_data
133+
self._image.x = [0, self._image_shape[1]]
134+
self._image.y = [0, self._image_shape[0]]
135+
136+
@property
137+
def center(self):
138+
"""
139+
Center of current view in pixels in x, y.
140+
"""
141+
x_center = (self._scales['x'].min + self._scales['x'].max) / 2
142+
y_center = (self._scales['y'].min + self._scales['y'].max) / 2
143+
return (x_center, y_center)
144+
145+
@property
146+
def scale_widths(self):
147+
width_x = self._scales['x'].max - self._scales['x'].min
148+
width_y = self._scales['y'].max - self._scales['y'].min
149+
return (width_x, width_y)
150+
151+
@center.setter
152+
def center(self, value):
153+
x_c, y_c = value
154+
155+
width_x, width_y = self.scale_widths
156+
self._scales['x'].max = x_c + width_x / 2
157+
self._scales['x'].min = x_c - width_x / 2
158+
self._scales['y'].max = y_c + width_y / 2
159+
self._scales['y'].min = y_c - width_y / 2
160+
161+
def set_color(self, colors):
162+
# colors here means a list of hex colors
163+
self._image.scales['image'].colors = colors
164+
165+
166+
def bqcolors(colormap, reverse=False):
167+
from matplotlib import cm as cmp
168+
from matplotlib.colors import to_hex
169+
170+
# bqplot-image-gl has 256 levels
171+
LEVELS = 256
172+
173+
# Make a matplotlib colormap object
174+
mpl = cmp.get_cmap(colormap, LEVELS)
175+
176+
# Get RGBA colors
177+
mpl_colors = mpl(np.linspace(0, 1, LEVELS))
178+
179+
# Convert RGBA to hex
180+
bq_colors = [to_hex(mpl_colors[i, :]) for i in range(LEVELS)]
181+
182+
if reverse:
183+
bq_colors = bq_colors[::-1]
184+
185+
return bq_colors
186+
187+
"""
188+
next(iter(imviz.app._viewer_store.values())).figure
189+
"""
190+
191+
192+
class ImageWidget(ipw.VBox):
193+
click_center = trait.Bool(default_value=False).tag(sync=True)
194+
click_drag = trait.Bool(default_value=False).tag(sync=True)
195+
scroll_pan = trait.Bool(default_value=False).tag(sync=True)
196+
image_width = trait.Int(help="Width of the image (not viewer)").tag(sync=True)
197+
image_height = trait.Int(help="Height of the image (not viewer)").tag(sync=True)
198+
zoom_level = trait.Float(help="Current zoom of the view").tag(sync=True)
199+
marker = trait.Any(help="Markers").tag(sync=True)
200+
cuts = trait.Tuple(help="Cut levels").tag(sync=True)
201+
stretch = trait.Unicode(help='Stretch algorithm name').tag(sync=True)
202+
203+
def __init__(self, *args, image_width=500, image_height=500):
204+
super().__init__(*args)
205+
self.image_width = image_width
206+
self.image_height = image_height
207+
self._astro_im = _AstroImage()
208+
209+
# The methods, grouped loosely by purpose
210+
211+
# Methods for loading data
212+
# @abstractmethod
213+
# def load_fits(self, file):
214+
# raise NotImplementedError
215+
216+
def load_array(self, array):
217+
raise NotImplementedError
218+
219+
# @abstractmethod
220+
# def load_nddata(self, data):
221+
# raise NotImplementedError
222+
223+
# Saving contents of the view and accessing the view
224+
@abstractmethod
225+
def save(self, filename):
226+
raise NotImplementedError
227+
228+
# Marker-related methods
229+
@abstractmethod
230+
def start_marking(self):
231+
raise NotImplementedError
232+
233+
@abstractmethod
234+
def stop_marking(self):
235+
raise NotImplementedError
236+
237+
@abstractmethod
238+
def add_markers(self):
239+
raise NotImplementedError
240+
241+
@abstractmethod
242+
def get_markers(self):
243+
raise NotImplementedError
244+
245+
@abstractmethod
246+
def remove_markers(self):
247+
raise NotImplementedError
248+
249+
# @abstractmethod
250+
# def get_all_markers(self):
251+
# raise NotImplementedError
252+
253+
# @abstractmethod
254+
# def get_markers_by_name(self, marker_name=None):
255+
# raise NotImplementedError
256+
257+
# Methods that modify the view
258+
@abstractmethod
259+
def center_on(self):
260+
raise NotImplementedError
261+
262+
@abstractmethod
263+
def offset_to(self):
264+
raise NotImplementedError
265+
266+
@abstractmethod
267+
def zoom(self):
268+
raise NotImplementedError

0 commit comments

Comments
 (0)