-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add View3dImageProcessor, add interactive methods to View3d
- Loading branch information
Showing
5 changed files
with
431 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
opencsp/common/lib/cv/spot_analysis/image_processor/View3dImageProcessor.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
from typing import Callable | ||
|
||
import cv2 as cv | ||
import matplotlib.backend_bases | ||
import numpy as np | ||
|
||
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisOperable import SpotAnalysisOperable | ||
from opencsp.common.lib.cv.spot_analysis.image_processor.AbstractSpotAnalysisImageProcessor import ( | ||
AbstractSpotAnalysisImagesProcessor, | ||
) | ||
import opencsp.common.lib.render.figure_management as fm | ||
import opencsp.common.lib.render_control.RenderControlAxis as rca | ||
import opencsp.common.lib.render_control.RenderControlFigure as rcf | ||
import opencsp.common.lib.render_control.RenderControlSurface as rcs | ||
import opencsp.common.lib.tool.file_tools as ft | ||
|
||
|
||
class View3dImageProcessor(AbstractSpotAnalysisImagesProcessor): | ||
def __init__( | ||
self, | ||
label: str | rca.RenderControlAxis = 'Light Intensity', | ||
interactive: bool | Callable[[SpotAnalysisOperable], bool] = False, | ||
max_resolution: tuple[int, int] | None = None, | ||
crop_to_threshold: int | None = None, | ||
): | ||
""" | ||
Interprets the current image as a 3D surface plot and either displays it, or if interactive it displays the | ||
surface and waits on the next press of the "enter" key. | ||
Parameters | ||
---------- | ||
label : str | rca.RenderControlAxis, optional | ||
The label to use for the window title, by default 'Light Intensity' | ||
interactive : bool | Callable[[SpotAnalysisOperable], bool], optional | ||
If True then the spot analysis pipeline is paused until the user presses the "enter" key, by default False | ||
max_resolution : tuple[int, int] | None, optional | ||
Limits the resolution along the x and y axes to the given values. No limit if None. By default None. | ||
crop_to_threshold : int | None, optional | ||
Crops the image on the x and y axis to the first/last value >= the given threshold. None to not crop the | ||
image. Useful when trying to inspect hot spots on images with very concentrated values. By default None. | ||
""" | ||
super().__init__(self.__class__.__name__) | ||
|
||
self.interactive = interactive | ||
self.enter_pressed = False | ||
self.closed = False | ||
self.max_resolution = max_resolution | ||
self.crop_to_threshold = crop_to_threshold | ||
|
||
self.rcf = rcf.RenderControlFigure(tile=False) | ||
if isinstance(label, str): | ||
self.rca = rca.RenderControlAxis(z_label=label) | ||
else: | ||
self.rca = label | ||
self.rcs = rcs.RenderControlSurface(alpha=1.0, color=None, contour='xyz') | ||
|
||
self._init_figure_record() | ||
|
||
def _init_figure_record(self): | ||
self.fig_record = fm.setup_figure_for_3d_data( | ||
self.rcf, | ||
self.rca, | ||
equal=False, | ||
number_in_name=False, | ||
name=self.rca.z_label, | ||
code_tag=f"{__file__}.__init__()", | ||
) | ||
self.view = self.fig_record.view | ||
self.axes = self.fig_record.figure.gca() | ||
|
||
self.enter_pressed = False | ||
self.closed = False | ||
self.fig_record.figure.canvas.mpl_connect('close_event', self.on_close) | ||
self.fig_record.figure.canvas.mpl_connect('key_release_event', self.on_key_release) | ||
|
||
def on_key_release(self, event: matplotlib.backend_bases.KeyEvent): | ||
if event.key == "enter" or event.key == "return": | ||
self.enter_pressed = True | ||
|
||
def on_close(self, event: matplotlib.backend_bases.CloseEvent): | ||
self.closed = True | ||
|
||
def _get_range_for_threshold(self, image: np.ndarray, threshold: int, axis: int) -> tuple[int, int]: | ||
""" | ||
Get the start (inclusive) and end (exclusive) range for which the given image is >= the given threshold. | ||
Parameters | ||
---------- | ||
image : np.ndarray | ||
The 2d numpy array to be searched. | ||
threshold : int | ||
The cutoff value that the returned region should have pixels greater than. | ||
axis : int | ||
0 for rows (y), 1 for columns (x) | ||
Returns | ||
------- | ||
start, end: tuple[int, int] | ||
The start (inclusive) and end (exclusive) matching range. Returns the full image size if there are no | ||
matching pixels. | ||
""" | ||
# If we want the maximum value for all rows, then we need to accumulate across columns. | ||
# If we want the maximum value for all columns, then we need to accumulate across rows. | ||
perpendicular_axis = 0 if axis == 1 else 1 | ||
|
||
# find matches | ||
img_matching = np.max(image, perpendicular_axis) >= self.crop_to_threshold | ||
match_idxs = np.argwhere(img_matching) | ||
|
||
# find the range | ||
if match_idxs.size > 0: | ||
start, end = match_idxs[0][0], match_idxs[-1][0] + 1 | ||
else: | ||
start, end = 0, image.shape[axis] | ||
|
||
return start, end | ||
|
||
def _execute(self, operable: SpotAnalysisOperable, is_last: bool) -> list[SpotAnalysisOperable]: | ||
image = operable.primary_image.nparray | ||
image_path = ( | ||
operable.primary_image_source_path | ||
or operable.primary_image.source_path | ||
or operable.primary_image.cache_path | ||
) | ||
|
||
# check if the view has been closed | ||
if self.closed: | ||
# UI design decision: it feels more natural to me (Ben) for the plot to not be shown again when it has | ||
# been closed instead of being reinitialized and popping back up. | ||
return [operable] | ||
# self._init_figure_record() | ||
|
||
# reduce data based on threshold | ||
if self.crop_to_threshold is not None: | ||
x_start, x_end = self._get_range_for_threshold(image, self.crop_to_threshold, 1) | ||
y_start, y_end = self._get_range_for_threshold(image, self.crop_to_threshold, 0) | ||
image = image[y_start:y_end, x_start:x_end] | ||
|
||
# reduce data based on max_resolution | ||
if self.max_resolution is not None: | ||
width = np.min([image.shape[1], self.max_resolution[0]]) | ||
height = np.min([image.shape[0], self.max_resolution[1]]) | ||
image = cv.resize(image, (height, width), interpolation=cv.INTER_AREA) | ||
|
||
# Clear the previous data | ||
self.fig_record.view.clear() | ||
|
||
# Update the title | ||
_, image_name, _ = ft.path_components(image_path) | ||
self.fig_record.title = image_name | ||
|
||
# Draw the new data | ||
self.view.draw_xyz_surface(image, self.rcs) | ||
|
||
# draw | ||
self.view.show(block=False) | ||
|
||
# wait for the user to press enter | ||
wait_for_enter_key = self.interactive if isinstance(self.interactive, bool) else self.interactive(operable) | ||
if wait_for_enter_key: | ||
self.enter_pressed = False | ||
while True: | ||
if self.enter_pressed or self.closed: | ||
break | ||
self.fig_record.figure.waitforbuttonpress(0.1) | ||
|
||
return [operable] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.