1616from opencsp .common .lib .cv .spot_analysis .SpotAnalysisOperable import SpotAnalysisOperable
1717from opencsp .common .lib .cv .spot_analysis .SpotAnalysisOperablesStream import _SpotAnalysisOperablesStream
1818from opencsp .common .lib .cv .spot_analysis .SpotAnalysisOperableAttributeParser import SpotAnalysisOperableAttributeParser
19- from opencsp .common .lib .cv .spot_analysis .SpotAnalysisPipeline import SpotAnalysisPipeline
2019from opencsp .common .lib .cv .spot_analysis .VisualizationCoordinator import VisualizationCoordinator
2120import opencsp .common .lib .render .VideoHandler as vh
2221import opencsp .common .lib .opencsp_path .opencsp_root_path as orp
2322import opencsp .common .lib .tool .file_tools as ft
23+ import opencsp .common .lib .tool .image_tools as it
2424import 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 )
0 commit comments