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

Visualize show spectra #723

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions docs/visualize_show_spectra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Show Spectra for Selected Pixels (Developing)

`ShowSpectra` is a class that is capable of handling user's selecting and clicking behavior by showing spectra of user selected pixels and storing selected coordinates as well as spectra.

*class* **plantcv.visualize.ShowSpectra(spectral_data, figsize=(12,6))**
- To initialize the ShowSpectra class, the only required parameter is `spectral_data`, which is of type `__main__.Spectral_data`.
- Another optional parameter is the desired figure size `figsize`, by default `figsize=(12,6)`.

### Attributes
**spectral_data** (`__main__.Spectral_data`, required): input hyperspectral image.

**spectra** (`list`): spectra for all selected pixels.

**points** (`list`): list of coordinates of selected pixels.

```python

from plantcv import plantcv as pcv

show_spectra = pcv.visualize.ShowSpectra(spectral_data=array)

```

Check out this video for a sample usage:
<iframe src="https://player.vimeo.com/video/522535625" width="640" height="360" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/visualize/show_spectra.py)
3 changes: 2 additions & 1 deletion plantcv/plantcv/visualize/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from plantcv.plantcv.visualize.obj_sizes import obj_sizes
from plantcv.plantcv.visualize.obj_size_ecdf import obj_size_ecdf
from plantcv.plantcv.visualize.hyper_histogram import hyper_histogram
from plantcv.plantcv.visualize.show_spectra import ShowSpectra

__all__ = ["pseudocolor", "colorize_masks", "histogram", "clustered_contours", "colorspaces", "auto_threshold_methods",
"overlay_two_imgs", "colorize_label_img", "obj_size_ecdf", "obj_sizes", "hyper_histogram"]
"overlay_two_imgs", "colorize_label_img", "obj_size_ecdf", "obj_sizes", "hyper_histogram", "ShowSpectra"]
154 changes: 154 additions & 0 deletions plantcv/plantcv/visualize/show_spectra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Show spectral or spectra of mouse selected pixel(s) for a given hyperspectral image

from scipy.spatial import distance
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.widgets import Slider
import cv2


def _find_closest(pt, pts):
""" Given coordinates of a point and a list of coordinates of a bunch of points, find the point that has the smallest Euclidean to the given point

:param pt: (tuple) coordinates of a point
:param pts: (a list of tuples) coordinates of a list of points
:return: index of the closest point and the coordinates of that point
"""
if pt in pts:
return pt
dists = distance.cdist([pt], pts, 'euclidean')
idx = np.argmin(dists)
return idx, pts[idx]


class ShowSpectra(object):
"""
An interactive visualization tool that shows spectral (spectra) for selected pixel(s).
"""

def __init__(self, spectral_data, figsize=(12, 8)):
"""
Initialization
:param spectral_data: hyperspectral image data
:param figsize: desired figure size, (12,8) by default
"""
print("Warning: this tool is under development and is expected to have updates frequently, please check the documentation page to make sure you are using the correct version!")

# initialize the pseudocolor rgb data (convert from BGR to RGB)
self.pseudo_rgb = cv2.cvtColor(spectral_data.pseudo_rgb, cv2.COLOR_BGR2RGB)

self.fig, self.axes = plt.subplots(1, 2, figsize=figsize)
self.axes[0].imshow(self.pseudo_rgb)
self.axes[0].set_title("Please click on interested pixels\n Right click to remove")

self.axes[1].set_xlabel("wavelength (nm)")
self.axes[1].set_ylabel("reflectance")
self.axes[1].set_title("Spectra")
self.axes[1].set_ylim([0, 1])

# adjust the main plot to make room for the sliders
plt.subplots_adjust(left=0.25, bottom=0.25)

# make a horizontal slider to control the radius
axradius = self.fig.add_axes([0.15, 0.035, 0.3, 0.035], facecolor='lightgoldenrodyellow') # [left, bottom, width, height]
self.radius_slider = Slider(
ax=axradius,
label="radius",
valmin=0,
valmax=500,
valinit=1,
orientation="horizontal"
)

# Set useblit=True on most backends for enhanced performance.
# cursor = Cursor(axes[0], horizOn=True, vertOn=True, useblit=True, color='red', linewidth=2)

self.points = []
self.spectra = []
self.events = []

self.array_data = spectral_data.array_data
self.wvs = [k for k in spectral_data.wavelength_dict.keys()]

self.spectral_std = None
self.spectral_mean = None

# initialize radius
self.r = 0
self.x = None
self.y = None
self.rectangle = None

self.fig.canvas.mpl_connect('button_press_event', self.onclick)
self.radius_slider.on_changed(self.update)

def spectra_roi(self):
"""Pull out the spectra inside a square ROI
"""
r = int(self.r)
y = int(self.y)
x = int(self.x)
kernel_ = np.ones((2 * r + 1, 2 * r + 1)) / (4 * r * r + 4 * r + 1)

square = self.array_data[(y - r):(y + r + 1), (x - r):(x + r + 1), :]

num_bands = square.shape[2]

kernel = np.repeat(kernel_[:, :, np.newaxis], num_bands, axis=2)

multiplied = np.multiply(square, kernel)
multiplied_spectra = np.reshape(multiplied, (-1, num_bands))

self.spectral_mean = multiplied_spectra.sum(axis=0)
self.spectral_std = multiplied_spectra.std(axis=0)
# only prepare the patch for plotting if r>1 (not only one pixel, but a square of pixels)
if r > 0:
self.rectangle = patches.Rectangle((x - r, y - r), 2 * r, 2 * r, edgecolor="red", fill=False)

def onclick(self, event):
self.events.append(event)
if str(event.inaxes._subplotspec) == 'GridSpec(1, 2)[0:1, 0:1]':
if event.button == 1:
self.x, self.y = event.xdata, event.ydata
self.axes[0].plot(event.xdata, event.ydata, 'x', c='red')
self.spectra_roi()
if self.r > 1:
self.axes[0].add_patch(self.rectangle)

self.axes[1].errorbar(self.wvs, self.spectral_mean, xerr=self.spectral_std / 2)
self.points.append((event.xdata, event.ydata))
else:
idx_remove, _ = _find_closest((event.xdata, event.ydata), self.points)
# remove the last added point
# idx_remove = -1

# remove the closest point to the user right clicked one
self.points.pop(idx_remove)
if len(self.points) > 0:
self.x, self.y = self.points[-1]
ax0plots = self.axes[0].lines
ax0patches = self.axes[0].patches
ax1plots = self.axes[1].lines
self.axes[0].lines.remove(ax0plots[idx_remove])
if len(ax0patches) > 0:
self.axes[0].patches.remove(ax0patches[idx_remove])
self.axes[1].lines.remove(ax1plots[idx_remove])
self.fig.canvas.draw()

def update(self, val):
self.r = self.radius_slider.val
self.spectra_roi()

# remove old plots
idx_remove = -1
ax0patches = self.axes[0].patches
if len(ax0patches) > 0:
self.axes[0].patches.remove(ax0patches[idx_remove])
ax1plots = self.axes[1].lines
self.axes[1].lines.remove(ax1plots[idx_remove])

# add new plots
self.axes[0].add_patch(self.rectangle)
self.axes[1].errorbar(self.wvs, self.spectral_mean, xerr=self.spectral_std / 2)
self.fig.canvas.draw_idle()
62 changes: 62 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6492,6 +6492,68 @@ def test_plantcv_visualize_overlay_two_imgs_bad_alpha():
with pytest.raises(RuntimeError):
_ = pcv.visualize.overlay_two_imgs(img1=img1, img2=img2, alpha=alpha)

def test_plantcv_visualize_show_spectra():
# read hypersectral data
spectral_filename = os.path.join(HYPERSPECTRAL_TEST_DATA, HYPERSPECTRAL_DATA)
array_data = pcv.hyperspectral.read_data(filename=spectral_filename)

# initialization
show_spectra = pcv.visualize.ShowSpectra(array_data, figsize=(12, 6))
assert len(show_spectra.events) == 0

# create mock events
e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=show_spectra.fig.canvas, x=0, y=0,button=1)
e1.inaxes = show_spectra.axes[0]
e1.inaxes._subplotspec = show_spectra.axes[0]._subplotspec
e1.xdata = 0
e1.ydata = 0

e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=show_spectra.fig.canvas, x=0, y=0, button=3)
e2.inaxes = show_spectra.axes[0]
e2.inaxes._subplotspec = show_spectra.axes[0]._subplotspec
e2.xdata = 0
e2.ydata = 0

e1_ = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=show_spectra.fig.canvas, x=0, y=0,button=1)
e1_.inaxes = show_spectra.axes[0]
e1_.inaxes._subplotspec = show_spectra.axes[0]._subplotspec
e1_.xdata = 0
e1_.ydata = 0

e3 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=show_spectra.fig.canvas, x=1, y=0, button=3)
e3.inaxes = show_spectra.axes[0]
e3.inaxes._subplotspec = show_spectra.axes[0]._subplotspec
e3.xdata = 1
e3.ydata = 0

show_spectra.onclick(e1)
show_spectra.onclick(e2)
show_spectra.onclick(e1_)
show_spectra.onclick(e1_)
show_spectra.onclick(e3)

assert len(show_spectra.events) == 5

# test for updating
# initialization
array_d = array_data.array_data
array_data.array_data = np.concatenate((array_d, array_d, array_d, array_d, array_d), axis=0)
show_spectra = pcv.visualize.ShowSpectra(array_data, figsize=(12, 6))
e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=show_spectra.fig.canvas, x=1, y=1,button=1)
e1.inaxes = show_spectra.axes[0]
e1.inaxes._subplotspec = show_spectra.axes[0]._subplotspec
e1.xdata = 2
e1.ydata = 2
show_spectra.onclick(e1)
val = 2
show_spectra.radius_slider.val = val
show_spectra.update(val)
show_spectra.onclick(e1)
show_spectra.update(val)
show_spectra.onclick(e2)
assert len(show_spectra.axes[0].patches) > 0
show_spectra.onclick(e1)
assert len(show_spectra.axes[0].patches) > 0

def test_plantcv_visualize_overlay_two_imgs_size_mismatch():
pcv.params.debug = None
Expand Down