diff --git a/contrib/common/lib/cv/spot_analysis/image_processor/ViewAnnotationsImageProcessor.py b/contrib/common/lib/cv/spot_analysis/image_processor/ViewAnnotationsImageProcessor.py index 30b64694..70c7f52d 100644 --- a/contrib/common/lib/cv/spot_analysis/image_processor/ViewAnnotationsImageProcessor.py +++ b/contrib/common/lib/cv/spot_analysis/image_processor/ViewAnnotationsImageProcessor.py @@ -120,8 +120,9 @@ def visualize_operable( for fiducials in to_render: fiducials.render_to_figure(self.figure, image, include_label) - # show the visualization - self.figure.view.show(block=False, legend=include_label) + # show the legend + if include_label: + self.figure.figure.legend() return [self.figure] diff --git a/contrib/common/lib/cv/spot_analysis/image_processor/ViewHighlightImageProcessor.py b/contrib/common/lib/cv/spot_analysis/image_processor/ViewHighlightImageProcessor.py index f1dc936d..530d0a0c 100644 --- a/contrib/common/lib/cv/spot_analysis/image_processor/ViewHighlightImageProcessor.py +++ b/contrib/common/lib/cv/spot_analysis/image_processor/ViewHighlightImageProcessor.py @@ -116,7 +116,6 @@ def visualize_operable( # show the visualization self.prepare_figure_records([self.figure], [new_image.shape]) self.figure.view.imshow(new_image) - self.figure.view.show(block=False) # build the return value cacheable_image = CacheableImage(new_image) diff --git a/opencsp/common/lib/cv/spot_analysis/VisualizationCoordinator.py b/opencsp/common/lib/cv/spot_analysis/VisualizationCoordinator.py index 3c96ee32..d2c8dc36 100644 --- a/opencsp/common/lib/cv/spot_analysis/VisualizationCoordinator.py +++ b/opencsp/common/lib/cv/spot_analysis/VisualizationCoordinator.py @@ -11,6 +11,7 @@ import opencsp.common.lib.render_control.RenderControlFigure as rcf import opencsp.common.lib.render_control.RenderControlFigureRecord as rcfr import opencsp.common.lib.tool.log_tools as lt +import opencsp.common.lib.tool.system_tools as st class VisualizationCoordinator: @@ -304,6 +305,9 @@ def visualize( processor_visualizations, _visualization_image_no_axes = visualization_processor._visualize_operable( operable, is_last ) + if not st.is_notebook(): + for fig_record in visualization_processor._initialized_figure_records: + fig_record.view.show(block=False) # compile all visualizations together into a single operable to be returned if len(processor_visualizations) > 0: diff --git a/opencsp/common/lib/cv/spot_analysis/image_processor/AbstractVisualizationImageProcessor.py b/opencsp/common/lib/cv/spot_analysis/image_processor/AbstractVisualizationImageProcessor.py index 954d752e..0e71db15 100644 --- a/opencsp/common/lib/cv/spot_analysis/image_processor/AbstractVisualizationImageProcessor.py +++ b/opencsp/common/lib/cv/spot_analysis/image_processor/AbstractVisualizationImageProcessor.py @@ -17,6 +17,7 @@ import opencsp.common.lib.render_control.RenderControlFigureRecord as rcfr import opencsp.common.lib.tool.image_tools as it import opencsp.common.lib.tool.log_tools as lt +import opencsp.common.lib.tool.system_tools as st class AbstractVisualizationImageProcessor(AbstractSpotAnalysisImageProcessor, ABC): @@ -156,6 +157,8 @@ def __init__( visualization image processor that has its base_image_selector set to 'visualization', and then the value is unset. """ + self._initialized_figure_records: weakref.WeakSet[rcfr.RenderControlFigureRecord] = weakref.WeakSet() + """ The figure records returned from init_figure_records(). """ @property @abstractmethod @@ -299,6 +302,9 @@ def _init_figure_records(self, render_control_fig: rcf.RenderControlFigure) -> l """ self._render_control_fig = weakref.ref(render_control_fig) ret = self.init_figure_records(render_control_fig) + self._initialized_figure_records.clear() + for fig_record in ret: + self._initialized_figure_records.add(fig_record) self.initialized_figure_records = True return ret @@ -390,6 +396,11 @@ def _execute(self, operable: SpotAnalysisOperable, is_last: bool) -> list[SpotAn self._init_figure_records(render_control) new_visualizations, _visualization_image_no_axes = self._visualize_operable(operable, is_last) + # draw the visualizations + if not st.is_notebook(): + for fig_record in self._initialized_figure_records: + fig_record.view.show(block=False) + # get the visualization images list visualization_images = copy.copy(operable.visualization_images) if self not in visualization_images: diff --git a/opencsp/common/lib/cv/spot_analysis/image_processor/View3dImageProcessor.py b/opencsp/common/lib/cv/spot_analysis/image_processor/View3dImageProcessor.py index e343ef13..032a14e2 100644 --- a/opencsp/common/lib/cv/spot_analysis/image_processor/View3dImageProcessor.py +++ b/opencsp/common/lib/cv/spot_analysis/image_processor/View3dImageProcessor.py @@ -119,9 +119,6 @@ def visualize_operable( x_mesh, y_mesh = np.meshgrid(x_arr, y_arr) self.view.draw_xyz_surface_customshape(x_mesh, y_mesh, image, self.rcs) - # draw - self.view.show(block=False) - return [self.fig_record] def close_figures(self): diff --git a/opencsp/common/lib/cv/spot_analysis/image_processor/ViewCrossSectionImageProcessor.py b/opencsp/common/lib/cv/spot_analysis/image_processor/ViewCrossSectionImageProcessor.py index 82982733..504f361a 100644 --- a/opencsp/common/lib/cv/spot_analysis/image_processor/ViewCrossSectionImageProcessor.py +++ b/opencsp/common/lib/cv/spot_analysis/image_processor/ViewCrossSectionImageProcessor.py @@ -332,13 +332,9 @@ def visualize_operable( # Draw the image w/ cross section line overlays i_view = self.views[0] - tc = lambda x, y: operable.transform_coordinates(p2.Pxy((x, y)))[1].astuple() - img_xy, img_xy2 = tc(0, 0), tc(cropped_width, cropped_height) - i_view.draw_image( - base_image.nparray, img_xy, (img_xy2[0] - img_xy[0], img_xy2[1] - img_xy[1]), invert_ylim=True - ) - i_view.draw_pq_list([tc(cs_cropped_x, 0), tc(cs_cropped_x, cropped_height)], style=vstyle) - i_view.draw_pq_list([tc(0, cs_cropped_y), tc(cropped_width, cs_cropped_y)], style=hstyle) + i_view.draw_image(base_image, (0, 0), (cropped_width, cropped_height)) + i_view.draw_pq_list([(cs_cropped_x, 0), (cs_cropped_x, cropped_height)], style=vstyle) + i_view.draw_pq_list([(0, cs_cropped_y_mlab), (cropped_width, cs_cropped_y_mlab)], style=hstyle) # Draw the cross sections for the no-sun image. # Draw the cross sections for the primary image using the same axes. @@ -346,16 +342,10 @@ def visualize_operable( graphs_per_plot_cnt += self._draw_null_image_cross_section(operable, cs_loc_cropped, cropped_region) graphs_per_plot_cnt += self._draw_cross_section(operable, np_image, cs_loc, cropped_region) - # draw - first_view = True - for view in self.views: - legend = graphs_per_plot_cnt > 1 - - # jhs modified, only the first view has actual data to display in the enclosed_energy jupyter notebook - # example, so I am only showing that one here. Also, this is true for the target_identification example - if first_view: - view.show(block=False, legend=legend) - first_view = False + # add the legend + if graphs_per_plot_cnt > 1: + for fig_record in self._figure_records: + fig_record.figure.legend() # explicitly set the y-axis range if self.y_range is not None: diff --git a/opencsp/common/lib/cv/spot_analysis/image_processor/ViewFalseColorImageProcessor.py b/opencsp/common/lib/cv/spot_analysis/image_processor/ViewFalseColorImageProcessor.py index 548d8d03..7573034d 100644 --- a/opencsp/common/lib/cv/spot_analysis/image_processor/ViewFalseColorImageProcessor.py +++ b/opencsp/common/lib/cv/spot_analysis/image_processor/ViewFalseColorImageProcessor.py @@ -148,11 +148,9 @@ def visualize_operable( else: ret = [self.apply_mapping_jet(operable, base_image)] - processed_image = ret[0].nparray - - self.figure.clear() - self.figure.view.imshow(processed_image) - self.figure.view.show(block=False) + # draw the image + self.prepare_figure_records([self.figure], [ret[0].nparray.shape]) + self.figure.view.imshow(ret[0]) return ret diff --git a/opencsp/common/lib/render/View3d.py b/opencsp/common/lib/render/View3d.py index db7595f8..ec83cc34 100644 --- a/opencsp/common/lib/render/View3d.py +++ b/opencsp/common/lib/render/View3d.py @@ -16,6 +16,7 @@ import scipy.ndimage from typing import TYPE_CHECKING +from opencsp.common.lib.cv.CacheableImage import CacheableImage import opencsp.common.lib.render.axis_3d as ax3d import opencsp.common.lib.render.view_spec as vs import opencsp.common.lib.render_control.RenderControlHeatmap as rcheat @@ -486,6 +487,8 @@ def imshow(self, *args, colorbar=False, **kwargs) -> None: img = args[0] args = list(args) args[0] = load_as_necessary(img) + if isinstance(args[0], CacheableImage): + args[0] = args[0].nparray im = self.axis.imshow(*args, interpolation="none", **kwargs) if self.equal: @@ -497,7 +500,7 @@ def imshow(self, *args, colorbar=False, **kwargs) -> None: def draw_image( self, - path_or_array: str | np.ndarray, + path_or_array_or_cacheable: str | np.ndarray | CacheableImage, xy_location: tuple[float, float] = None, width_height: tuple[float, float] = None, cmap: str | matplotlib.colors.Colormap = None, @@ -513,7 +516,7 @@ def draw_image( Parameters ---------- - path_or_array : str | np.ndarray + path_or_array_or_cacheable : str | np.ndarray | CacheableImage The image to be drawn. xy_location : tuple[float, float], optional The location at which to draw the image, in graph coordinate units. @@ -533,10 +536,12 @@ def draw_image( large to small order instead of small to large order. This effectively puts the origin for the graph at the top. """ - if isinstance(path_or_array, str): - img = mpimg.imread(path_or_array) + if isinstance(path_or_array_or_cacheable, str): + img = mpimg.imread(path_or_array_or_cacheable) + elif isinstance(path_or_array_or_cacheable, CacheableImage): + img = path_or_array_or_cacheable.nparray else: - img: np.ndarray = path_or_array + img: np.ndarray = path_or_array_or_cacheable imgw, imgh = img.shape[1], img.shape[0] xbnd, ybnd = self.axis.get_xbound(), self.axis.get_ybound() xdraw, ydraw = xbnd, ybnd diff --git a/opencsp/common/lib/tool/system_tools.py b/opencsp/common/lib/tool/system_tools.py index 6ec34801..4330484a 100644 --- a/opencsp/common/lib/tool/system_tools.py +++ b/opencsp/common/lib/tool/system_tools.py @@ -5,6 +5,28 @@ from opencsp import opencsp_settings +def is_notebook() -> bool: + """ + Is this code running in a notebook. From + https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook + + Returns + ------- + bool + True if this code is executing in a Jypter notebook, or False otherwise. + """ + try: + shell = get_ipython().__class__.__name__ # type: ignore + if shell == 'ZMQInteractiveShell': + return True # Jupyter notebook or qtconsole + elif shell == 'TerminalInteractiveShell': + return False # Terminal running IPython + else: + return False # Other type (?) + except NameError: + return False # Probably standard Python interpreter + + def is_solo(): """Determines if this computer is one of the Solo HPC nodes. diff --git a/opencsp/default_settings.ini b/opencsp/default_settings.ini index 4f1d9d69..909cb4ed 100644 --- a/opencsp/default_settings.ini +++ b/opencsp/default_settings.ini @@ -1,8 +1,8 @@ # These are the default settings for the OpenCSP code base. # # Settings values can be overridden by creating a file in one of the following locations: -# Windows: %USERPROFILE%/.opencsp/opencsp_settings.ini -# Other: ~/.config/opencsp/opencsp_settings.ini +# Windows: %USERPROFILE%/.opencsp/settings/opencsp_settings.ini +# Other: ~/.config/opencsp/settings/opencsp_settings.ini # In addition, the environmental variable OPENCSP_SETTINGS_DIRS, if set, will be # split on semicolon ';' characters and each directory searched for a # 'opencsp_settings.ini' file.