diff --git a/orangecontrib/snom/widgets/owpreprocessimage.py b/orangecontrib/snom/widgets/owpreprocessimage.py new file mode 100644 index 0000000..70dc4a5 --- /dev/null +++ b/orangecontrib/snom/widgets/owpreprocessimage.py @@ -0,0 +1,219 @@ +import time + +from Orange.data import Domain, DiscreteVariable, ContinuousVariable +from Orange.widgets.settings import DomainContextHandler +from Orange.widgets.utils.itemmodels import DomainModel +from orangecontrib.snom.widgets.preprocessors.registry import preprocess_image_editors +from orangewidget import gui +from orangewidget.settings import SettingProvider, ContextSetting, Setting + +import Orange.data +from Orange import preprocess +from Orange.widgets.widget import Output + +from orangecontrib.spectroscopy.widgets.owhyper import ImagePlot + +from orangecontrib.spectroscopy.widgets.owpreprocess import ( + GeneralPreprocess, + create_preprocessor, + InterruptException, +) + +from orangewidget.widget import Msg + + +class AImagePlot(ImagePlot): + def clear_markings(self): + pass + + +class ImagePreviews: + curveplot = SettingProvider(AImagePlot) + curveplot_after = SettingProvider(AImagePlot) + + value_type = 1 + + def __init__(self): + # the name of curveplot is kept because GeneralPreprocess + # expects these names + self.curveplot = AImagePlot(self) + self.curveplot_after = AImagePlot(self) + + def shutdown(self): + self.curveplot.shutdown() + self.curveplot_after.shutdown() + + +class SpectralImagePreprocess(GeneralPreprocess, ImagePreviews, openclass=True): + def __init__(self): + ImagePreviews.__init__(self) + super().__init__() + + def onDeleteWidget(self): + super().onDeleteWidget() + ImagePreviews.shutdown(self) + + +class OWPreprocessImage(SpectralImagePreprocess): + name = "Preprocess image" + id = "orangecontrib.snom.widgets.preprocessimage" + description = "Process image" + icon = "icons/preprocessimage.svg" + priority = 1010 + + settings_version = 2 + + settingsHandler = DomainContextHandler() + + _max_preview_spectra = 1000000 + preview_curves = Setting(10000) + + editor_registry = preprocess_image_editors + BUTTON_ADD_LABEL = "Add preprocessor..." + + attr_value = ContextSetting(None) + + class Outputs: + preprocessed_data = Output("Integrated Data", Orange.data.Table, default=True) + preprocessor = Output("Preprocessor", preprocess.preprocess.Preprocess) + + class Warning(SpectralImagePreprocess.Warning): + threshold_error = Msg("Low slider should be less than High") + + class Error(SpectralImagePreprocess.Error): + image_too_big = Msg("Image for chosen features is too big ({} x {}).") + + class Information(SpectralImagePreprocess.Information): + not_shown = Msg("Undefined positions: {} data point(s) are not shown.") + + def image_values(self): + attr_value = self.attr_value.name if self.attr_value else None + return lambda data, attr=attr_value: data.transform(Domain([data.domain[attr]])) + + def image_values_fixed_levels(self): + return None + + def __init__(self): + self.markings_list = [] + super().__init__() + + self.feature_value_model = DomainModel( + DomainModel.SEPARATED, valid_types=ContinuousVariable + ) + self.feature_value = gui.comboBox( + self.preview_settings_box, + self, + "attr_value", + label="Show feature", + contentsLength=12, + searchable=True, + callback=self.update_feature_value, + model=self.feature_value_model, + ) + + self.contextAboutToBeOpened.connect(lambda x: self.init_interface_data(x[0])) + + self.preview_runner.preview_updated.connect(self.redraw_data) + + def update_feature_value(self): + self.redraw_data() + + def redraw_data(self): + self.curveplot.update_view() + self.curveplot_after.update_view() + + def init_interface_data(self, data): + self.init_attr_values(data) + self.curveplot.init_interface_data(data) + self.curveplot_after.init_interface_data(data) + + def init_attr_values(self, data): + domain = data.domain if data is not None else None + self.feature_value_model.set_domain(domain) + self.attr_value = ( + self.feature_value_model[0] if self.feature_value_model else None + ) + + def show_preview(self, show_info_anyway=False): + super().show_preview(False) + + def create_outputs(self): + self._reference_compat_warning() + pp_def = [ + self.preprocessormodel.item(i) + for i in range(self.preprocessormodel.rowCount()) + ] + self.start( + self.run_task, + self.data, + self.reference_data, + pp_def, + self.process_reference, + ) + + def set_data(self, data): + super().set_data(data) + + self.closeContext() + + def valid_context(data): + if data is None: + return False + annotation_features = [ + v + for v in data.domain.metas + data.domain.class_vars + if isinstance(v, (DiscreteVariable, ContinuousVariable)) + ] + return len(annotation_features) >= 1 + + if valid_context(data): + self.openContext(data) + else: + # to generate valid interface even if context was not loaded + self.contextAboutToBeOpened.emit([data]) + + self.curveplot.update_view() + self.curveplot_after.update_view() + + @staticmethod + def run_task( + data: Orange.data.Table, + reference: Orange.data.Table, + pp_def, + process_reference, + state, + ): + def progress_interrupt(i: float): + state.set_progress_value(i) + if state.is_interruption_requested(): + raise InterruptException + + # Protects against running the task in succession many times, as would + # happen when adding a preprocessor (there, commit() is called twice). + # Wait 100 ms before processing - if a new task is started in meanwhile, + # allow that is easily` cancelled. + for _ in range(10): + time.sleep(0.010) + progress_interrupt(0) + + n = len(pp_def) + plist = [] + for i in range(n): + progress_interrupt(i / n * 100) + item = pp_def[i] + pp = create_preprocessor(item, reference) + plist.append(pp) + if data is not None: + data = pp(data) + progress_interrupt((i / n + 0.5 / n) * 100) + if process_reference and reference is not None and i != n - 1: + reference = pp(reference) + # if there are no preprocessors, return None instead of an empty list + preprocessor = preprocess.preprocess.PreprocessorList(plist) if plist else None + return data, preprocessor + + +if __name__ == "__main__": # pragma: no cover + from Orange.widgets.utils.widgetpreview import WidgetPreview + + WidgetPreview(OWPreprocessImage).run(Orange.data.Table("whitelight.gsf")) diff --git a/orangecontrib/snom/widgets/preprocessors/__init__.py b/orangecontrib/snom/widgets/preprocessors/__init__.py index d78515b..83e3ef9 100644 --- a/orangecontrib/snom/widgets/preprocessors/__init__.py +++ b/orangecontrib/snom/widgets/preprocessors/__init__.py @@ -1,3 +1,4 @@ # load and register editors from . import registration_example # noqa: F401 from . import phase_unwrap # noqa: F401 +from . import multiply # noqa: F401 diff --git a/orangecontrib/snom/widgets/preprocessors/multiply.py b/orangecontrib/snom/widgets/preprocessors/multiply.py new file mode 100644 index 0000000..2135c75 --- /dev/null +++ b/orangecontrib/snom/widgets/preprocessors/multiply.py @@ -0,0 +1,73 @@ +# this is just an example of registration + +from AnyQt.QtWidgets import QFormLayout + +from Orange.data import Domain +from Orange.preprocess import Preprocess +from orangecontrib.snom.widgets.preprocessors.registry import preprocess_image_editors + +from orangecontrib.spectroscopy.preprocess import SelectColumn, CommonDomain + +from orangecontrib.spectroscopy.widgets.preprocessors.utils import BaseEditorOrange +from orangecontrib.spectroscopy.widgets.gui import lineEditFloatRange + + +class AddFeature(SelectColumn): + InheritEq = True + + +class _AddCommon(CommonDomain): + def __init__(self, amount, domain): + super().__init__(domain) + self.amount = amount + + def transformed(self, data): + return data.X + self.amount + + +class AddConstant(Preprocess): + def __init__(self, amount=0.0): + self.amount = amount + + def __call__(self, data): + common = _AddCommon(self.amount, data.domain) + atts = [ + a.copy(compute_value=AddFeature(i, common)) + for i, a in enumerate(data.domain.attributes) + ] + domain = Domain(atts, data.domain.class_vars, data.domain.metas) + return data.from_table(domain, data) + + +class AddEditor(BaseEditorOrange): + name = "Add constant" + qualname = "orangecontrib.snom.add_constant_test" + + def __init__(self, parent=None, **kwargs): + super().__init__(parent, **kwargs) + + self.amount = 0.0 + + form = QFormLayout() + amounte = lineEditFloatRange(self, self, "amount", callback=self.edited.emit) + form.addRow("Addition", amounte) + self.controlArea.setLayout(form) + + def activateOptions(self): + pass # actions when user starts changing options + + def setParameters(self, params): + self.amount = params.get("amount", 0.0) + + @classmethod + def createinstance(cls, params): + params = dict(params) + amount = float(params.get("amount", 0.0)) + return AddConstant(amount=amount) + + def set_preview_data(self, data): + if data: + pass # TODO any settings + + +preprocess_image_editors.register(AddEditor, 100) diff --git a/orangecontrib/snom/widgets/preprocessors/registry.py b/orangecontrib/snom/widgets/preprocessors/registry.py new file mode 100644 index 0000000..c14e97c --- /dev/null +++ b/orangecontrib/snom/widgets/preprocessors/registry.py @@ -0,0 +1,5 @@ +from orangecontrib.spectroscopy.widgets.preprocessors.registry import ( + PreprocessorEditorRegistry, +) + +preprocess_image_editors = PreprocessorEditorRegistry() diff --git a/orangecontrib/snom/widgets/tests/test_owpreprocessimage.py b/orangecontrib/snom/widgets/tests/test_owpreprocessimage.py new file mode 100644 index 0000000..8dcde9d --- /dev/null +++ b/orangecontrib/snom/widgets/tests/test_owpreprocessimage.py @@ -0,0 +1,138 @@ +import numpy as np + +from orangewidget.tests.utils import excepthook_catch + +from Orange.data import Table, Domain +from Orange.widgets.tests.base import WidgetTest +from Orange.preprocess.preprocess import Preprocess + +from orangecontrib.spectroscopy.tests import spectral_preprocess +from orangecontrib.spectroscopy.tests.spectral_preprocess import ( + pack_editor, + wait_for_preview, +) +from orangecontrib.spectroscopy.widgets.preprocessors.misc import ( + CutEditor, + SavitzkyGolayFilteringEditor, +) + +from orangecontrib.snom.widgets.owpreprocessimage import OWPreprocessImage +from orangecontrib.snom.widgets.preprocessors.registry import preprocess_image_editors + + +PREPROCESSORS = list(map(pack_editor, preprocess_image_editors.sorted())) + + +WHITELIGHT = Table("whitelight.gsf") + + +class TestAllPreprocessors(WidgetTest): + def test_allpreproc_indv(self): + data = WHITELIGHT + for p in PREPROCESSORS: + with self.subTest(p.viewclass): + self.widget = self.create_widget(OWPreprocessImage) + self.send_signal("Data", data) + self.widget.add_preprocessor(p) + self.widget.commit.now() + wait_for_preview(self.widget) + self.wait_until_finished(timeout=10000) + + def test_allpreproc_indv_empty(self): + data = WHITELIGHT + for p in PREPROCESSORS: + with self.subTest(p.viewclass): + self.widget = self.create_widget(OWPreprocessImage) + self.send_signal("Data", data[:0]) + self.widget.add_preprocessor(p) + self.widget.commit.now() + wait_for_preview(self.widget) + self.wait_until_finished(timeout=10000) + # no attributes + data = data.transform( + Domain([], class_vars=data.domain.class_vars, metas=data.domain.metas) + ) + for p in PREPROCESSORS: + with self.subTest(p.viewclass, type="no attributes"): + self.widget = self.create_widget(OWPreprocessImage) + self.send_signal("Data", data) + self.widget.add_preprocessor(p) + self.widget.commit.now() + wait_for_preview(self.widget) + self.wait_until_finished(timeout=10000) + + +class TestOWPreprocess(WidgetTest): + def setUp(self): + self.widget = self.create_widget(OWPreprocessImage) + + def test_load_unload(self): + self.send_signal("Data", Table("iris.tab")) + self.send_signal("Data", None) + + def failing_saving_preprocessors(self): + settings = self.widget.settingsHandler.pack_data(self.widget) + self.assertEqual([], settings["storedsettings"]["preprocessors"]) + self.widget.add_preprocessor(self.widget.PREPROCESSORS[0]) + settings = self.widget.settingsHandler.pack_data(self.widget) + self.assertEqual( + self.widget.PREPROCESSORS[0].qualname, + settings["storedsettings"]["preprocessors"][0][0], + ) + + def test_output_preprocessor_without_data(self): + self.widget.add_preprocessor(pack_editor(CutEditor)) + self.widget.commit.now() + self.wait_until_finished() + out = self.get_output(self.widget.Outputs.preprocessor) + self.assertIsInstance(out, Preprocess) + + def test_empty_no_inputs(self): + self.widget.commit.now() + self.wait_until_finished() + p = self.get_output(self.widget.Outputs.preprocessor) + d = self.get_output(self.widget.Outputs.preprocessed_data) + self.assertEqual(None, p) + self.assertEqual(None, d) + + def test_no_preprocessors(self): + data = WHITELIGHT + self.send_signal(self.widget.Inputs.data, data) + self.widget.commit.now() + self.wait_until_finished() + d = self.get_output(self.widget.Outputs.preprocessed_data) + self.assertEqual(WHITELIGHT, d) + + def test_widget_vs_manual(self): + data = WHITELIGHT + self.send_signal(self.widget.Inputs.data, data) + self.widget.add_preprocessor(pack_editor(CutEditor)) + self.widget.add_preprocessor(pack_editor(SavitzkyGolayFilteringEditor)) + self.widget.commit.now() + self.wait_until_finished() + p = self.get_output(self.widget.Outputs.preprocessor) + d = self.get_output(self.widget.Outputs.preprocessed_data) + manual = p(data) + np.testing.assert_equal(d.X, manual.X) + + def test_invalid_preprocessors(self): + settings = {"storedsettings": {"preprocessors": [("xyz.abc.notme", {})]}} + with self.assertRaises(KeyError): + with excepthook_catch(raise_on_exit=True): + widget = self.create_widget(OWPreprocessImage, settings) + self.assertTrue(widget.Error.loading.is_shown()) + + +class TestPreprocessWarning(spectral_preprocess.TestWarning): + widget_cls = OWPreprocessImage + + def test_exception_preview_after_data(self): + self.editor.raise_exception = True + self.editor.edited.emit() + wait_for_preview(self.widget) + self.assertIsNone(self.widget.curveplot_after.data) + + self.editor.raise_exception = False + self.editor.edited.emit() + wait_for_preview(self.widget) + self.assertIsNotNone(self.widget.curveplot_after.data)