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

EX: new kernel based concentration analysis example. #231

Merged
merged 34 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
070d0b6
EX: new kernel based concentration analysis example.
moritzmarquardt Oct 16, 2023
219da61
MAINT: Rename file.
jwboth Oct 16, 2023
e2b9126
DOC/MAINT: Add comments and fix style
jwboth Oct 16, 2023
18cf3ff
MAINT: add some class structure
jwboth Oct 16, 2023
f0e5277
MAINT: Accelerate interpolation.
jwboth Oct 16, 2023
435a8ba
ENH: Add functionlity to change data type with correct scaling
jwboth Oct 16, 2023
5bdd499
MAINT: Increase robustness.
jwboth Oct 16, 2023
a2c3ffc
MAINT: Use no cleaning filter if only 1 baseilne image provided.
jwboth Oct 16, 2023
1748675
MAINT: Rename method - more general
jwboth Oct 16, 2023
c6687b0
ENH: Add new diff type in concentration analysis.
jwboth Oct 16, 2023
ee353be
MAINT: Simplify style
jwboth Oct 16, 2023
2d10dee
EX: Integrate concentration analysis into example
jwboth Oct 16, 2023
3b96083
ENH: Provide box selection assistant.
jwboth Oct 16, 2023
bd33f1c
EX: Allow to use assistant to choose samples.
jwboth Oct 16, 2023
62ac7c2
EX: Use pre analysis for calibration.
jwboth Oct 16, 2023
cf29cf5
MAINT: Allow non-scalar output (when model is None).
jwboth Oct 16, 2023
3a797fe
new kernel classes initial comit
moritzmarquardt Oct 16, 2023
92a2049
extract characteristic data function initial
moritzmarquardt Oct 16, 2023
afc5a35
kernel interpolation class initial commit
moritzmarquardt Oct 16, 2023
13eefd4
add new files necessary for kernell interpolation
moritzmarquardt Oct 16, 2023
69d5cad
use darsia new kernel interpol functionality
moritzmarquardt Oct 16, 2023
ad3ea42
DOC: Add simple documentation.
jwboth Oct 16, 2023
947a67e
DOC: Add typing
jwboth Oct 16, 2023
5487a65
DOC: Add description of method
jwboth Oct 16, 2023
eb5cca2
DOC: Add simple documentation.
jwboth Oct 16, 2023
81040b9
EX: Reuse analysis for the better narrative.
jwboth Oct 16, 2023
df5f2d3
EX: Add example data for kernel interpolation test.
jwboth Oct 16, 2023
482173f
MAINT: Make letter placement in plot relative to image size.
jwboth Oct 16, 2023
4770c91
EX: Use predefined images, use pre-defined samples by default, close …
jwboth Oct 16, 2023
e9990a0
TST: Add kernel example to integration tests.
jwboth Oct 16, 2023
f0c81be
EX: Use OS independent paths
jwboth Oct 16, 2023
ae3b9e6
MAINT: remove indicator plot
moritzmarquardt Oct 17, 2023
6f6799a
EX: Add clip as part of model, use concentrations in % and add colorbar.
jwboth Oct 17, 2023
713871e
Merge branch 'kernel-based-concentration-analysis' of https://github.…
jwboth Oct 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
118 changes: 118 additions & 0 deletions examples/kernel_interpolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import os

import matplotlib.pyplot as plt
import numpy as np
import skimage

import darsia

# ! ---- DATA MANAGEMENT ---- !

# The example images originate from a water tracer experiment with a multi-colored
# indicator. Green color corresponds to concentrations close to 100%, while blue
# color corresponds to concentrations ~99-0%. The images have been cropped and resized
# mainly for data storage reasons. It is recommended to use full resolution images.
baseline_path = (
f"{os.path.dirname(__file__)}/images/kernel_interpolation_example_base.npz"
)
image_path = f"{os.path.dirname(__file__)}/images/kernel_interpolation_example_test.npz"
baseline = darsia.imread(baseline_path)
image = darsia.imread(image_path)

# ! ---- MAIN CONCENTRATION ANALYSIS AND CALIBRATION ---- !
"""
Before using kernel interpolation to transform the given image to a concentration image,
the support data on which the interpolation depends has to be defined.
In the thesis these were predefined samples of the smoothed and preprocessed image and
corresponding concentrations.
Now it is possible to select these samples in a GUI using darsia.BoxSelectionAssistant().
In these samples of the image, the most common colour was identified using a histogram
analysis, which is now available through darsia.extract_characteristic_data().
"""

# Predefine concentration analysis for now without any model (to be defined later).
analysis = darsia.ConcentrationAnalysis(
base=baseline.img_as(float),
restoration=darsia.TVD(
weight=0.025, eps=1e-4, max_num_iter=100, method="isotropic Bregman"
),
**{"diff option": "plain"},
)

# The goal is to define ome ROIs for which physical information is known.
# One possibility is to use a GUI for interactive use. This option can
# be activated on demand. For testing purposes this example by default
# uses a pre-defined sample selection.
if True:
samples = [
(slice(15, 40), slice(20, 45)),
(slice(15, 40), slice(220, 245)),
(slice(15, 40), slice(420, 445)),
(slice(15, 40), slice(720, 745)),
]
else:
# Same but under the use of a graphical user interface.
# Ask user to provide characteristic regions with expected concentration values
assistant = darsia.BoxSelectionAssistant(image)
samples = assistant()

# For simplicity the concentrations are pre-defined. These could also be defined
# by the user.
n = len(samples)
concentrations = np.append(np.linspace(1, 0.99, n - 1), 0)

# Now add kernel interpolation as model trained by the extracted information.
"""
comments:
- Here, the support points as defined above are used to build a Kernel Interpolation.
For this, the 'concentration' without any model is used, correpsonding to the difference
to the baseline.
- This Kernel interpolation is then used as the model in the darsia.ConcentrationAnalysis
- use plain difference to keep all information (no cut off or norm)
this is the advantage of the kernel interpolation - it can handle negative colours
- finding the right kernel parameters is part of the modeling
"""
smooth_RGB = analysis(image.img_as(float)).img
colours_RGB = darsia.extract_characteristic_data(
signal=smooth_RGB, samples=samples, verbosity=True, surpress_plot=True
)
analysis.model = darsia.KernelInterpolation(
darsia.GaussianKernel(gamma=9.73), colours_RGB, concentrations
)

# Finally, apply the (full) concentration analysis to analyze the test image
concentration_image = analysis(image.img_as(float)).img

# ! ----- VISUALISATION ---- !

plt.figure("concentration average along one dimension")
plt.plot(np.average(concentration_image, axis=0))
plt.xlabel("horizontal pixel")
plt.ylabel("concentration")

concentration_image[
concentration_image > 1
] = 1 # für visualisierung von größer 1 values
moritzmarquardt marked this conversation as resolved.
Show resolved Hide resolved
concentration_image[concentration_image < 0] = 0
fig = plt.figure()
fig.suptitle("original image and resulting concentration")
ax = plt.subplot(212)
ax.imshow(concentration_image)
ax.set_ylabel("vertical pixel")
ax.set_xlabel("horizontal pixel")
ax = plt.subplot(211)
ax.imshow(skimage.img_as_ubyte(image.img))
ax.set_ylabel("vertical pixel")
ax.set_xlabel("horizontal pixel")

# plt.figure("indicator")
# indicator = np.arange(101) / 100
# plt.axis("off")
# plt.imshow([indicator, indicator, indicator, indicator, indicator])
moritzmarquardt marked this conversation as resolved.
Show resolved Hide resolved

# To enable the example as test, the plots are closed after short time.
# Pause longer if it is desired to keep the images on the screen.
print("Warning: The plot is closed after short time to enable testing.")
plt.show(block=False)
plt.pause(5)
plt.close()
4 changes: 4 additions & 0 deletions src/darsia/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from darsia.utils.dtype import *
from darsia.utils.grid import *
from darsia.utils.fv import *
from darsia.utils.kernels import *
from darsia.utils.extractcharacteristicdata import *
from darsia.corrections.basecorrection import *
from darsia.corrections.shape.curvature import *
from darsia.corrections.shape.affine import *
Expand All @@ -46,6 +48,7 @@
from darsia.signals.models.staticthresholdmodel import *
from darsia.signals.models.dynamicthresholdmodel import *
from darsia.signals.models.binarydataselector import *
from darsia.signals.models.kernelinterpolation import *
from darsia.signals.reduction.signalreduction import *
from darsia.signals.reduction.monochromatic import *
from darsia.signals.reduction.dimensionreduction import *
Expand All @@ -68,6 +71,7 @@
from darsia.manager.co2analysis import *
from darsia.assistants.base_assistant import *
from darsia.assistants.point_selection_assistant import *
from darsia.assistants.box_selection_assistant import *
from darsia.assistants.rotation_correction_assistant import *
from darsia.assistants.subregion_assistant import *
from darsia.assistants.crop_assistant import *
Expand Down
62 changes: 62 additions & 0 deletions src/darsia/assistants/box_selection_assistant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Module for defining subregions interactively."""

from typing import Optional

import numpy as np

import darsia


class BoxSelectionAssistant(darsia.PointSelectionAssistant):
def __init__(self, img: darsia.Image, **kwargs) -> None:
super().__init__(img)

self.name = "Box selection assistant"
"""Name of assistant."""

self.width = kwargs.get("width", 100)
"""Width and height of selected box(es)."""

self.boxes = None
"""Output of assistant."""

self.shape = img.shape
"""Image shape."""

self.dim = img.space_dim
"""Dimension of image."""

if self.dim != 2:
raise NotImplementedError

def _convert_pts(self) -> None:
"""Convert list of points to list of boxes in terms of slices."""

self.boxes = []
half_width = self.width / 2
# Point selection assistant produces points with cartesian ordering
pts_matrix_indexing = [np.flip(pt) for pt in self.pts]
for pt in pts_matrix_indexing:
self.boxes.append(
tuple(
[
slice(
max(int(pt[d] - half_width), 0),
min(int(pt[d] + half_width), self.shape[d]),
)
for d in range(self.dim)
]
)
)

def __call__(self) -> Optional[np.ndarray]:
"""Call the assistant."""

super().__call__()
self._convert_pts()
return self.boxes

def _print_info(self) -> None:
"""Print info about boxes so far assigned by the assistant."""
self._convert_pts()
print(self.boxes)
30 changes: 30 additions & 0 deletions src/darsia/image/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,36 @@ def astype(self, data_type) -> Any:

return copy_image

def img_as(self, data_type) -> Any:
"""Change data type via skimage.

Args:
data_type: target data type

Returns:
Image: image with transformed data type

"""
copy_image = self.copy()
if data_type in [bool]:
copy_image.img = skimage.img_as_bool(copy_image.img)
elif data_type in [float]:
copy_image.img = skimage.img_as_float(copy_image.img)
elif data_type in [np.float32]:
copy_image.img = skimage.img_as_float32(copy_image.img)
elif data_type in [np.float64]:
copy_image.img = skimage.img_as_float64(copy_image.img)
elif data_type in [int]:
copy_image.img = skimage.img_as_int(copy_image.img)
elif data_type in [np.uint8]:
copy_image.img = skimage.img_as_ubyte(copy_image.img)
elif data_type in [np.uint16]:
copy_image.img = skimage.img_as_uint(copy_image.img)
else:
raise NotImplementedError

return copy_image

# ! ---- Extraction routines

def metadata(self) -> dict:
Expand Down
23 changes: 15 additions & 8 deletions src/darsia/multi_image_analysis/concentrationanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ def find_cleaning_filter(

# Use internal baseline images, if available.
baseline_images = self._base_collection[1:]
if len(baseline_images) == 0:
baseline_images = None

self.threshold_cleaning_filter = None
"""Cleaning filter."""
Expand All @@ -171,7 +173,7 @@ def find_cleaning_filter(
diff = self._subtract_background(probe_img)

# Extract scalar version
monochromatic_diff = self._extract_scalar_information(diff)
monochromatic_diff = self._reduce_signal(diff)

# Consider elementwise max
self.threshold_cleaning_filter = np.maximum(
Expand Down Expand Up @@ -230,7 +232,7 @@ def __call__(self, img: darsia.Image) -> darsia.Image:
self._inspect_diff(diff)

# Extract monochromatic version and take difference wrt the baseline image
signal = self._extract_scalar_information(diff)
signal = self._reduce_signal(diff)

# Provide possibility for tuning and inspection of intermediate results
self._inspect_scalar_signal(signal)
Expand All @@ -255,7 +257,11 @@ def __call__(self, img: darsia.Image) -> darsia.Image:
plt.show()

metadata = img.metadata()
return darsia.ScalarImage(concentration, **metadata)
is_scalar = len(concentration.shape) == len(img.shape) - 1
if is_scalar:
return darsia.ScalarImage(concentration, **metadata)
else:
return type(img)(concentration, **metadata)

# ! ---- Inspection routines
def _inspect_diff(self, img: np.ndarray) -> None:
Expand Down Expand Up @@ -318,6 +324,8 @@ def _subtract_background(self, img: darsia.Image) -> darsia.Image:
diff = np.clip(-img.img, 0, None)
elif self._diff_option == "absolute":
diff = np.absolute(img.img)
elif self._diff_option == "plain":
diff = img.img
else:
raise ValueError(f"Diff option {self._diff_option} not supported")
else:
Expand All @@ -330,12 +338,14 @@ def _subtract_background(self, img: darsia.Image) -> darsia.Image:
diff = skimage.util.compare_images(
img.img, self.base.img, method="diff"
)
elif self._diff_option == "plain":
diff = img.img - self.base.img
else:
raise ValueError(f"Diff option {self._diff_option} not supported")

return diff

def _extract_scalar_information(self, img: np.ndarray) -> np.ndarray:
def _reduce_signal(self, img: np.ndarray) -> np.ndarray:
"""
Make a scalar image from potentially multi-colored image.

Expand Down Expand Up @@ -382,10 +392,7 @@ def _balance_signal(self, img: np.ndarray) -> np.ndarray:
np.ndarray: balanced image

"""
if self.balancing is not None:
return self.balancing(img)
else:
return img
return img if self.balancing is None else self.balancing(img)

def _prepare_signal(self, signal: np.ndarray) -> np.ndarray:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/darsia/restoration/tvd.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, key: str = "", **kwargs) -> None:

"""
# Determine method
self.method = kwargs.pop(key + "method", "chambolle")
self.method = kwargs.pop(key + "method", "chambolle").lower()

# Method-specific parameters
if self.method == "heterogeneous bregman":
Expand Down
Loading
Loading