Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using image parameters for preprocessing #14

Merged
merged 8 commits into from
Sep 30, 2024
94 changes: 94 additions & 0 deletions orangecontrib/snom/preprocess/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import numpy as np

from Orange.data import Domain
from Orange.preprocess import Preprocess
from orangecontrib.spectroscopy.preprocess import CommonDomain, SelectColumn
from orangecontrib.spectroscopy.utils import (
InvalidAxisException,
values_to_linspace,
index_values,
)


class PreprocessImageOpts(Preprocess):
pass


class PreprocessImageOpts2D(PreprocessImageOpts):
def __call__(self, data, image_opts):
common = self.image_transformer(data, image_opts)
at = data.domain[image_opts["attr_value"]].copy(
compute_value=SelectColumn(0, common)
)
domain = domain_with_single_attribute_in_x(at, data.domain)
return data.transform(domain)

def image_transformer(self, data, image_opts):
raise NotImplementedError


def axes_to_ndim_linspace(coordinates):
# modified to avoid domains as much as possible
ls = []
indices = []

for i in range(coordinates.shape[1]):
coor = coordinates[:, i]
lsa = values_to_linspace(coor)
if lsa is None:
raise InvalidAxisException(i)
ls.append(lsa)
indices.append(index_values(coor, lsa))

return ls, tuple(indices)


def get_ndim_hyperspec(data, attrs):
# mostly copied from orangecontrib.spectroscopy.utils,
# but returns the indices too
# also avoid Orange domains as much as possible
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about this, is it faster? Should we just/also change the orangecontrib.spectroscopy.utils implementation? I think it used domains because get_column() didn't exist at that time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it because it seemed nicer to avoid domains where there are not needed. I do not expect any performance speedups, and yes, we could also do it in Spectroscopy.

coordinates = np.hstack([data.get_column(a).reshape(-1, 1) for a in attrs])

ls, indices = axes_to_ndim_linspace(coordinates)

# set data
new_shape = tuple([lsa[2] for lsa in ls]) + (data.X.shape[1],)
hyperspec = np.ones(new_shape) * np.nan

hyperspec[indices] = data.X

return hyperspec, ls, indices


def domain_with_single_attribute_in_x(attribute, domain):
"""Create a domain with only the attribute in domain.attributes and ensure
that the same attribute is removed from metas and class_vars if it was present
there."""
class_vars = [a for a in domain.class_vars if a.name != attribute.name]
metas = [a for a in domain.metas if a.name != attribute.name]
return Domain([attribute], class_vars, metas)


class CommonDomainImage2D(CommonDomain):
def __init__(self, domain: Domain, image_opts: dict):
self.domain = domain
self.image_opts = image_opts

def __call__(self, data):
data = self.transform_domain(data)
vat = data.domain[self.image_opts["attr_value"]]
ndom = domain_with_single_attribute_in_x(vat, data.domain)
data = data.transform(ndom)
try:
hypercube, _, indices = get_ndim_hyperspec(
data, (self.image_opts["attr_x"], self.image_opts["attr_y"])
)
image = hypercube[:, :, 0]
transformed = self.transform_image(image)
return transformed[indices].reshape(-1, 1)
except InvalidAxisException:
return np.full((len(data), 1), np.nan)
return self.transformed(data)

def transform_image(self, image):
raise NotImplementedError
89 changes: 81 additions & 8 deletions orangecontrib/snom/widgets/owpreprocessimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from Orange.widgets.settings import DomainContextHandler
from Orange.widgets.utils.itemmodels import DomainModel
from orangecontrib.snom.widgets.preprocessors.registry import preprocess_image_editors
from orangecontrib.snom.preprocess.utils import PreprocessImageOpts
from orangewidget import gui
from orangewidget.settings import SettingProvider, ContextSetting, Setting

Expand All @@ -19,6 +20,7 @@
GeneralPreprocess,
create_preprocessor,
InterruptException,
PreviewRunner,
)

from orangewidget.widget import Msg
Expand Down Expand Up @@ -67,6 +69,69 @@ def onDeleteWidget(self):
ImagePreviews.shutdown(self)


def execute_with_image_opts(pp, data, image_opts):
if isinstance(pp, PreprocessImageOpts):
return pp(data, image_opts)
return pp(data)


class ImagePreviewRunner(PreviewRunner):
def show_preview(self, show_info_anyway=False):
"""Shows preview and also passes preview data to the widgets"""
master = self.master
self.preview_pos = master.flow_view.preview_n()
self.last_partial = None
self.show_info_anyway = show_info_anyway
self.preview_data = None
self.after_data = None
pp_def = [
master.preprocessormodel.item(i)
for i in range(master.preprocessormodel.rowCount())
]
if master.data is not None:
data = master.sample_data(master.data)
image_opts = master.image_opts()
self.start(
self.run_preview,
data,
master.reference_data,
image_opts,
pp_def,
master.process_reference,
)
else:
master.curveplot.set_data(None)
master.curveplot_after.set_data(None)

@staticmethod
def run_preview(
data: Orange.data.Table,
reference: Orange.data.Table,
image_opts,
pp_def,
process_reference,
state,
):
def progress_interrupt(i: float):
if state.is_interruption_requested():
raise InterruptException

n = len(pp_def)
orig_data = data
for i in range(n):
progress_interrupt(0)
state.set_partial_result((i, data, reference))
item = pp_def[i]
pp = create_preprocessor(item, reference)
data = execute_with_image_opts(pp, data, image_opts)
progress_interrupt(0)
if process_reference and reference is not None and i != n - 1:
reference = execute_with_image_opts(pp, reference, image_opts)
progress_interrupt(0)
state.set_partial_result((n, data, None))
return orig_data, data


class OWPreprocessImage(SpectralImagePreprocess):
name = "Preprocess image"
id = "orangecontrib.snom.widgets.preprocessimage"
Expand Down Expand Up @@ -112,6 +177,8 @@ def __init__(self):
self.markings_list = []
super().__init__()

self.preview_runner = ImagePreviewRunner(self)

self.feature_value_model = DomainModel(
order=(
DomainModel.ATTRIBUTES,
Expand Down Expand Up @@ -173,10 +240,10 @@ def update_attr(self):
self.curveplot.attr_y = self.attr_y
self.curveplot_after.attr_x = self.attr_x
self.curveplot_after.attr_y = self.attr_y
self.redraw_data()
self.on_modelchanged()

def update_feature_value(self):
self.redraw_data()
self.on_modelchanged()

def redraw_data(self):
self.curveplot.update_view()
Expand All @@ -197,19 +264,25 @@ def init_attr_values(self, data):
self.attr_x = self.xy_model[0] if self.xy_model else None
self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 else self.attr_x

def show_preview(self, show_info_anyway=False):
super().show_preview(False)
def image_opts(self):
return {
'attr_x': str(self.attr_x),
'attr_y': str(self.attr_y),
'attr_value': str(self.attr_value),
}

def create_outputs(self):
self._reference_compat_warning()
pp_def = [
self.preprocessormodel.item(i)
for i in range(self.preprocessormodel.rowCount())
]
image_opts = self.image_opts()
self.start(
self.run_task,
self.data,
self.reference_data,
image_opts,
pp_def,
self.process_reference,
)
Expand All @@ -235,13 +308,13 @@ def valid_context(data):
# to generate valid interface even if context was not loaded
self.contextAboutToBeOpened.emit([data])

self.curveplot.update_view()
self.curveplot_after.update_view()
self.redraw_data()

@staticmethod
def run_task(
data: Orange.data.Table,
reference: Orange.data.Table,
image_opts,
pp_def,
process_reference,
state,
Expand All @@ -267,10 +340,10 @@ def progress_interrupt(i: float):
pp = create_preprocessor(item, reference)
plist.append(pp)
if data is not None:
data = pp(data)
data = execute_with_image_opts(pp, data, image_opts)
progress_interrupt((i / n + 0.5 / n) * 100)
if process_reference and reference is not None and i != n - 1:
reference = pp(reference)
reference = execute_with_image_opts(pp, reference, image_opts)
# if there are no preprocessors, return None instead of an empty list
preprocessor = preprocess.preprocess.PreprocessorList(plist) if plist else None
return data, preprocessor
Expand Down
48 changes: 17 additions & 31 deletions orangecontrib/snom/widgets/preprocessors/background_fit.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,37 @@
# 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.snom.widgets.preprocessors.utils import reshape_to_image

from orangecontrib.spectroscopy.preprocess import SelectColumn, CommonDomain

from orangecontrib.spectroscopy.widgets.preprocessors.utils import BaseEditorOrange
from orangecontrib.spectroscopy.widgets.gui import lineEditIntRange

from pySNOM.images import BackgroundPolyFit
import numpy as np


class AddFeature(SelectColumn):
InheritEq = True
from orangecontrib.snom.preprocess.utils import (
PreprocessImageOpts2D,
CommonDomainImage2D,
)
from orangecontrib.snom.widgets.preprocessors.registry import preprocess_image_editors


class _BackGroundFitCommon(CommonDomain):
def __init__(self, xorder, yorder, domain):
super().__init__(domain)
class _BackGroundFitCommon(CommonDomainImage2D):
def __init__(self, xorder, yorder, domain, image_opts):
super().__init__(domain, image_opts)
self.xorder = xorder
self.yorder = yorder

def transformed(self, data):
im = reshape_to_image(data.X, data.metas[:, 0], data.metas[:, 1])
d, b = BackgroundPolyFit(xorder=self.xorder, yorder=self.yorder).transform(im)
return np.reshape(d, (-1, 1))
def transform_image(self, image):
d, b = BackgroundPolyFit(xorder=self.xorder, yorder=self.yorder).transform(
image
)
return d


class BackGroundFit(Preprocess):
class BackGroundFit(PreprocessImageOpts2D):
def __init__(self, xorder=1, yorder=1):
self.xorder = xorder
self.yorder = yorder

def __call__(self, data):
common = _BackGroundFitCommon(self.xorder, self.yorder, 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)
def image_transformer(self, data, image_opts):
return _BackGroundFitCommon(self.xorder, self.yorder, data.domain, image_opts)


class BackGroundFitEditor(BaseEditorOrange):
Expand Down Expand Up @@ -84,4 +70,4 @@ def set_preview_data(self, data):
pass # TODO any settings


# preprocess_image_editors.register(BackGroundFitEditor, 400)
preprocess_image_editors.register(BackGroundFitEditor, 400)
35 changes: 12 additions & 23 deletions orangecontrib/snom/widgets/preprocessors/linelevel.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,33 @@
from AnyQt.QtWidgets import QFormLayout

from Orange.data import Domain
from Orange.preprocess import Preprocess

from orangewidget.gui import comboBox

from pySNOM.images import LineLevel

from orangecontrib.spectroscopy.preprocess import SelectColumn, CommonDomain
from orangecontrib.spectroscopy.widgets.preprocessors.utils import BaseEditorOrange

from orangecontrib.snom.widgets.preprocessors.registry import preprocess_image_editors
from orangecontrib.snom.preprocess.utils import (
PreprocessImageOpts2D,
CommonDomainImage2D,
)


class AddFeature(SelectColumn):
InheritEq = True


class _LineLevelCommon(CommonDomain):
def __init__(self, method, domain):
super().__init__(domain)
class _LineLevelCommon(CommonDomainImage2D):
def __init__(self, method, domain, image_opts):
super().__init__(domain, image_opts)
self.method = method

def transformed(self, data):
# TODO figure out 1D to 2D properly
return LineLevel(method=self.method).transform(data.X)
def transform_image(self, image):
return LineLevel(method=self.method).transform(image)


class LineLevelProcessor(Preprocess):
class LineLevelProcessor(PreprocessImageOpts2D):
def __init__(self, method="median"):
self.method = method

def __call__(self, data):
common = _LineLevelCommon(self.method, 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)
def image_transformer(self, data, image_opts):
return _LineLevelCommon(self.method, data.domain, image_opts)


class LineLevelEditor(BaseEditorOrange):
Expand Down
Loading