Skip to content

Commit dd0d98a

Browse files
authored
Merge pull request #286 from sandialabs/revert-231-230-code-feature-update-most-of-the-image-processors-for-spotanalysis
Revert "230 code feature update most of the image processors for spotanalysis"
2 parents 0d810a1 + 84eeb9f commit dd0d98a

File tree

57 files changed

+1614
-2452
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1614
-2452
lines changed

contrib/common/lib/cv/spot_analysis/image_processor/EnclosedEnergyImageProcessor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import cv2 as cv
66
import numpy as np
77

8-
from contrib.common.lib.cv.spot_analysis.PixelOfInterest import PixelOfInterest
8+
from contrib.common.lib.cv.spot_analysis.PixelLocation import PixelOfInterest
99
from opencsp.common.lib.cv.CacheableImage import CacheableImage
1010
import opencsp.common.lib.geometry.Pxy as p2
1111
import opencsp.common.lib.render.Color as color
@@ -45,9 +45,9 @@ def __init__(
4545
"""
4646
Parameters
4747
----------
48-
center_locator: Callable[[SpotAnalysisOperable], tuple[int, int]] | tuple[int, int] | str | PixelOfInterest, optional
48+
center_locator: Callable[[SpotAnalysisOperable], tuple[int, int]] | tuple[int, int] | str | PixelLocation, optional
4949
The pixel location in the image from which to start measureing the
50-
enclosed energy. See :py:class:`PixelOfInterest` for more details.
50+
enclosed energy. See :py:class:`PixelLocation` for more details.
5151
enclosed_shape: str, optional
5252
The shape to use for calculating enclosed energy. Options are
5353
"circle" or "square". Default is "cirle".

opencsp/common/lib/csp/StandardPlotOutput.py

Lines changed: 56 additions & 71 deletions
Large diffs are not rendered by default.

opencsp/common/lib/cv/CacheableImage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def source_path(self, new_val: str | None):
251251
ValueError,
252252
"Error in CacheableImage.source_path(): "
253253
+ f"the contents of self.nparray and {new_val} do not match!"
254-
+ f" ({self.cache_path=}, {self._source_path=})",
254+
+ f" ({self.cache_path=}, {self.source_path=})",
255255
)
256256

257257
self._source_path = new_val
@@ -355,7 +355,7 @@ def from_single_source(
355355
elif isinstance(array_or_path, str):
356356
path: str = array_or_path
357357
if path.lower().endswith(".npy"):
358-
return cls(cache_path_name_ext=path)
358+
return cls(cache_path=path)
359359
return cls(source_path=path)
360360
elif isinstance(array_or_path, np.ndarray):
361361
array: np.ndarray = array_or_path

opencsp/common/lib/cv/SpotAnalysis.py

Lines changed: 50 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,24 @@
1616
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisOperable import SpotAnalysisOperable
1717
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisOperablesStream import _SpotAnalysisOperablesStream
1818
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisOperableAttributeParser import SpotAnalysisOperableAttributeParser
19-
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisPipeline import SpotAnalysisPipeline
2019
from opencsp.common.lib.cv.spot_analysis.VisualizationCoordinator import VisualizationCoordinator
2120
import opencsp.common.lib.render.VideoHandler as vh
2221
import opencsp.common.lib.opencsp_path.opencsp_root_path as orp
2322
import opencsp.common.lib.tool.file_tools as ft
23+
import opencsp.common.lib.tool.image_tools as it
2424
import opencsp.common.lib.tool.log_tools as lt
2525

2626

27-
class SpotAnalysis(Iterator[SpotAnalysisOperable]):
27+
class SpotAnalysis(Iterator[tuple[SpotAnalysisOperable]]):
2828
"""Spot Analysis class for characterizing beams of light.
2929
3030
This is meant to be a general beam characterization tool that receives as
3131
input an image (or video) of a single beam, and generates annotations and
3232
numerical statistics as output. In general, there is an image processing
3333
step, an image analysis step, and a visualization step. Any number of
3434
processors and analyzers can be chained together to form a pipeline for
35-
evaluation.
36-
37-
Once defined, a SpotAnalysis instance can evaluate any number of input
38-
images with either the :py:meth:`set_primary_images` or
39-
:py:meth:`set_input_operables` methods.
35+
evaluation. Once defined, a SpotAnalysis instance can be used to evaluate
36+
one image/video per evaluation.
4037
4138
A list of possible use cases for this class include:
4239
a. BCS
@@ -179,12 +176,15 @@ def __init__(
179176
cases listed above. """
180177
self.image_processors: list[AbstractSpotAnalysisImageProcessor] = []
181178
""" List of processors, one per step of the analysis. The output from
182-
each processor can include one or more operables, and is made available
183-
to all subsequent processors. """
179+
each processor can include one or more images (or numeric values), and
180+
is made available to all subsequent processors. """
181+
self._results_iter: Iterator[SpotAnalysisOperable] = None
182+
""" The returned value from iter(self.image_processors[-1]). Initialized
183+
on the first call to process_next(). """
184+
self._prev_result: SpotAnalysisOperable = None
185+
""" The previously returned result. """
184186
self.input_stream: _SpotAnalysisOperablesStream = None
185187
""" The images to be processed. """
186-
self._processing_pipeline: SpotAnalysisPipeline = None
187-
""" The current iterator and evaluation pipeline. """
188188
self.save_dir: str = save_dir
189189
""" If not None, then primary images will be saved to the given
190190
directory as a PNG after having been fully processed. """
@@ -206,19 +206,26 @@ def __init__(
206206
all values here will be made available for processing as the default
207207
values. """
208208
self.visualization_coordinator = VisualizationCoordinator()
209-
""" Manages interactive viewing of all visualization image processors. """
209+
""" Shows the same image from all visualization processors at the same time. """
210210

211211
self.set_image_processors(image_processors)
212212

213213
def set_image_processors(self, image_processors: list[AbstractSpotAnalysisImageProcessor]):
214214
self.image_processors = image_processors
215215

216+
# chain the image processors together
217+
for i, image_processor in enumerate(self.image_processors):
218+
if i == 0:
219+
continue
220+
image_processor.assign_inputs(self.image_processors[i - 1])
221+
216222
# register the visualization processors
217223
self.visualization_coordinator.clear()
218224
self.visualization_coordinator.register_visualization_processors(image_processors)
219225

220-
# register the image processors for in-flight results
221-
self._operables_in_flight = {processor: [] for processor in image_processors}
226+
# assign the input stream to the first image processor
227+
if self.input_stream != None:
228+
self._assign_inputs(self.input_stream)
222229

223230
@staticmethod
224231
def _images2stream(
@@ -230,16 +237,11 @@ def _images2stream(
230237
return ImagesIterable(images)
231238

232239
def _assign_inputs(self, input_operables: Iterator[SpotAnalysisOperable]):
233-
# sanitize arguments
234240
if not isinstance(input_operables, _SpotAnalysisOperablesStream):
235241
input_operables = _SpotAnalysisOperablesStream(input_operables)
236-
237-
# assign inputs
238242
self.input_stream = input_operables
239-
240-
# reset temporary values
241243
self._prev_result = None
242-
self.set_image_processors(self.image_processors)
244+
self.image_processors[0].assign_inputs(self.input_stream)
243245

244246
def set_primary_images(self, images: list[str] | list[np.ndarray] | vh.VideoHandler | ImagesStream):
245247
"""Assigns the images of the spot to be analyzed, in preparation for process_next().
@@ -252,55 +254,31 @@ def set_primary_images(self, images: list[str] | list[np.ndarray] | vh.VideoHand
252254
handler instance set with a video to be broken into frames, or an
253255
images stream.
254256
255-
Notes
256-
-----
257-
Only one of set_primary_images or :py:meth:`set_input_operables` should
258-
be used to assign input images.
259-
"""
257+
See also: set_input_operables()"""
260258
primary_images = self._images2stream(images)
261259
images_stream = SpotAnalysisImagesStream(primary_images, {})
262-
self._assign_inputs(images_stream)
260+
self._assign_inputs(_SpotAnalysisOperablesStream(images_stream))
263261

264262
def set_input_operables(
265263
self,
266264
input_operables: _SpotAnalysisOperablesStream | list[SpotAnalysisOperable] | Iterator[SpotAnalysisOperable],
267265
):
268266
"""Assigns primary and supporting images, and other necessary data, in preparation for process_next().
269267
270-
Notes
271-
-----
272-
Only one of set_primary_images or :py:meth:`set_input_operables` should
273-
be used to assign input images.
274-
"""
268+
See also: set_primary_images()"""
275269
self._assign_inputs(input_operables)
276270

277271
def set_default_support_images(self, support_images: dict[ImageType, CacheableImage]):
278272
"""Provides extra support images for use during image processing, as a
279273
default for when the support images are not otherwise from the input
280274
operables. Note that this does not include the primary images or other
281275
data."""
282-
# check for state consistency
283-
if self.input_stream is None:
284-
lt.error_and_raise(
285-
RuntimeError,
286-
"Error in SpotAnalysis.set_default_support_images(): "
287-
+ "must call set_input_operables() or set_primary_images() first.",
288-
)
289-
290276
self.input_stream.set_defaults(support_images, self.input_stream.default_data)
291277

292278
def set_default_data(self, operable: SpotAnalysisOperable):
293279
"""Provides extra data for use during image processing, as a default
294280
for when the data is not otherwise from the input operables. Note that
295281
this does not include the primary or supporting images."""
296-
# check for state consistency
297-
if self.input_stream is None:
298-
lt.error_and_raise(
299-
RuntimeError,
300-
"Error in SpotAnalysis.set_default_data(): "
301-
+ "must call set_input_operables() or set_primary_images() first.",
302-
)
303-
304282
self.input_stream.set_defaults(self.input_stream.default_support_images, operable)
305283

306284
def process_next(self):
@@ -313,22 +291,23 @@ def process_next(self):
313291
The processed primary image and other associated data. None if done
314292
processing.
315293
"""
316-
last_processor = self.image_processors[-1]
294+
if self._results_iter is None:
295+
self._results_iter = iter(self.image_processors[-1])
317296

318-
# sanity check
319-
if self.input_stream is None:
320-
lt.error_and_raise(
321-
RuntimeError,
322-
"Error in SpotAnalysis.process_next(): "
323-
+ "must assign inputs via set_primary_images() or set_input_operables() first.",
324-
)
297+
# Release memory from the previous result
298+
if self._prev_result is not None:
299+
self.image_processors[-1].cache_images_to_disk_as_necessary()
300+
self._prev_result = None
325301

326-
# Create the processing pipeline, as necessary
327-
if self._processing_pipeline is None:
328-
self._initialize_processing_pipeline()
302+
# Attempt to get the next image. Raises StopIteration if there are no
303+
# more results available.
304+
try:
305+
result = next(self._results_iter)
306+
except StopIteration:
307+
return None
329308

330-
# Get the next result
331-
return self._processing_pipeline.process_next()
309+
self._prev_result = result
310+
return result
332311

333312
def _save_image(self, save_path_name_ext: str, image: CacheableImage, description: str):
334313
# check for overwrite
@@ -348,7 +327,7 @@ def _save_image(self, save_path_name_ext: str, image: CacheableImage, descriptio
348327
if save_ext in ["np", "npy"]:
349328
np.save(save_path_name_ext, image.nparray, allow_pickle=False)
350329
else:
351-
image.to_image().save(save_path_name_ext)
330+
it.numpy_to_image(image.nparray).save(save_path_name_ext)
352331

353332
return True
354333

@@ -392,17 +371,16 @@ def save_image(
392371
# Save the resulting processed image
393372
if save_dir != None:
394373
# Get the original file name
395-
orig_image_name = ""
396-
orig_image_path, orig_image_name_ext = operable.get_primary_path_nameext()
397-
if orig_image_name_ext != None:
398-
_, orig_image_name, orig_image_ext = ft.path_components(orig_image_name_ext)
399-
orig_image_name += "_"
374+
orig_image_path_name = ""
375+
if operable.primary_image.source_path != None:
376+
_, orig_image_path_name, _ = ft.path_components(operable.primary_image.source_path)
377+
orig_image_path_name += "_"
400378

401379
# Get the output name of the file to save to
402380
sa_name_appendix = ft.convert_string_to_file_body(self.name)
403-
image_name = f"{orig_image_name}{sa_name_appendix}"
381+
image_name = f"{orig_image_path_name}{sa_name_appendix}"
404382
if image_name in self.saved_names:
405-
image_name = f"{orig_image_name}{sa_name_appendix}_{self.save_idx}"
383+
image_name = f"{orig_image_path_name}{sa_name_appendix}_{self.save_idx}"
406384
self.save_idx += 1
407385
self.saved_names.add(image_name)
408386
image_path_name_ext = os.path.join(save_dir, f"{image_name}.{save_ext}")
@@ -437,14 +415,12 @@ def save_image(
437415

438416
return image_path_name_ext
439417

440-
def _initialize_processing_pipeline(self):
441-
self._processing_pipeline = SpotAnalysisPipeline(self.image_processors, iter(self.input_stream))
442-
443418
def __iter__(self):
444-
self._initialize_processing_pipeline()
419+
self.save_idx = 0
420+
self._prev_result = None
445421
return self
446422

447-
def __next__(self) -> SpotAnalysisOperable:
423+
def __next__(self):
448424
ret = self.process_next()
449425
if ret == None:
450426
raise StopIteration
@@ -469,7 +445,7 @@ def __next__(self) -> SpotAnalysisOperable:
469445
PopulationStatisticsImageProcessor(min_pop_size=-1),
470446
EchoImageProcessor(),
471447
LogScaleImageProcessor(),
472-
ViewFalseColorImageProcessor(),
448+
FalseColorImageProcessor(),
473449
]
474450
sa = SpotAnalysis("BCS Test", image_processors, save_dir=outdir, save_overwrite=True)
475451
image_name_exts = ft.files_in_directory(indir)

opencsp/common/lib/cv/annotations/HotspotAnnotation.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from opencsp.common.lib.cv.annotations.PointAnnotations import PointAnnotations
22
import opencsp.common.lib.geometry.Pxy as p2
3-
import opencsp.common.lib.render.Color as color
43
import opencsp.common.lib.render_control.RenderControlPointSeq as rcps
54

65

@@ -50,8 +49,5 @@ def __init__(self, style: rcps.RenderControlPointSeq = None, point: p2.Pxy = Non
5049
"""
5150
# "ChatGPT 4o" assisted with generating this docstring.
5251
if style is None:
53-
style = rcps.default(color=color.magenta(), marker='x')
52+
style = rcps.RenderControlPointSeq(color="blue", marker="x", markersize=1)
5453
super().__init__(style, point)
55-
56-
def __str__(self):
57-
return f"HotspotAnnotation<{self.points.x[0]},{self.points.y[0]}>"

0 commit comments

Comments
 (0)