Skip to content

Commit 942d912

Browse files
committed
ImageViewer: Fix and improve setting default attributes
1 parent 6e99e5d commit 942d912

File tree

2 files changed

+142
-59
lines changed

2 files changed

+142
-59
lines changed

orangecontrib/imageanalytics/widgets/owimageviewer.py

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
QNetworkRequest,
4242
)
4343
from AnyQt.QtWidgets import QApplication, QShortcut
44-
from Orange.data import DiscreteVariable, Domain, StringVariable, Table, Variable
44+
from Orange.data import DiscreteVariable, StringVariable, Table, Variable
4545
from Orange.widgets import gui, settings
4646
from Orange.widgets.utils.annotated_data import create_annotated_table
4747
from Orange.widgets.utils.itemmodels import DomainModel
@@ -50,10 +50,7 @@
5050
from orangewidget.utils.itemmodels import PyListModel
5151
from orangewidget.widget import Message, Msg
5252

53-
from orangecontrib.imageanalytics.utils.image_utils import (
54-
extract_paths,
55-
filter_image_attributes,
56-
)
53+
from orangecontrib.imageanalytics.utils.image_utils import extract_paths
5754
from orangecontrib.imageanalytics.widgets.utils.imagepreview import Preview
5855
from orangecontrib.imageanalytics.widgets.utils.thumbnailview import (
5956
IconView as _IconView,
@@ -206,11 +203,6 @@ def _intersectSet(self, rect: QRect) -> List[QModelIndex]:
206203
return indices
207204

208205

209-
class MetaDomainModel(DomainModel):
210-
def set_domain(self, domain: Domain):
211-
super().set_domain(None if domain is None else Domain([], metas=domain.metas))
212-
213-
214206
class OWImageViewer(OWWidget):
215207
name = "Image Viewer"
216208
description = "View images referred to in the data."
@@ -231,8 +223,13 @@ class Error(OWWidget.Error):
231223
"Unable to display images! Please ensure that the chosen "
232224
"Image Filename Attribute store the correct paths to the images."
233225
)
226+
no_image_attr = Msg(
227+
"Data does not contain any variables with image names.\n"
228+
"Numeric variables cannot store image names."
229+
)
234230

235231
settingsHandler = settings.DomainContextHandler()
232+
settings_version = 2
236233

237234
image_attr: Optional[Variable] = settings.ContextSetting(None)
238235
title_attr: Optional[Variable] = settings.ContextSetting(None)
@@ -257,9 +254,7 @@ def __init__(self):
257254
self._errcount = 0
258255
self._successcount = 0
259256

260-
self.image_model = MetaDomainModel(
261-
valid_types=(StringVariable, DiscreteVariable)
262-
)
257+
self.image_model = DomainModel(valid_types=StringVariable)
263258
gui.comboBox(
264259
self.controlArea,
265260
self,
@@ -311,38 +306,59 @@ def sizeHint(self):
311306

312307
@Inputs.data
313308
def setData(self, data):
314-
self.closeContext()
309+
if self.image_attr is not None:
310+
# Don't store invalid contexts because they will match anything
311+
# and crash the widget when they're used.
312+
self.closeContext()
315313
self.clear()
316-
self.data = data
317-
318-
if data is not None:
319-
self.image_model.set_domain(data.domain)
320-
self.title_model.set_domain(data.domain)
321-
im_attr = filter_image_attributes(self.data)
322-
self.image_attr = im_attr[0] if im_attr else None
323-
self.title_attr = self.title_model[0] if self.title_model else None
314+
if data is None:
315+
self.commit.now()
316+
return
324317

325-
self.openContext(data)
318+
self.image_model.set_domain(data.domain)
319+
self.title_model.set_domain(data.domain)
320+
if not self.image_model:
321+
self.Error.no_image_attr()
322+
self.commit.now()
323+
return
326324

327-
if self.image_model:
328-
self.setupModel()
325+
self.data = data
326+
self._propose_image_and_title_attr()
327+
self.openContext(data)
328+
self.setupModel()
329329
self.commit.now()
330330

331-
def __select_image_attr(self):
332-
for attr in self.image_model:
333-
if attr.attributes.get("type").lower() == "image":
334-
return attr
335-
# check if function already exist
336-
return self.image_model[0] if self.image_model else None
331+
def _propose_image_and_title_attr(self):
332+
self.image_attr = max(
333+
self.image_model,
334+
key=lambda attr: attr.attributes.get("type", "").lower() == "image"
335+
)
336+
# Use class variable if it exists. Otherwise,
337+
# prefer string variables (there will be at least one, otherwise
338+
# image_model is empty and widget reports an error,
339+
# but avoid those marked as "image" and in particular the one used
340+
# for image_attr
341+
self.title_attr = self.data.domain.class_var or max(
342+
# exclude separators
343+
(attr for attr in self.title_model if isinstance(attr, Variable)),
344+
key=lambda attr:
345+
isinstance(attr, StringVariable)
346+
and (3
347+
- (attr.attributes.get("type", "").lower() == "image")
348+
- (attr is self.image_attr))
349+
)
337350

338351
def clear(self):
339352
self.data = None
340353
self.Error.no_images_shown.clear()
354+
self.Error.no_image_attr.clear()
341355
if self.__watcher is not None:
342356
self.__watcher.finishedAt.disconnect(self.__on_load_finished)
343357
self.__watcher = None
344358
self._cancelAllTasks()
345359
self.clearModel()
360+
self.image_attr = None
361+
self.title_attr = None
346362
self.image_model.set_domain(None)
347363
self.title_model.set_domain(None)
348364
self.selected_items = set()
@@ -377,11 +393,11 @@ def setupModel(self):
377393
self.thumbnailView.selectionModel().selectionChanged.connect(
378394
self.onSelectionChanged
379395
)
380-
self.__watcher = FutureSetWatcher()
381-
self.__watcher.setFutures([it.future for it in self.items])
382-
self.__watcher.finishedAt.connect(self.__on_load_finished)
383-
self.__set_selected_items()
384-
self._updateStatus()
396+
self.__watcher = FutureSetWatcher()
397+
self.__watcher.setFutures([it.future for it in self.items])
398+
self.__watcher.finishedAt.connect(self.__on_load_finished)
399+
self.__set_selected_items()
400+
self._updateStatus()
385401

386402
def __set_selected_items(self):
387403
model = self.thumbnailView.model()
@@ -469,6 +485,17 @@ def onDeleteWidget(self):
469485
self.clear()
470486
super().onDeleteWidget()
471487

488+
@classmethod
489+
def migrate_context(cls, context, version):
490+
if version < 2:
491+
# Remove contexts in which image_attr is None because they match
492+
# anything and crash the widget.
493+
# Also remove context in which image_attr is not a string variable
494+
# because widget now requires a string variable.
495+
image_attr = context.values.get("image_attr")
496+
if image_attr is None or image_attr[1] != 103:
497+
raise settings.IncompatibleContext
498+
472499

473500
def column_data_as_qurl(
474501
table: Table, var: [StringVariable, DiscreteVariable]

orangecontrib/imageanalytics/widgets/tests/test_owimageviewer.py

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from AnyQt.QtCore import QItemSelection, QItemSelectionModel
2-
from typing import Set
3-
41
import os
2+
from typing import Set
53
import unittest
4+
from unittest.mock import Mock
5+
66
import numpy as np
77
from numpy.testing import assert_array_equal
88

9+
from AnyQt.QtCore import QItemSelection, QItemSelectionModel
10+
911
from Orange.data import (
1012
Table,
1113
StringVariable,
@@ -106,7 +108,7 @@ def test_selection(self):
106108
d = self.image_data
107109
str_var.attributes["origin"] = d.domain["Image"].attributes["origin"]
108110
new_data = Table(
109-
Domain([], metas=(str_var,) + d.domain.metas[1:]),
111+
Domain([], metas=(str_var,) + d.domain.metas[:-1]),
110112
np.empty((len(d), 0)),
111113
metas=d.metas,
112114
)
@@ -132,44 +134,98 @@ def test_selection_schema(self):
132134
assert_array_equal(self.image_data[2:3].metas, output.metas)
133135

134136
def test_settings_schema(self):
135-
data = Table("zoo")
136-
data = data.transform(
137-
Domain([], metas=data.domain.metas + data.domain.attributes)
138-
)
137+
domain = Domain([], None, [StringVariable(n) for n in "abc"])
138+
data = Table.from_list(domain, [list("abc")] * 3)
139139
self.send_signal(self.widget.Inputs.data, data)
140140

141-
simulate.combobox_activate_item(self.widget.controls.image_attr, "toothed")
142-
simulate.combobox_activate_item(self.widget.controls.title_attr, "toothed")
141+
simulate.combobox_activate_item(self.widget.controls.image_attr, "b")
142+
simulate.combobox_activate_item(self.widget.controls.title_attr, "c")
143143

144144
settings = self.widget.settingsHandler.pack_data(self.widget)
145145
widget = self.create_widget(OWImageViewer, stored_settings=settings)
146146
self.send_signal(widget.Inputs.data, data, widget=widget)
147147

148-
self.assertEqual(data.domain["toothed"], self.widget.image_attr)
149-
self.assertEqual(data.domain["toothed"], self.widget.title_attr)
148+
self.assertEqual(data.domain["b"], self.widget.image_attr)
149+
self.assertEqual(data.domain["c"], self.widget.title_attr)
150150

151151
def test_set_attributes(self):
152152
data = self.image_data
153153
# by default - image attribute is one with type image
154154
self.send_signal(self.widget.Inputs.data, data)
155155
self.assertEqual(data.domain["Image"], self.widget.image_attr)
156-
self.assertEqual(data.domain["b"], self.widget.title_attr)
156+
self.assertEqual(data.domain["Image"], self.widget.title_attr)
157+
self.__select_images({"afternoon-4175917_640.jpg", "atomium-4179270_640.jpg"})
158+
self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))
159+
self.assertIsNotNone(self.get_output(self.widget.Outputs.data))
157160

158-
# none of attributes have type image select first non-continuous from meta
161+
# no suitable attributes
159162
data = data.transform(
160163
Domain(data.domain.attributes, metas=data.domain.metas[:2])
161164
)
162165
self.send_signal(self.widget.Inputs.data, data)
163-
self.assertEqual(data.domain["c"], self.widget.image_attr)
164-
self.assertEqual(data.domain["b"], self.widget.title_attr)
166+
self.assertEqual(None, self.widget.image_attr)
167+
self.assertEqual(None, self.widget.title_attr)
168+
self.assertTrue(self.widget.Error.no_image_attr.is_shown())
169+
self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))
170+
self.assertIsNone(self.get_output(self.widget.Outputs.data))
165171

166-
# no suitable attributes - image_attr is None
167-
data = data.transform(
168-
Domain(data.domain.attributes, metas=data.domain.metas[:1])
169-
)
170-
self.send_signal(self.widget.Inputs.data, data)
171-
self.assertIsNone(self.widget.image_attr)
172-
self.assertEqual(data.domain["b"], self.widget.title_attr)
172+
self.send_signal(self.widget.Inputs.data, None)
173+
self.assertFalse(self.widget.Error.no_image_attr.is_shown())
174+
175+
def test_default_attr_priority(self):
176+
w = self.widget
177+
w.data = Mock()
178+
w.data.domain.class_var = None
179+
180+
attrs = [
181+
DiscreteVariable("a", values=["a", "b", "c"]),
182+
ContinuousVariable("b")
183+
]
184+
class_var = DiscreteVariable("c", values=["a", "b", "c"])
185+
metas = [
186+
ContinuousVariable("d"),
187+
DiscreteVariable("e", values=["a", "b", "c"])
188+
] + [StringVariable(f"s{i}") for i in range(4)]
189+
*_, s0, s1, s2, s3 = metas
190+
s1.attributes = s2.attributes = {"type": "image"}
191+
192+
domain = Domain(attrs, class_var, metas)
193+
w.image_model.set_domain(domain)
194+
w.title_model.set_domain(domain)
195+
w._propose_image_and_title_attr()
196+
self.assertIs(s1, w.image_attr)
197+
self.assertIs(s0, w.title_attr)
198+
199+
w.data.domain.class_var = class_var
200+
w.image_attr = w.title_attr = None
201+
w._propose_image_and_title_attr()
202+
self.assertIs(s1, w.image_attr)
203+
self.assertIs(class_var, w.title_attr)
204+
205+
w.data.domain.class_var = None
206+
domain = Domain(attrs, class_var, metas[3:])
207+
w.image_model.set_domain(domain)
208+
w.title_model.set_domain(domain)
209+
w.image_attr = w.title_attr = None
210+
w._propose_image_and_title_attr()
211+
self.assertIs(s1, w.image_attr)
212+
self.assertIs(s3, w.title_attr)
213+
214+
domain = Domain(attrs, class_var, metas[3:-1])
215+
w.image_model.set_domain(domain)
216+
w.title_model.set_domain(domain)
217+
w.image_attr = w.title_attr = None
218+
w._propose_image_and_title_attr()
219+
self.assertIs(s1, w.image_attr)
220+
self.assertIs(s2, w.title_attr)
221+
222+
domain = Domain(attrs, class_var, metas[3:4])
223+
w.image_model.set_domain(domain)
224+
w.title_model.set_domain(domain)
225+
w.image_attr = w.title_attr = None
226+
w._propose_image_and_title_attr()
227+
self.assertIs(s1, w.image_attr)
228+
self.assertIs(s1, w.title_attr)
173229

174230

175231
if __name__ == "__main__":

0 commit comments

Comments
 (0)