Skip to content

Commit

Permalink
Merge pull request #687 from markotoplak/hyper-robust
Browse files Browse the repository at this point in the history
HyperSpectra: handle degenerate (all nan) coordinates and data
  • Loading branch information
markotoplak authored Sep 18, 2023
2 parents a174f91 + dc9d6b7 commit 50fbe2d
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 38 deletions.
14 changes: 12 additions & 2 deletions orangecontrib/spectroscopy/tests/test_owhyper.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,16 @@ def setUpClass(cls):
irisunknown = Interpolate(np.arange(20))(cls.iris)
# dataset without any attributes, but XY
whitelight0 = cls.whitelight.transform(
Orange.data.Domain([], None, metas=cls.whitelight.domain.metas))
cls.strange_data = [None, cls.iris1, iris0, empty, irisunknown, whitelight0]
Orange.data.Domain([], None, metas=cls.whitelight.domain.metas))[:100]
unknowns = cls.iris.copy()
with unknowns.unlocked():
unknowns.X[:, :] = float("nan")
single_pixel = cls.iris[:50] # all image coordinates map to one spot
unknown_pixel = single_pixel.copy()
with unknown_pixel.unlocked():
unknown_pixel.Y[:] = float("nan")
cls.strange_data = [None, cls.iris1, iris0, empty, irisunknown, whitelight0,
unknowns, single_pixel, unknown_pixel]

def setUp(self):
self.widget = self.create_widget(OWHyper) # type: OWHyper
Expand Down Expand Up @@ -159,7 +167,9 @@ def try_big_selection(self):

def test_strange(self):
for data in self.strange_data:
self.widget = self.create_widget(OWHyper)
self.send_signal("Data", data)
wait_for_image(self.widget)
self.try_big_selection()

def test_context_not_open_invalid(self):
Expand Down
7 changes: 5 additions & 2 deletions orangecontrib/spectroscopy/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ def values_to_linspace(vals):

def location_values(vals, linspace):
vals = np.asarray(vals)
if linspace[2] == 1: # everything is the same value
if linspace is None or linspace[2] == 1: # everything is the same value
width = 1
else:
width = (linspace[1] - linspace[0]) / (linspace[2] - 1)
return (vals - linspace[0]) / width
start = 0
if linspace is not None:
start = linspace[0]
return (vals - start) / width


def index_values(vals, linspace):
Expand Down
83 changes: 49 additions & 34 deletions orangecontrib/spectroscopy/widgets/owhyper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import collections.abc
import math
from collections import OrderedDict
from xml.sax.saxutils import escape

Expand Down Expand Up @@ -60,6 +61,10 @@ class ImageTooBigException(Exception):
pass


class UndefinedImageException(Exception):
pass


def refresh_integral_markings(dis, markings_list, curveplot):
for m in markings_list:
if m in curveplot.markings:
Expand Down Expand Up @@ -125,7 +130,7 @@ def get_levels(img):
while img.size > 2 ** 16:
img = img[::2, ::2]
mn, mx = bottleneck.nanmin(img), bottleneck.nanmax(img)
if mn == mx:
if mn == mx or math.isnan(mx) or math.isnan(mn):
mn = 0
mx = 255
return [mn, mx]
Expand Down Expand Up @@ -793,6 +798,8 @@ def set_data(self, data):
self.data_ids = {}

def refresh_img_selection(self):
if self.lsx is None or self.lsy is None:
return
selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=np.uint8)
selected_px[self.data_imagepixels[self.data_valid_positions, 0],
self.data_imagepixels[self.data_valid_positions, 1]] = \
Expand Down Expand Up @@ -867,12 +874,9 @@ def update_view(self):
self.data_imagepixels = None
self.data_valid_positions = None

if self.data and self.attr_x and self.attr_y:
self.start(self.compute_image, self.data, self.attr_x, self.attr_y,
self.parent.image_values(),
self.parent.image_values_fixed_levels())
else:
self.image_updated.emit()
self.start(self.compute_image, self.data, self.attr_x, self.attr_y,
self.parent.image_values(),
self.parent.image_values_fixed_levels())

def set_visible_image(self, img: np.ndarray, rect: QRectF):
self.vis_img.setImage(img)
Expand All @@ -896,6 +900,9 @@ def set_visible_image_comp_mode(self, comp_mode: QPainter.CompositionMode):
def compute_image(data: Orange.data.Table, attr_x, attr_y,
image_values, image_values_fixed_levels, state: TaskState):

if data is None or attr_x is None or attr_y is None:
raise UndefinedImageException

def progress_interrupt(i: float):
if state.is_interruption_requested():
raise InterruptException
Expand All @@ -922,17 +929,22 @@ def extract_col(data, var):
res.image_values_fixed_levels = image_values_fixed_levels
progress_interrupt(0)

if lsx[-1] * lsy[-1] > IMAGE_TOO_BIG:
if lsx is not None and lsy is not None \
and lsx[-1] * lsy[-1] > IMAGE_TOO_BIG:
raise ImageTooBigException((lsx[-1], lsy[-1]))

# the code below does this, but part-wise:
# d = image_values(data).X[:, 0]
parts = []
for slice in split_to_size(len(data), 10000):
part = image_values(data[slice]).X
parts.append(part)
progress_interrupt(0)
d = np.concatenate(parts)
if lsx is not None and lsy is not None:
# the code below does this, but part-wise:
# d = image_values(data).X[:, 0]
parts = []
for slice in split_to_size(len(data), 10000):
part = image_values(data[slice]).X
parts.append(part)
progress_interrupt(0)
d = np.concatenate(parts)
else:
# no need to compute integrals when nothing will be shown
d = np.full((data.X.shape[0], 1), float("nan"))

res.d = d
progress_interrupt(0)
Expand All @@ -957,25 +969,26 @@ def on_done(self, res):
if invalid_positions:
self.parent.Information.not_shown(invalid_positions)

imdata = np.ones((lsy[2], lsx[2], d.shape[1])) * float("nan")
imdata[yindex[valid], xindex[valid]] = d[valid]

self.data_values = d
self.data_imagepixels = np.vstack((yindex, xindex)).T
self.img.setImage(imdata, autoLevels=False)
self.update_levels()
self.update_rgb_levels()
self.update_color_schema()
self.update_legend_visible()
if lsx is not None and lsy is not None:
imdata = np.ones((lsy[2], lsx[2], d.shape[1])) * float("nan")
imdata[yindex[valid], xindex[valid]] = d[valid]

# shift centres of the pixels so that the axes are useful
shiftx = _shift(lsx)
shifty = _shift(lsy)
left = lsx[0] - shiftx
bottom = lsy[0] - shifty
width = (lsx[1]-lsx[0]) + 2*shiftx
height = (lsy[1]-lsy[0]) + 2*shifty
self.img.setRect(QRectF(left, bottom, width, height))
self.data_values = d
self.data_imagepixels = np.vstack((yindex, xindex)).T
self.img.setImage(imdata, autoLevels=False)
self.update_levels()
self.update_rgb_levels()
self.update_color_schema()
self.update_legend_visible()

# shift centres of the pixels so that the axes are useful
shiftx = _shift(lsx)
shifty = _shift(lsy)
left = lsx[0] - shiftx
bottom = lsy[0] - shifty
width = (lsx[1]-lsx[0]) + 2*shiftx
height = (lsy[1]-lsy[0]) + 2*shifty
self.img.setRect(QRectF(left, bottom, width, height))

self.refresh_img_selection()
self.image_updated.emit()
Expand All @@ -990,6 +1003,8 @@ def on_exception(self, ex: Exception):
if isinstance(ex, ImageTooBigException):
self.parent.Error.image_too_big(ex.args[0][0], ex.args[0][1])
self.image_updated.emit()
elif isinstance(ex, UndefinedImageException):
self.image_updated.emit()
else:
raise ex

Expand Down

0 comments on commit 50fbe2d

Please sign in to comment.