diff --git a/contrib/app/sofast/run_and_characterize_sofast_1_cam.py b/contrib/app/sofast/run_and_characterize_sofast_1_cam.py index 629e3c8d8..f3ff20b89 100644 --- a/contrib/app/sofast/run_and_characterize_sofast_1_cam.py +++ b/contrib/app/sofast/run_and_characterize_sofast_1_cam.py @@ -35,7 +35,7 @@ def main(): output_file = None # Define measurement parameters - optic_screen_dist = 10.516 # meter + dist_optic_screen = 10.516 # meter optic_name = 'NSTTF Facet' optic_measure_point = Pxyz((0, 0, 0)) # meter @@ -126,7 +126,7 @@ def func_close_all(): print('Saving Data') # Get Measurement object - measurement = system.get_measurements(optic_measure_point, optic_screen_dist, optic_name)[0] + measurement = system.get_measurements(optic_measure_point, dist_optic_screen, optic_name)[0] calibration = calibrations[0] # Process data diff --git a/contrib/app/sofast/run_and_characterize_sofast_2_cam.py b/contrib/app/sofast/run_and_characterize_sofast_2_cam.py index c40a98e49..d4a9197b5 100644 --- a/contrib/app/sofast/run_and_characterize_sofast_2_cam.py +++ b/contrib/app/sofast/run_and_characterize_sofast_2_cam.py @@ -41,7 +41,7 @@ def main(): output_file_base = None # Define measurement parameters - optic_screen_dist = 10.516 # meter + dist_optic_screen = 10.516 # meter optic_name = 'NSTTF Facet' optic_measure_point = Pxyz((0, 0, 0)) # meter @@ -152,7 +152,7 @@ def func_close_all(): print('Saving Data') # Get Measurement object - measurements = system.get_measurements(optic_measure_point, optic_screen_dist, optic_name) + measurements = system.get_measurements(optic_measure_point, dist_optic_screen, optic_name) # Process data print('Processing data') diff --git a/contrib/scripts/sensitive_strings.py b/contrib/scripts/sensitive_strings.py index f49141b0e..9e98dbc21 100644 --- a/contrib/scripts/sensitive_strings.py +++ b/contrib/scripts/sensitive_strings.py @@ -12,6 +12,7 @@ from opencsp.common.lib.opencsp_path import opencsp_settings import opencsp.common.lib.opencsp_path.opencsp_root_path as orp import opencsp.common.lib.process.subprocess_tools as st +import opencsp.common.lib.tool.exception_tools as et import opencsp.common.lib.tool.file_tools as ft import opencsp.common.lib.tool.hdf5_tools as h5 import opencsp.common.lib.tool.image_tools as it @@ -124,15 +125,21 @@ def _is_binary_file(self, file_path: str, file_name_ext: str): return is_binary_file - def _enqueue_binary_file_for_later_processing(self, file_path: str, file_name_ext: str): + def _enqueue_unknown_binary_files_for_later_processing(self, file_path: str, file_name_ext: str): + """If the given file is recognized as an allowed file, and it's fingerprint matches the allowed file, then we + can dismiss it from the list of unfound files and add it to the list of the accepted files. + + However, if the given file isn't recognized or it's fingerprint is different, then add it to the unknown list, + to be dealt with later.""" file_ff = ff.FileFingerprint.for_file(self.root_search_dir, file_path, file_name_ext) if file_ff in self.allowed_binary_files: # we already know and trust this binary file - self.unfound_allowed_binary_files.remove(file_ff) + with et.ignored(ValueError): + self.unfound_allowed_binary_files.remove(file_ff) self.accepted_binary_files.append(file_ff) else: - # we'll deal with unknown files as a group + # we'll deal with unknown files as a group later self.unknown_binary_files.append(file_ff) def parse_file(self, file_path: str, file_name_ext: str) -> list[str]: @@ -437,7 +444,7 @@ def search_files(self): # need to check this file if self._is_binary_file(file_path, file_name_ext): # deal with non-parseable binary files as a group, below - self._enqueue_binary_file_for_later_processing(file_path, file_name_ext) + self._enqueue_unknown_binary_files_for_later_processing(file_path, file_name_ext) else: # check text files for sensitive strings file_matches = self.search_file(file_path, file_name_ext) diff --git a/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/all_binaries.csv b/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/all_binaries.csv index 61860ac0f..80d4ef7a1 100644 --- a/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/all_binaries.csv +++ b/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/all_binaries.csv @@ -1,3 +1,3 @@ relative path,name_ext,size,hash -c,img1.png,1,01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b -c,img2.jpg,1,01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b \ No newline at end of file +c,img1.png,19,37b29d95963d70c6c969272b7b03b9ca4e4a832c86ef059768b15293be74582c +c,img2.jpg,4,6987740fb624e3e9943ec5d9ac5519b72cea1b35fb4bde5719df3923a36c08f7 \ No newline at end of file diff --git a/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/single_binary.csv b/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/single_binary.csv index f874c4728..8e3ac2808 100644 --- a/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/single_binary.csv +++ b/contrib/scripts/test/data/input/sensitive_strings/per_test_allowed_binaries/single_binary.csv @@ -1,2 +1,2 @@ relative path,name_ext,size,hash -c,img1.png,1,01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b +c,img1.png,19,37b29d95963d70c6c969272b7b03b9ca4e4a832c86ef059768b15293be74582c diff --git a/contrib/scripts/test/data/input/sensitive_strings/root_search_dir/c/img1.png b/contrib/scripts/test/data/input/sensitive_strings/root_search_dir/c/img1.png index 2b38c4775..5256b9059 100644 --- a/contrib/scripts/test/data/input/sensitive_strings/root_search_dir/c/img1.png +++ b/contrib/scripts/test/data/input/sensitive_strings/root_search_dir/c/img1.png @@ -1 +1 @@ -img1 \ No newline at end of file +totally a png image \ No newline at end of file diff --git a/contrib/test_data_generation/sofast_fringe/downsample_data.py b/contrib/test_data_generation/sofast_fringe/downsample_data.py index 1e9ef5c36..aa088e36d 100644 --- a/contrib/test_data_generation/sofast_fringe/downsample_data.py +++ b/contrib/test_data_generation/sofast_fringe/downsample_data.py @@ -5,6 +5,7 @@ import sys from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe as Measurement +import opencsp.app.sofast.lib.DistanceOpticScreen as osd from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir sys.path.append(os.path.join(opencsp_code_dir(), '..')) @@ -31,13 +32,15 @@ def downsample_measurement(file: str, n: int) -> Measurement: # Downsample measurement mask_images = ddg.downsample_images(measurement_orig.mask_images, n) fringe_images = ddg.downsample_images(measurement_orig.fringe_images, n) + dist_optic_screen_measure = osd.DistanceOpticScreen( + measurement_orig.v_measure_point_facet, measurement_orig.dist_optic_screen + ) return Measurement( mask_images=mask_images, fringe_images=fringe_images, fringe_periods_x=measurement_orig.fringe_periods_x, fringe_periods_y=measurement_orig.fringe_periods_y, - measure_point=measurement_orig.measure_point, - optic_screen_dist=measurement_orig.optic_screen_dist, + dist_optic_screen_measure=dist_optic_screen_measure, date=measurement_orig.date, name=measurement_orig.name, ) diff --git a/example/sofast_fixed/run_and_characterize_fixed_pattern.py b/example/sofast_fixed/run_and_characterize_fixed_pattern.py index a836db3bd..e511d966e 100644 --- a/example/sofast_fixed/run_and_characterize_fixed_pattern.py +++ b/example/sofast_fixed/run_and_characterize_fixed_pattern.py @@ -13,6 +13,7 @@ from opencsp.app.sofast.lib.DotLocationsFixedPattern import DotLocationsFixedPattern from opencsp.app.sofast.lib.MeasurementSofastFixed import MeasurementSofastFixed +import opencsp.app.sofast.lib.DistanceOpticScreen as osd from opencsp.app.sofast.lib.ProcessSofastFixed import ProcessSofastFixed from opencsp.app.sofast.lib.SystemSofastFixed import SystemSofastFixed from opencsp.common.lib.camera.Camera import Camera @@ -102,9 +103,8 @@ def run_next(): # Capture image frame = image_acquisition.get_frame() # Process - measurement = MeasurementSofastFixed( - frame, v_measure_point_facet, dist_optic_screen, pt_origin, name='NSTTF Facet' - ) + dist_optic_screen_measure = osd.DistanceOpticScreen(v_measure_point_facet, dist_optic_screen) + measurement = MeasurementSofastFixed(frame, dist_optic_screen_measure, pt_origin, name='NSTTF Facet') process(fixed_pattern_dot_locs, spatial_orientation, camera, facet_data, measurement, surface_data) # Continue or exit image_projection.root.after(200, run_next) diff --git a/opencsp/app/camera_calibration/CameraCalibration.py b/opencsp/app/camera_calibration/CameraCalibration.py index 27cfd0e64..7ccef597e 100644 --- a/opencsp/app/camera_calibration/CameraCalibration.py +++ b/opencsp/app/camera_calibration/CameraCalibration.py @@ -20,6 +20,7 @@ from opencsp.common.lib.camera.Camera import Camera from opencsp.common.lib.geometry.Vxy import Vxy from opencsp.common.lib.geometry.Vxyz import Vxyz +import opencsp.common.lib.tool.tk_tools as tkt class CalibrationGUI: @@ -29,7 +30,7 @@ def __init__(self): """ # Create tkinter object - self.root = tkinter.Tk() + self.root = tkt.window() # Set title self.root.title('Camera Calibration') @@ -275,7 +276,7 @@ def find_corners(self): def view_found_corners(self): # Create new window - root_corns = tkinter.Toplevel(self.root) + root_corns = tkt.window(self.root, TopLevel=True) # Get number checkerboard points npts = self.get_npts() diff --git a/opencsp/app/select_image_points/SelectImagePoints.py b/opencsp/app/select_image_points/SelectImagePoints.py index 07b01cb3f..f360a9385 100644 --- a/opencsp/app/select_image_points/SelectImagePoints.py +++ b/opencsp/app/select_image_points/SelectImagePoints.py @@ -17,6 +17,8 @@ import numpy as np import rawpy +import opencsp.common.lib.tool.tk_tools as tkt + class SelectImagePoints: """ @@ -200,5 +202,5 @@ def _load_raw_image(file) -> np.ndarray: ) if file_selected != '': # Create window - win = SelectImagePoints(tk.Tk(), file_selected) + win = SelectImagePoints(tkt.window(), file_selected) win.run() diff --git a/opencsp/app/sofast/SofastGUI.py b/opencsp/app/sofast/SofastGUI.py index c181629b6..41f1cdd53 100644 --- a/opencsp/app/sofast/SofastGUI.py +++ b/opencsp/app/sofast/SofastGUI.py @@ -22,7 +22,7 @@ from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection from opencsp.common.lib.geometry.Vxyz import Vxyz import opencsp.common.lib.tool.exception_tools as et -from opencsp.common.lib.tool.TkToolTip import TkToolTip +import opencsp.common.lib.tool.tk_tools as tkt class SofastGUI(ssc.SofastServiceCallback): @@ -36,7 +36,7 @@ def __init__(self) -> 'SofastGUI': self.service = SofastService(self) # Create tkinter object - self.root = tkinter.Tk() + self.root = tkt.window() # Set title self.root.title('SOFAST') @@ -148,7 +148,7 @@ def _create_layout(self) -> None: label_frame_load_system, text='Connect Camera', command=self.load_image_acquisition ) self.btn_load_image_acquisition.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip( + tkt.TkToolTip( self.btn_load_image_acquisition, 'Select the type of camera in the dropdown menu to the right. Click to connect to the camera.', ) @@ -159,7 +159,7 @@ def _create_layout(self) -> None: label_frame_load_system, text='Load ImageProjection', command=self.load_image_projection ) self.btn_load_image_projection.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_load_image_projection, 'Select an ImageProjection HDF file to load and display.') + tkt.TkToolTip(self.btn_load_image_projection, 'Select an ImageProjection HDF file to load and display.') r += 1 # =============== First Column - Projection controls =============== @@ -168,26 +168,26 @@ def _create_layout(self) -> None: label_frame_projector, text='Show Calibration Image', command=self.show_calibration_image ) self.btn_show_cal_image.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_show_cal_image, 'Shows calibration image on projection window.') + tkt.TkToolTip(self.btn_show_cal_image, 'Shows calibration image on projection window.') r += 1 self.btn_show_axes = tkinter.Button(label_frame_projector, text='Show Screen Axes', command=self.show_axes) self.btn_show_axes.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_show_axes, 'Shows screen axes on projection window.') + tkt.TkToolTip(self.btn_show_axes, 'Shows screen axes on projection window.') r += 1 self.btn_show_crosshairs = tkinter.Button( label_frame_projector, text='Show Crosshairs', command=self.show_crosshairs ) self.btn_show_crosshairs.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_show_crosshairs, 'Shows crosshairs on projection window.') + tkt.TkToolTip(self.btn_show_crosshairs, 'Shows crosshairs on projection window.') r += 1 self.btn_close_projection = tkinter.Button( label_frame_projector, text='Close Display Window', command=self.close_projection_window ) self.btn_close_projection.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_close_projection, 'Close only projection window.') + tkt.TkToolTip(self.btn_close_projection, 'Close only projection window.') r += 1 # =============== First Column - Camera controls =============== @@ -197,13 +197,13 @@ def _create_layout(self) -> None: label_frame_camera, text='Calibrate Exposure Time', command=self.run_exposure_cal ) self.btn_exposure_cal.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_exposure_cal, 'Automatically performs camera exposure calibration.') + tkt.TkToolTip(self.btn_exposure_cal, 'Automatically performs camera exposure calibration.') r += 1 # Set camera exposure self.btn_set_exposure = tkinter.Button(label_frame_camera, text='Set Exposure Time', command=self.set_exposure) self.btn_set_exposure.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip( + tkt.TkToolTip( self.btn_set_exposure, 'Set the camera exposure time value. The current value is displayed when clicked.' ) r += 1 @@ -211,19 +211,19 @@ def _create_layout(self) -> None: # Show snapshot button self.btn_show_snapshot = tkinter.Button(label_frame_camera, text='Show Snapshot', command=self.show_snapshot) self.btn_show_snapshot.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_show_snapshot, 'Shows a camera image and pixel brightness histogram.') + tkt.TkToolTip(self.btn_show_snapshot, 'Shows a camera image and pixel brightness histogram.') r += 1 # Save snapshot button self.btn_save_snapshot = tkinter.Button(label_frame_camera, text='Save Snapshot', command=self.save_snapshot) self.btn_save_snapshot.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_save_snapshot, 'Saves image from camera as a PNG.') + tkt.TkToolTip(self.btn_save_snapshot, 'Saves image from camera as a PNG.') r += 1 # Live view button self.btn_live_view = tkinter.Button(label_frame_camera, text='Live View', command=self.live_view) self.btn_live_view.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_live_view, 'Shows live view from connected camera.') + tkt.TkToolTip(self.btn_live_view, 'Shows live view from connected camera.') r += 1 # Perform exposure calibration @@ -231,7 +231,7 @@ def _create_layout(self) -> None: label_frame_camera, text='Close Camera', command=self.close_image_acquisition ) self.btn_close_camera.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_close_camera, 'Closes connection to camera.') + tkt.TkToolTip(self.btn_close_camera, 'Closes connection to camera.') r += 1 # =============== First Column - System run controls =============== @@ -241,7 +241,7 @@ def _create_layout(self) -> None: label_frame_run, text='Run Data Capture', command=self.run_measurement ) self.btn_run_measurement.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_run_measurement, 'Runs SOFAST data capture. Mask then fringes are captured.') + tkt.TkToolTip(self.btn_run_measurement, 'Runs SOFAST data capture. Mask then fringes are captured.') r += 1 # Perform projector-camera brightness calibration @@ -249,7 +249,7 @@ def _create_layout(self) -> None: label_frame_run, text='Run Response Calibration', command=self.run_gray_levels_cal ) self.btn_gray_levels_cal.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip( + tkt.TkToolTip( self.btn_gray_levels_cal, 'Performs Projector-Camera Brightness Calibration sequence. Must select a destination HDF file first.', ) @@ -260,7 +260,7 @@ def _create_layout(self) -> None: label_frame_run, text='Load Response Calibration', command=self.load_gray_levels_cal ) self.btn_load_gray_levels_cal.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip( + tkt.TkToolTip( self.btn_load_gray_levels_cal, 'Loads a previously saved Projector-Camera Brightness Calibration sequence data file.', ) @@ -271,7 +271,7 @@ def _create_layout(self) -> None: label_frame_run, text='View Response Calibration', command=self.view_gray_levels_cal ) self.btn_view_gray_levels_cal.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip( + tkt.TkToolTip( self.btn_view_gray_levels_cal, 'Views current Projector-Camera Brightness Calibration sequence data file.' ) r += 1 @@ -280,7 +280,7 @@ def _create_layout(self) -> None: # Close window button self.btn_close = tkinter.Button(self.root, text='Close All', command=self.close) self.btn_close.grid(row=4, column=0, pady=2, padx=2, sticky='nesw') - TkToolTip(self.btn_close, 'Closes all windows.') + tkt.TkToolTip(self.btn_close, 'Closes all windows.') # =============== Second Column - Settings =============== r = 0 @@ -288,7 +288,7 @@ def _create_layout(self) -> None: self.var_cam_select = tkinter.StringVar(value=list(self.cam_options.keys())[0]) lbl_camera_type = tkinter.Label(label_frame_settings, text='Select camera:', font=('calibre', 10, 'bold')) drop_camera_type = tkinter.OptionMenu(label_frame_settings, self.var_cam_select, *list(self.cam_options.keys())) - TkToolTip(drop_camera_type, 'Select type of camera object to load.') + tkt.TkToolTip(drop_camera_type, 'Select type of camera object to load.') lbl_camera_type.grid(row=r, column=1, pady=2, padx=2, sticky='nse') drop_camera_type.grid(row=r, column=2, pady=2, padx=2, sticky='nsw') @@ -302,7 +302,7 @@ def _create_layout(self) -> None: drop_cal_type = tkinter.OptionMenu( label_frame_settings, self.var_cal_select, *list(SofastService.cal_options.keys()) ) - TkToolTip(drop_cal_type, 'Select type of Projector-Camera Brightness Calibration process to use.') + tkt.TkToolTip(drop_cal_type, 'Select type of Projector-Camera Brightness Calibration process to use.') lbl_cal_type.grid(row=r, column=1, pady=2, padx=2, sticky='nse') drop_cal_type.grid(row=r, column=2, pady=2, padx=2, sticky='nsw') @@ -329,11 +329,11 @@ def _create_layout(self) -> None: to=100, width=5, ) - TkToolTip( + tkt.TkToolTip( entry_fringe_x, 'Number of fringe X periods to show during a data capture. Each fringe period will always be phase shifted four times.', ) - TkToolTip( + tkt.TkToolTip( entry_fringe_y, 'Number of fringe Y periods to show during a data capture. Each fringe period will always be phase shifted four times.', ) @@ -360,7 +360,7 @@ def _create_layout(self) -> None: entry_meas_pt = tkinter.Entry( label_frame_settings, textvariable=self.var_meas_pt, font=('calibre', 10, 'normal'), width=20 ) - TkToolTip( + tkt.TkToolTip( entry_meas_pt, 'Location, in mirror coordinates (meters), of the point on the mirror where the "mirror-to-screen distance" is measured.', ) @@ -378,7 +378,7 @@ def _create_layout(self) -> None: entry_meas_dist = tkinter.Entry( label_frame_settings, textvariable=self.var_meas_dist, font=('calibre', 10, 'normal'), width=20 ) - TkToolTip( + tkt.TkToolTip( entry_meas_dist, 'The distance in meters from the display crosshairs to the "Measure Point XYZ" location on the mirror.', ) @@ -394,7 +394,7 @@ def _create_layout(self) -> None: entry_meas_name = tkinter.Entry( label_frame_settings, textvariable=self.var_meas_name, font=('calibre', 10, 'normal'), width=20 ) - TkToolTip(entry_meas_name, 'The name of the measurement to be saved in the measurement HDF file.') + tkt.TkToolTip(entry_meas_name, 'The name of the measurement to be saved in the measurement HDF file.') lbl_meas_name.grid(row=r, column=1, pady=2, padx=2, sticky='nsw', columnspan=2) r += 1 @@ -515,7 +515,7 @@ def load_image_projection(self) -> None: # Load data image_projection_data = ImageProjection.load_from_hdf(file) # Create new window - projector_root = tkinter.Toplevel(self.root) + projector_root = tkt.window(self.root, TopLevel=True) # Show window self.service.image_projection = ImageProjection(projector_root, image_projection_data) @@ -552,14 +552,10 @@ def run_measurement(self) -> None: if not self._check_calibration_loaded(): return - # Get fringe periods - periods_x = [4**idx for idx in range(self.var_fringe_periods_x.get())] - periods_y = [4**idx for idx in range(self.var_fringe_periods_y.get())] - periods_x[0] -= 0.1 - periods_y[0] -= 0.1 - - # Create fringe object - fringes = Fringes(periods_x, periods_y) + # Get fringe object + fringe_periods_x = self.var_fringe_periods_x.get() + fringe_periods_y = self.var_fringe_periods_y.get() + fringes = Fringes.from_num_periods(fringe_periods_x, fringe_periods_y) # Define what to run after measurement sequence def run_next(): diff --git a/opencsp/app/sofast/SofastService.py b/opencsp/app/sofast/SofastService.py index e7d971704..d326a5e95 100644 --- a/opencsp/app/sofast/SofastService.py +++ b/opencsp/app/sofast/SofastService.py @@ -2,11 +2,15 @@ and data capture. Can capture datasets and return HDF5 files. """ +import copy from typing import Callable +import matplotlib.backend_bases +import matplotlib.figure import matplotlib.pyplot as plt import numpy as np +import opencsp.app.sofast.lib.MeasurementSofastFringe as msf from opencsp.app.sofast.lib.Fringes import Fringes from opencsp.common.lib.camera.ImageAcquisitionAbstract import ImageAcquisitionAbstract from opencsp.common.lib.camera.ImageAcquisition_DCAM_mono import ImageAcquisition as ImageAcquisition_DCAM @@ -40,6 +44,24 @@ class SofastService: """ Available calibration objects to use """ def __init__(self, callback: ssc.SofastServiceCallback = None) -> 'SofastService': + """Service object for standard callibration, measurement, and analysis with the SOFAST tool. + + The goal of this class is to provide a standard interface for interactive use of SOFAST, such as from the SOFAST + GUI or from a RESTful interface. This class also tracks instances of other necessary classes, such as for a + camera or projector. When these instances are free'd they will have their own 'close()' methods evaluated as + well. The necessary classes are free'd in several situations, including at least: + - in SofastService.close() + - when the attribute is unset + - when SofastService is destructed + + There are currently two SOFAST systems 'fringe' and 'fixed'. So far this server interacts with the 'fringe' + system, with possible extension to use the 'fixed' system in the future. + + Params: + ------- + callback : SofastServiceCallback + Callbacks for this instance, for when attributes get set or unset. + """ self.callback = callback if callback else ssc.SofastServiceCallback() # Set defaults @@ -48,15 +70,12 @@ def __init__(self, callback: ssc.SofastServiceCallback = None) -> 'SofastService self._calibration: ImageCalibrationAbstract = None self._system: SystemSofastFringe = None + # Track plots that will need to be eventually closed + self._open_plots: list[matplotlib.figure.Figure] = [] + def __del__(self): with et.ignored(Exception): - self.image_acquisition = None - with et.ignored(Exception): - self.image_projection = None - with et.ignored(Exception): - self._system.close_all() - with et.ignored(Exception): - self._system = None + self.close() @property def system(self) -> SystemSofastFringe: @@ -77,26 +96,32 @@ def system(self) -> SystemSofastFringe: @system.setter def system(self, val: SystemSofastFringe): + """Set or unset the system instance. Does NOT call system.close() when being unset.""" if val is None: old = self._system self._system = None self.callback.on_system_unset(old) + # 'close_all()' also closes the acquisition and projection instances, which might not be desired + # old.close_all() else: self._system = val self.callback.on_system_set(val) @property - def image_projection(self) -> ImageProjection: + def image_projection(self) -> ImageProjection | None: + """The image projection (projector) instance. None if not yet set.""" return self._image_projection @image_projection.setter def image_projection(self, val: ImageProjection): + """Set or unset the image projection (projector) instance. If being unset, then the instance's 'close()' method + will be evaluated.""" if val is None: if self._image_projection is not None: old_ip = self._image_projection self._image_projection = None self.callback.on_image_projection_unset(old_ip) - old_ip.close() + old_ip.close() # release system resources self.system = None else: self._image_projection = val @@ -104,17 +129,20 @@ def image_projection(self, val: ImageProjection): self._load_system_elements() @property - def image_acquisition(self) -> ImageAcquisitionAbstract: + def image_acquisition(self) -> ImageAcquisitionAbstract | None: + """The image acquisition (camera) instance. None if not yet set.""" return self._image_acquisition @image_acquisition.setter def image_acquisition(self, val: ImageAcquisitionAbstract): + """Set or unset the image acquisition (camera) instance. If being unset, then the instance's 'close()' method + will be evaluated.""" if val is None: if self._image_acquisition is not None: old_ia = self._image_acquisition self._image_acquisition = None self.callback.on_image_acquisition_unset(old_ia) - old_ia.close() + old_ia.close() # release system resources self.system = None else: self._image_acquisition = val @@ -122,15 +150,18 @@ def image_acquisition(self, val: ImageAcquisitionAbstract): self._load_system_elements() @property - def calibration(self) -> ImageCalibrationAbstract: + def calibration(self) -> ImageCalibrationAbstract | None: + """The grayscale calibration instance. None if not yet set""" return self._calibration @calibration.setter def calibration(self, val: ImageCalibrationAbstract): + """Set or unset the system instance. Does NOT call calibration.close() when being unset.""" if val is None: old = self._calibration self._calibration = None self.callback.on_calibration_unset(old) + # old.close() # no such method else: self._calibration = val self.callback.on_calibration_set(val) @@ -167,8 +198,11 @@ def _load_system_elements(self) -> bool: return False def run_measurement(self, fringes: Fringes, on_done: Callable = None) -> None: - """Runs data collect and saved data.""" + """Runs data collection with the given fringes. + Once the data has been captured, the images will be available from the system instance. Similarly, the data can + then be processed with the system instance, such as with SystemSofastFringe.get_measurements(). + """ # Get minimum display value from calibration if self.calibration is None: lt.error_and_raise( @@ -185,7 +219,8 @@ def run_measurement(self, fringes: Fringes, on_done: Callable = None) -> None: self.system.capture_mask_and_fringe_images(on_done) def run_exposure_cal(self) -> None: - """Runs camera exposure calibration""" + """Runs camera exposure calibration. This adjusts the exposure time of the camera to keep the pixels from being + under or over saturated.""" if self.image_acquisition is None: lt.error_and_raise(RuntimeError, 'Camera must be connected.') @@ -199,7 +234,8 @@ def run_exposure_cal(self) -> None: self.system.run_camera_exposure_calibration(run_next) def load_gray_levels_cal(self, hdf5_file_path_name_ext: str) -> None: - """Loads saved results of a projector-camera intensity calibration""" + """Loads saved results of a projector-camera intensity calibration, which can then be accessed via the + 'calibration' instance.""" # Load file datasets = ['ImageCalibration/calibration_type'] cal_type = h5.load_hdf5_datasets(datasets, hdf5_file_path_name_ext)['calibration_type'] @@ -212,7 +248,8 @@ def load_gray_levels_cal(self, hdf5_file_path_name_ext: str) -> None: lt.error_and_raise(ValueError, f'Selected calibration type, {cal_type}, not supported.') def plot_gray_levels_cal(self) -> None: - """Shows plot of gray levels calibration data""" + """Shows plot of gray levels calibration data. When the close() method of this instance is called (or this + instance is destructed), the plot will be closed automatically.""" title = 'Projector-Camera Calibration Curve' # Check that we have a calibration @@ -232,6 +269,10 @@ def plot_gray_levels_cal(self) -> None: ax.grid(True) ax.set_title('Projector-Camera Calibration Curve') + # Track this figure, to be closed eventually + self._open_plots.append(fig) + fig.canvas.mpl_connect('close_event', self._on_plot_closed) + # TODO use RenderControlFigureRecord: # def plot_gray_levels_cal(self, fig_record: fm.RenderControlFigureRecord = None) -> fm.RenderControlFigureRecord: # # Create the plot @@ -253,7 +294,18 @@ def plot_gray_levels_cal(self) -> None: # return fig_record - def get_exposure(self) -> float | None: + def _on_plot_closed(self, event: matplotlib.backend_bases.CloseEvent): + """Stop tracking plots that are still open when the plots get closed.""" + to_remove = None + for fig in self._open_plots: + if fig.canvas == event.canvas: + to_remove = fig + break + if to_remove is not None: + self._open_plots.remove(to_remove) + + def get_exposure(self) -> int | None: + """Returns the exposure time of the camera (microseconds).""" if self.image_acquisition is None: lt.error_and_raise( RuntimeError, @@ -262,15 +314,22 @@ def get_exposure(self) -> float | None: ) return self.image_acquisition.exposure_time - def set_exposure(self, new_exp: float) -> None: - """Sets camera exposure time value to the given value""" - self.image_acquisition.exposure_time = new_exp + def set_exposure(self, new_exp: int) -> None: + """Sets camera exposure time value to the given value (microseconds)""" + self.image_acquisition.exposure_time = int(new_exp) def close(self) -> None: """ - Closes all windows - + Closes all windows and releases all contained objects. This includes: + - image_projection + - image_acquisition + - system + - plots """ + # Note that this method is called from the destructor, and so must be resilient to all the possible errors + # therein. More information can be found in the python documentation at + # https://docs.python.org/3/reference/datamodel.html#object.__del__ + # Close image projection with et.ignored(Exception): self.image_projection = None @@ -285,5 +344,6 @@ def close(self) -> None: self.system = None # Close plots - with et.ignored(Exception): - plt.close('all') + for fig in copy.copy(self._open_plots): + with et.ignored(Exception): + plt.close(fig) diff --git a/opencsp/app/sofast/lib/AbstractMeasurementSofast.py b/opencsp/app/sofast/lib/AbstractMeasurementSofast.py new file mode 100644 index 000000000..483c39083 --- /dev/null +++ b/opencsp/app/sofast/lib/AbstractMeasurementSofast.py @@ -0,0 +1,94 @@ +"""Measurement class for SofastFringe +""" + +from abc import ABC +import datetime as dt + +import opencsp.app.sofast.lib.DistanceOpticScreen as sod +import opencsp.common.lib.tool.hdf5_tools as h5 + + +class AbstractMeasurementSofast(h5.HDF5_IO_Abstract, ABC): + """Sofast measurement (fringe, fixed, etc) data class that contains captured images + and metadata about the measurement. + + By abstracting out the common parts of Fringe and Fixed measurements, we allow the OpticScreenDistance to be + determined once and used for many measurements. Having a common interface also reduces the burden on the programmer + to learn how to use each sofast mode. + """ + + def __init__( + self, dist_optic_screen_measure: sod.DistanceOpticScreen, date: dt.datetime, name: str + ) -> 'AbstractMeasurementSofast': + """ + Parameters + ---------- + dist_optic_screen_measure : MeasurementDistance + Optic-screen distance measurement. + date : datetime + Collection date/time. + name : str + Name or serial number of measurement. + + """ + # Get default values + if date is None: + date = dt.datetime.now() + + # Save input measurement data + self.dist_optic_screen_measure = dist_optic_screen_measure + self.date = date + self.name = name + + def __repr__(self) -> str: + # "AbstractMeasurementSofast: { name }" + cls_name = type(self).__name__ + return cls_name + ': { ' + self.name + ' }' + + @property + def v_measure_point_facet(self): + """Convenience method for accessing dist_optic_screen_measure.v_measure_point_facet""" + return self.dist_optic_screen_measure.v_measure_point_facet + + @property + def dist_optic_screen(self): + """Convenience method for accessing dist_optic_screen_measure.dist_optic_screen""" + return self.dist_optic_screen_measure.dist_optic_screen + + @classmethod + def _load_from_hdf(cls, file: str, prefix: str) -> dict[str, any]: + """ + Loads from HDF file + + Parameters + ---------- + file : string + HDF file to load + + """ + # Load grid data + datasets = [prefix + '/date', prefix + '/name'] + kwargs = h5.load_hdf5_datasets(datasets, file) + + kwargs['dist_optic_screen_measure'] = sod.DistanceOpticScreen.load_from_hdf(file, prefix) + kwargs['date'] = dt.datetime.fromisoformat(kwargs['date']) + + return kwargs + + def _save_to_hdf(self, file: str, prefix: str) -> None: + """ + Saves to HDF file + + Parameters + ---------- + file : string + HDF file to save + + NOTE: Collection date is saved as string in iso-format. + """ + datasets = [prefix + '/date', prefix + '/name'] + data = [self.date.isoformat(), self.name] + + # Save data + h5.save_hdf5_datasets(data, datasets, file) + self.dist_optic_screen_measure.save_to_hdf(file, prefix) diff --git a/opencsp/app/sofast/lib/DistanceOpticScreen.py b/opencsp/app/sofast/lib/DistanceOpticScreen.py new file mode 100644 index 000000000..f62148844 --- /dev/null +++ b/opencsp/app/sofast/lib/DistanceOpticScreen.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass, field + +from opencsp.common.lib.geometry.Vxyz import Vxyz +import opencsp.common.lib.tool.hdf5_tools as h5 + + +@dataclass +class DistanceOpticScreen(h5.HDF5_IO_Abstract): + """Represents a distance measurement between the optic and the origin of the screen coordinate system. + This measurement is typically achieved by displaying the crosshairs from the SofastGUI and measuring + from a point on the optic (measurement_point x,y,z) to the center of the crosshairs. + + The measure point is typically the center of the optic for on-axis optics, such as with spherical or flat + mirrors that are symmetric around their midpoint. + """ + + v_measure_point_facet: Vxyz = field(default_factory=lambda: Vxyz((0.0, 0.0, 0.0))) + """ Location of measure point, meters. """ + dist_optic_screen: float = 0 + """ Optic-screen distance, meters. """ + + def save_to_hdf(self, file: str, prefix: str) -> None: + """Saves data to given file. Data is stored as: PREFIX + Folder/Field_1 + + Parameters + ---------- + file : str + HDF file to save to + prefix : str, optional + Prefix to append to folder path within HDF file (folders must be separated by "/"). + Default is empty string ''. + """ + datasets = [prefix + '/v_measure_point_facet', prefix + '/dist_optic_screen'] + data = [self.v_measure_point_facet.data.squeeze(), self.dist_optic_screen] + + # Save data + h5.save_hdf5_datasets(data, datasets, file) + + @classmethod + def load_from_hdf(cls, file: str, prefix: str): + """Loads data from given file. Assumes data is stored as: PREFIX + Folder/Field_1 + + Parameters + ---------- + file : str + HDF file to load from + prefix : str, optional + Prefix to append to folder path within HDF file (folders must be separated by "/"). + Default is empty string ''. + """ + groups, file_names_and_shapes = h5.get_groups_and_datasets(file) + file_names = [name for name, shape in file_names_and_shapes] + datasets = [] + for s in ['/measure_point', '/v_measure_point_facet', '/dist_optic_screen', '/optic_screen_dist']: + dataset = prefix + s + if dataset in file_names: + datasets.append(dataset) + + # Load grid data + # datasets = [prefix + '/measure_point', prefix + '/dist_optic_screen'] + kwargs = h5.load_hdf5_datasets(datasets, file) + + # TODO update all existing HDF5 files to use consistent naming + if 'measure_point' in kwargs: + kwargs['v_measure_point_facet'] = kwargs['measure_point'] + del kwargs['measure_point'] + if 'optic_screen_dist' in kwargs: + kwargs['dist_optic_screen'] = kwargs['optic_screen_dist'] + del kwargs['optic_screen_dist'] + + kwargs['v_measure_point_facet'] = Vxyz(kwargs['v_measure_point_facet']) + + return cls(**kwargs) diff --git a/opencsp/app/sofast/lib/Fringes.py b/opencsp/app/sofast/lib/Fringes.py index 1d88afd01..3485cfac3 100644 --- a/opencsp/app/sofast/lib/Fringes.py +++ b/opencsp/app/sofast/lib/Fringes.py @@ -25,6 +25,38 @@ def __init__(self, periods_x: list, periods_y: list): self.num_y_images = np.size(periods_y) * self.phase_shifts_y self.num_images = self.num_y_images + self.num_x_images + @classmethod + def from_num_periods(cls, fringe_periods_x=4, fringe_periods_y=4) -> 'Fringes': + """Creates fringes to be displayed during run_measurement(). + + The fringes are displayed as sinusoidal grayscale images. A value of 1 means that only a single large sinusoidal + will be used. A higher value will display more images with a faster sinusoidal. Therefore, a higher value will + result in finer slope resolution, up to the resolving power of the optical setup. + + Note that a different number of periods may be required for x and for y, if the x and y resolutions differ + enough. + + Params: + ------- + fringe_periods_x: int, optional + Granularity for fringe periods in the x direction. Defaults to 4. + fringe_periods_y: int, optional + Granularity for fringe periods in the y direction. Defaults to 4. + + Returns: + fringes: The fringes object, to be used with run_measurement(). + """ + # Get fringe periods + periods_x = [4**idx for idx in range(fringe_periods_x)] + periods_y = [4**idx for idx in range(fringe_periods_y)] + periods_x[0] -= 0.1 + periods_y[0] -= 0.1 + + # Create fringe object + fringes = cls(periods_x, periods_y) + + return fringes + def get_frames(self, x: int, y: int, dtype: str, range_: list[float, float]) -> np.ndarray: """ Returns 3D ndarray of scaled, monochrome fringe images. diff --git a/opencsp/app/sofast/lib/MeasurementSofastFixed.py b/opencsp/app/sofast/lib/MeasurementSofastFixed.py index 4115835e2..0263e507c 100644 --- a/opencsp/app/sofast/lib/MeasurementSofastFixed.py +++ b/opencsp/app/sofast/lib/MeasurementSofastFixed.py @@ -2,12 +2,13 @@ import numpy as np +import opencsp.app.sofast.lib.AbstractMeasurementSofast as ams +import opencsp.app.sofast.lib.DistanceOpticScreen as osd from opencsp.common.lib.geometry.Vxy import Vxy -from opencsp.common.lib.geometry.Vxyz import Vxyz import opencsp.common.lib.tool.hdf5_tools as hdf5_tools -class MeasurementSofastFixed: +class MeasurementSofastFixed(ams.AbstractMeasurementSofast): """Fixed pattern measuremnt class. Stores measurement data. Can save and load to HDF file format. """ @@ -15,10 +16,9 @@ class MeasurementSofastFixed: def __init__( self, image: np.ndarray, - v_measure_point_facet: Vxyz, - dist_optic_screen: float, + dist_optic_screen_measure: osd.DistanceOpticScreen, origin: Vxy, - date: dt.datetime = dt.datetime.now(), + date: dt.datetime = None, name: str = '', ): """Saves measurement data in class. @@ -27,26 +27,21 @@ def __init__( ---------- image : np.ndarray (M, N) ndarray, measurement image - v_measure_point_facet : Vxyz - Location of measurem point on facet, meters - dist_optic_screen : float - Optic to screen distance, meters + dist_optic_screen_measure : DistanceOpticScreen + Measurement point on the optic, and distance from that point to the screen origin : Vxy The centroid of the origin dot, pixels - date : datetime - Collection date/time. - name : str - Name or serial number of measurement. + date : datetime, optional + Collection date/time. Default is dt.datetime.now() + name : str, optional + Name or serial number of measurement. Default is empty string '' """ + super().__init__(dist_optic_screen_measure, date, name) self.image = image - self.v_measure_point_facet = v_measure_point_facet - self.dist_optic_screen = dist_optic_screen self.origin = origin - self.date = date - self.name = name @classmethod - def load_from_hdf(cls, file) -> 'MeasurementSofastFixed': + def load_from_hdf(cls, file: str, prefix='') -> 'MeasurementSofastFixed': """ Loads from HDF file @@ -57,23 +52,15 @@ def load_from_hdf(cls, file) -> 'MeasurementSofastFixed': """ # Load grid data - datasets = [ - 'MeasurementSofastFixed/image', - 'MeasurementSofastFixed/v_measure_point_facet', - 'MeasurementSofastFixed/dist_optic_screen', - 'MeasurementSofastFixed/origin', - 'MeasurementSofastFixed/date', - 'MeasurementSofastFixed/name', - ] + datasets = [prefix + 'MeasurementSofastFixed/image', prefix + 'MeasurementSofastFixed/origin'] kwargs = hdf5_tools.load_hdf5_datasets(datasets, file) + kwargs.update(super()._load_from_hdf(file, prefix + 'MeasurementSofastFixed')) - kwargs['v_measure_point_facet'] = Vxyz(kwargs['v_measure_point_facet']) kwargs['origin'] = Vxy(kwargs['origin']) - kwargs['date'] = dt.datetime.fromisoformat(kwargs['date']) return cls(**kwargs) - def save_to_hdf(self, file) -> None: + def save_to_hdf(self, file: str, prefix='') -> None: """ Saves to HDF file @@ -81,25 +68,10 @@ def save_to_hdf(self, file) -> None: ---------- file : string HDF file to save - - NOTE: Collection date is saved as string in iso-format. """ - datasets = [ - 'MeasurementSofastFixed/image', - 'MeasurementSofastFixed/v_measure_point_facet', - 'MeasurementSofastFixed/dist_optic_screen', - 'MeasurementSofastFixed/origin', - 'MeasurementSofastFixed/date', - 'MeasurementSofastFixed/name', - ] - data = [ - self.image, - self.v_measure_point_facet.data.squeeze(), - self.dist_optic_screen, - self.origin.data.squeeze(), - self.date.isoformat(), - self.name, - ] + datasets = [prefix + 'MeasurementSofastFixed/image', prefix + 'MeasurementSofastFixed/origin'] + data = [self.image, self.origin.data.squeeze()] # Save data hdf5_tools.save_hdf5_datasets(data, datasets, file) + super()._save_to_hdf(file, prefix + 'MeasurementSofastFixed') diff --git a/opencsp/app/sofast/lib/MeasurementSofastFringe.py b/opencsp/app/sofast/lib/MeasurementSofastFringe.py index 5db28636a..1ffe10a0a 100644 --- a/opencsp/app/sofast/lib/MeasurementSofastFringe.py +++ b/opencsp/app/sofast/lib/MeasurementSofastFringe.py @@ -6,11 +6,12 @@ import numpy as np from opencsp.app.sofast.lib.ImageCalibrationAbstract import ImageCalibrationAbstract -from opencsp.common.lib.geometry.Vxyz import Vxyz +import opencsp.app.sofast.lib.AbstractMeasurementSofast as ams +import opencsp.app.sofast.lib.DistanceOpticScreen as osd import opencsp.common.lib.tool.hdf5_tools as hdf5_tools -class MeasurementSofastFringe: +class MeasurementSofastFringe(ams.AbstractMeasurementSofast): """SofastFringe measurement data class that contains captured images and metadata about the measurement. """ @@ -21,8 +22,7 @@ def __init__( fringe_images: np.ndarray, fringe_periods_x: np.ndarray, fringe_periods_y: np.ndarray, - measure_point: Vxyz, - optic_screen_dist: float, + dist_optic_screen_measure: osd.DistanceOpticScreen, date: dt.datetime, name: str = '', ) -> 'MeasurementSofastFringe': @@ -38,16 +38,16 @@ def __init__( MxNxN frame array. Y fringes first fringe_periods_x/y : 1d array Periods used to generate x/y fringes, fractional screens. - measure_point : Vxyz - Location of measure point, meters. - optic_screen_dist : float - Optic-screen distance, meters. + dist_optic_screen_measure : MeasurementDistance + Optic-screen distance measurement. date : datetime Collection date/time. name : str Name or serial number of measurement. """ + super().__init__(dist_optic_screen_measure, date, name) + # Check mask image size if mask_images.shape[2] != 2 or np.ndim(mask_images) != 3: raise ValueError(f'Two mask images needed, but {mask_images.shape[2]} given.') @@ -57,10 +57,6 @@ def __init__( self.fringe_images = fringe_images self.fringe_periods_x = fringe_periods_x self.fringe_periods_y = fringe_periods_y - self.measure_point = measure_point - self.optic_screen_dist = optic_screen_dist - self.date = date - self.name = name # Save calculations self.image_shape_xy = np.flip(self.mask_images.shape[:2]) @@ -76,9 +72,6 @@ def __init__( # Instantiate calibration objected fringes self._fringe_images_calibrated = None - def __repr__(self) -> str: - return 'MeasurementSofastFringe: { ' + self.name + ' }' - @property def fringe_images_y(self) -> np.ndarray: """Returns raw y-only fringes""" @@ -126,7 +119,7 @@ def calibrate_fringe_images(self, calibration: ImageCalibrationAbstract, **kwarg self._fringe_images_calibrated = calibration.apply_to_images(self, **kwargs) @classmethod - def load_from_hdf(cls, file) -> 'MeasurementSofastFringe': + def load_from_hdf(cls, file: str, prefix='') -> 'MeasurementSofastFringe': """ Loads from HDF file @@ -138,23 +131,17 @@ def load_from_hdf(cls, file) -> 'MeasurementSofastFringe': """ # Load grid data datasets = [ - 'MeasurementSofastFringe/mask_images', - 'MeasurementSofastFringe/fringe_images', - 'MeasurementSofastFringe/fringe_periods_x', - 'MeasurementSofastFringe/fringe_periods_y', - 'MeasurementSofastFringe/measure_point', - 'MeasurementSofastFringe/optic_screen_dist', - 'MeasurementSofastFringe/date', - 'MeasurementSofastFringe/name', + prefix + 'MeasurementSofastFringe/mask_images', + prefix + 'MeasurementSofastFringe/fringe_images', + prefix + 'MeasurementSofastFringe/fringe_periods_x', + prefix + 'MeasurementSofastFringe/fringe_periods_y', ] kwargs = hdf5_tools.load_hdf5_datasets(datasets, file) - - kwargs['measure_point'] = Vxyz(kwargs['measure_point']) - kwargs['date'] = dt.datetime.fromisoformat(kwargs['date']) + kwargs.update(super()._load_from_hdf(file, prefix + 'MeasurementSofastFringe')) return cls(**kwargs) - def save_to_hdf(self, file) -> None: + def save_to_hdf(self, file: str, prefix='') -> None: """ Saves to HDF file @@ -162,29 +149,15 @@ def save_to_hdf(self, file) -> None: ---------- file : string HDF file to save - - NOTE: Collection date is saved as string in iso-format. """ datasets = [ - 'MeasurementSofastFringe/mask_images', - 'MeasurementSofastFringe/fringe_images', - 'MeasurementSofastFringe/fringe_periods_x', - 'MeasurementSofastFringe/fringe_periods_y', - 'MeasurementSofastFringe/measure_point', - 'MeasurementSofastFringe/optic_screen_dist', - 'MeasurementSofastFringe/date', - 'MeasurementSofastFringe/name', - ] - data = [ - self.mask_images, - self.fringe_images, - self.fringe_periods_x, - self.fringe_periods_y, - self.measure_point.data.squeeze(), - self.optic_screen_dist, - self.date.isoformat(), - self.name, + prefix + 'MeasurementSofastFringe/mask_images', + prefix + 'MeasurementSofastFringe/fringe_images', + prefix + 'MeasurementSofastFringe/fringe_periods_x', + prefix + 'MeasurementSofastFringe/fringe_periods_y', ] + data = [self.mask_images, self.fringe_images, self.fringe_periods_x, self.fringe_periods_y] # Save data hdf5_tools.save_hdf5_datasets(data, datasets, file) + super()._save_to_hdf(file, prefix + 'MeasurementSofastFringe') diff --git a/opencsp/app/sofast/lib/ProcessSofastFringe.py b/opencsp/app/sofast/lib/ProcessSofastFringe.py index 56cac5c54..353020fe5 100644 --- a/opencsp/app/sofast/lib/ProcessSofastFringe.py +++ b/opencsp/app/sofast/lib/ProcessSofastFringe.py @@ -144,9 +144,9 @@ class ProcessSofastFringe(HDF5_SaveAbstract): - v_cam_optic_cam_refine_3 - v_cam_optic_centroid_cam_exp - CalculationError - - error_optic_screen_dist_1 - - error_optic_screen_dist_2 - - error_optic_screen_dist_3 + - error_dist_optic_screen_1 + - error_dist_optic_screen_2 + - error_dist_optic_screen_3 - error_reprojection_1 - error_reprojection_2 - error_reprojection_3 @@ -310,7 +310,7 @@ def _process_optic_undefined_geometry(self) -> None: ) = po.process_undefined_geometry( mask_raw, self.params.mask_keep_largest_area, - self.measurement.optic_screen_dist, + self.measurement.dist_optic_screen, self.orientation, self.camera, self.params.geometry_data_debug, @@ -362,8 +362,8 @@ def _process_optic_singlefacet_geometry(self, facet_data: DefinitionFacet) -> No ) = po.process_singlefacet_geometry( facet_data, mask_raw, - self.measurement.measure_point, - self.measurement.optic_screen_dist, + self.measurement.v_measure_point_facet, + self.measurement.dist_optic_screen, self.orientation, self.camera, self.params.geometry_params, @@ -424,10 +424,10 @@ def _process_optic_multifacet_geometry( facet_data, ensemble_data, mask_raw, - self.measurement.measure_point, + self.measurement.v_measure_point_facet, self.orientation, self.camera, - self.measurement.optic_screen_dist, + self.measurement.dist_optic_screen, self.params.geometry_params, self.params.geometry_data_debug, ) diff --git a/opencsp/app/sofast/lib/SystemSofastFringe.py b/opencsp/app/sofast/lib/SystemSofastFringe.py index 75f141571..e2266e2ba 100644 --- a/opencsp/app/sofast/lib/SystemSofastFringe.py +++ b/opencsp/app/sofast/lib/SystemSofastFringe.py @@ -11,6 +11,7 @@ from opencsp.app.sofast.lib.Fringes import Fringes from opencsp.app.sofast.lib.ImageCalibrationAbstract import ImageCalibrationAbstract from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe as Measurement +import opencsp.app.sofast.lib.DistanceOpticScreen as osd from opencsp.common.lib.camera.ImageAcquisitionAbstract import ImageAcquisitionAbstract from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection from opencsp.common.lib.geometry.Vxyz import Vxyz @@ -440,7 +441,7 @@ def get_calibration_images(self) -> list[ndarray]: images.append(np.concatenate(ims, axis=2)) return images - def get_measurements(self, v_measure_point: Vxyz, optic_screen_dist: float, name: str) -> list[Measurement]: + def get_measurements(self, v_measure_point: Vxyz, dist_optic_screen: float, name: str) -> list[Measurement]: """ Returns measurement object once mask and fringe images have been captured. @@ -449,7 +450,7 @@ def get_measurements(self, v_measure_point: Vxyz, optic_screen_dist: float, name ---------- v_measure_point : Vxyz Location of measure point in optic coordinates. - optic_screen_dist : float + dist_optic_screen : float Distance from mirror to center of screen during measurement. name : str Name/serial number of measurement. @@ -469,13 +470,13 @@ def get_measurements(self, v_measure_point: Vxyz, optic_screen_dist: float, name measurements = [] for fringe_images, mask_images in zip(self.fringe_images_captured, self.mask_images_captured): # Create measurement object + dist_optic_screen_measure = osd.DistanceOpticScreen(v_measure_point, dist_optic_screen) kwargs = dict( fringe_periods_x=np.array(self.fringes.periods_x), fringe_periods_y=np.array(self.fringes.periods_y), fringe_images=np.concatenate(fringe_images, axis=2), mask_images=np.concatenate(mask_images, axis=2), - measure_point=v_measure_point, - optic_screen_dist=optic_screen_dist, + dist_optic_screen_measure=dist_optic_screen_measure, date=dt.datetime.now(), name=name, ) diff --git a/opencsp/app/sofast/lib/calculation_data_classes.py b/opencsp/app/sofast/lib/calculation_data_classes.py index 855c452f7..52c5a5388 100644 --- a/opencsp/app/sofast/lib/calculation_data_classes.py +++ b/opencsp/app/sofast/lib/calculation_data_classes.py @@ -115,9 +115,9 @@ class CalculationError: errors during deflectometry calculations. """ - error_optic_screen_dist_1: float = None - error_optic_screen_dist_2: float = None - error_optic_screen_dist_3: float = None + error_dist_optic_screen_1: float = None + error_dist_optic_screen_2: float = None + error_dist_optic_screen_3: float = None error_reprojection_1: float = None error_reprojection_2: float = None error_reprojection_3: float = None @@ -133,17 +133,17 @@ def save_to_hdf(self, file: str, prefix: str = ''): Prefix to append to folder path within HDF file (folders must be separated by "/") """ data = [ - self.error_optic_screen_dist_1, - self.error_optic_screen_dist_2, - self.error_optic_screen_dist_3, + self.error_dist_optic_screen_1, + self.error_dist_optic_screen_2, + self.error_dist_optic_screen_3, self.error_reprojection_1, self.error_reprojection_2, self.error_reprojection_3, ] datasets = [ - prefix + 'CalculationError/error_optic_screen_dist_1', - prefix + 'CalculationError/error_optic_screen_dist_2', - prefix + 'CalculationError/error_optic_screen_dist_3', + prefix + 'CalculationError/error_dist_optic_screen_1', + prefix + 'CalculationError/error_dist_optic_screen_2', + prefix + 'CalculationError/error_dist_optic_screen_3', prefix + 'CalculationError/error_reprojection_1', prefix + 'CalculationError/error_reprojection_2', prefix + 'CalculationError/error_reprojection_3', diff --git a/opencsp/app/sofast/lib/process_optics_geometry.py b/opencsp/app/sofast/lib/process_optics_geometry.py index ede1c7403..76fdc27e6 100644 --- a/opencsp/app/sofast/lib/process_optics_geometry.py +++ b/opencsp/app/sofast/lib/process_optics_geometry.py @@ -27,7 +27,7 @@ def process_singlefacet_geometry( facet_data: DefinitionFacet, mask_raw: ndarray, v_measure_point_facet: Vxyz, - optic_screen_dist: float, + dist_optic_screen: float, orientation: SpatialOrientation, camera: Camera, params: ParamsOpticGeometry = ParamsOpticGeometry(), @@ -49,7 +49,7 @@ def process_singlefacet_geometry( Raw calculated mask v_measure_point_facet : Vxyz Measure point location on facet, meters - optic_screen_dist : float + dist_optic_screen : float Optic to screen distance, meters orientation : SpatialOrientation SpatialOrientation object @@ -122,7 +122,7 @@ def process_singlefacet_geometry( # Find expected position of optic centroid v_cam_optic_centroid_cam_exp = sp.t_from_distance( - v_mask_centroid_image, optic_screen_dist, camera, ori.v_cam_screen_cam + v_mask_centroid_image, dist_optic_screen, camera, ori.v_cam_screen_cam ) data_geometry_general.v_cam_optic_centroid_cam_exp = v_cam_optic_centroid_cam_exp @@ -202,7 +202,7 @@ def process_singlefacet_geometry( # Refine V with measured optic to display distance v_cam_optic_cam_refine_2 = sp.refine_v_distance( - v_cam_optic_cam_refine_1, optic_screen_dist, ori.v_cam_screen_cam, v_measure_point_optic_cam_refine_1 + v_cam_optic_cam_refine_1, dist_optic_screen, ori.v_cam_screen_cam, v_measure_point_optic_cam_refine_1 ) data_geometry_general.v_cam_optic_cam_refine_2 = v_cam_optic_cam_refine_2 @@ -223,27 +223,27 @@ def process_singlefacet_geometry( data_geometry_facet.u_cam_measure_point_facet = u_cam_measure_point_facet # Calculate errors from using only facet corners - error_optic_screen_dist_1 = sp.distance_error( - ori.v_cam_screen_cam, v_cam_optic_cam_refine_1 + v_measure_point_optic_cam_refine_1, optic_screen_dist + error_dist_optic_screen_1 = sp.distance_error( + ori.v_cam_screen_cam, v_cam_optic_cam_refine_1 + v_measure_point_optic_cam_refine_1, dist_optic_screen ) - data_error.error_optic_screen_dist_1 = error_optic_screen_dist_1 + data_error.error_dist_optic_screen_1 = error_dist_optic_screen_1 error_reprojection_1 = sp.reprojection_error( camera, v_facet_corners, loop_facet_image_refine.vertices, r_optic_cam_refine_1, v_cam_optic_cam_refine_1 ) data_error.error_reprojection_1 = error_reprojection_1 # Calculate errors after refining with measured distance - error_optic_screen_dist_2 = sp.distance_error( - ori.v_cam_screen_cam, v_cam_optic_cam_refine_2 + v_measure_point_optic_cam_refine_1, optic_screen_dist + error_dist_optic_screen_2 = sp.distance_error( + ori.v_cam_screen_cam, v_cam_optic_cam_refine_2 + v_measure_point_optic_cam_refine_1, dist_optic_screen ) - data_error.error_optic_screen_dist_2 = error_optic_screen_dist_2 + data_error.error_dist_optic_screen_2 = error_dist_optic_screen_2 error_reprojection_2 = sp.reprojection_error( camera, v_facet_corners, loop_facet_image_refine.vertices, r_optic_cam_refine_1, v_cam_optic_cam_refine_2 ) data_error.error_reprojection_2 = error_reprojection_2 # Save other data - data_geometry_facet.measure_point_screen_distance = optic_screen_dist + data_geometry_facet.measure_point_screen_distance = dist_optic_screen data_geometry_facet.spatial_orientation = ori data_geometry_facet.v_align_point_facet = v_centroid_facet @@ -259,7 +259,7 @@ def process_singlefacet_geometry( def process_undefined_geometry( mask_raw: ndarray, mask_keep_largest_area: bool, - optic_screen_dist: float, + dist_optic_screen: float, orientation: SpatialOrientation, camera: Camera, debug: DebugOpticsGeometry = DebugOpticsGeometry(), @@ -278,7 +278,7 @@ def process_undefined_geometry( Raw calculated mask mask_keep_largest_area : bool To apply the "keep largest area" mask operation - optic_screen_dist : float + dist_optic_screen : float Optic centroid to screen distance, meters orientation : SpatialOrientation SpatialOrientation object @@ -329,7 +329,7 @@ def process_undefined_geometry( data_image_processing_general.v_mask_centroid_image = v_mask_centroid_image # Find position of optic centroid in space - v_cam_optic_cam = sp.t_from_distance(v_mask_centroid_image, optic_screen_dist, camera, orientation.v_cam_screen_cam) + v_cam_optic_cam = sp.t_from_distance(v_mask_centroid_image, dist_optic_screen, camera, orientation.v_cam_screen_cam) data_geometry_general.v_cam_optic_cam_exp = v_cam_optic_cam # Find orientation of optic @@ -345,7 +345,7 @@ def process_undefined_geometry( # Save processed optic data data_geometry_facet.u_cam_measure_point_facet = u_cam_measure_point_facet - data_geometry_facet.measure_point_screen_distance = optic_screen_dist + data_geometry_facet.measure_point_screen_distance = dist_optic_screen data_geometry_facet.spatial_orientation = spatial_orientation data_geometry_facet.v_align_point_facet = Vxyz((0, 0, 0)) @@ -365,7 +365,7 @@ def process_multifacet_geometry( v_meas_pt_ensemble: Vxyz, orientation: SpatialOrientation, camera: Camera, - optic_screen_dist: float, + dist_optic_screen: float, params: ParamsOpticGeometry = ParamsOpticGeometry(), debug: DebugOpticsGeometry = DebugOpticsGeometry(), ) -> tuple[ @@ -391,7 +391,7 @@ def process_multifacet_geometry( SpatialOrientation object camera : Camera Camera object - optic_screen_dist : float + dist_optic_screen : float Optic to screen distance, meters params : ParamsOpticGeometry, optional ParamsOpticGeometry object, by default ParamsOpticGeometry() @@ -483,7 +483,7 @@ def process_multifacet_geometry( # Calculate expected position of ensemble centroid v_cam_ensemble_cent_cam_exp = sp.t_from_distance( - v_mask_centroid_image, optic_screen_dist, camera, orientation.v_cam_screen_cam + v_mask_centroid_image, dist_optic_screen, camera, orientation.v_cam_screen_cam ) data_geometry_general.v_cam_optic_centroid_cam_exp = v_cam_ensemble_cent_cam_exp @@ -597,15 +597,15 @@ def process_multifacet_geometry( # Refine T with measured distance v_cam_ensemble_cam_refine_3 = sp.refine_v_distance( - v_cam_ensemble_cam_refine_2, optic_screen_dist, orientation.v_cam_screen_cam, v_meas_pt_ensemble_cam_refine_2 + v_cam_ensemble_cam_refine_2, dist_optic_screen, orientation.v_cam_screen_cam, v_meas_pt_ensemble_cam_refine_2 ) data_geometry_general.v_cam_optic_cam_refine_3 = v_cam_ensemble_cam_refine_3 # Calculate error 1 (R/T calculated using only ensemble perimeter points) - error_optic_screen_dist_1 = sp.distance_error( - orientation.v_cam_screen_cam, v_cam_ensemble_cam_refine_1 + v_meas_pt_ensemble_cam_refine_1, optic_screen_dist + error_dist_optic_screen_1 = sp.distance_error( + orientation.v_cam_screen_cam, v_cam_ensemble_cam_refine_1 + v_meas_pt_ensemble_cam_refine_1, dist_optic_screen ) - data_error.error_optic_screen_dist_1 = error_optic_screen_dist_1 + data_error.error_dist_optic_screen_1 = error_dist_optic_screen_1 error_reprojection_1 = sp.reprojection_error( camera, v_ensemble_corns_ensemble, @@ -616,10 +616,10 @@ def process_multifacet_geometry( data_error.error_reprojection_1 = error_reprojection_1 # Calculate error 2 (R/T calculated using all facet corners) - error_optic_screen_dist_2 = sp.distance_error( - orientation.v_cam_screen_cam, v_cam_ensemble_cam_refine_2 + v_meas_pt_ensemble_cam_refine_2, optic_screen_dist + error_dist_optic_screen_2 = sp.distance_error( + orientation.v_cam_screen_cam, v_cam_ensemble_cam_refine_2 + v_meas_pt_ensemble_cam_refine_2, dist_optic_screen ) - data_error.error_optic_screen_dist_2 = error_optic_screen_dist_2 + data_error.error_dist_optic_screen_2 = error_dist_optic_screen_2 error_reprojection_2 = sp.reprojection_error( camera, v_ensemble_facet_corns_all, @@ -630,10 +630,10 @@ def process_multifacet_geometry( data_error.error_reprojection_2 = error_reprojection_2 # Calculate error 3 (T refined using measured distance) - error_optic_screen_dist_3 = sp.distance_error( - orientation.v_cam_screen_cam, v_cam_ensemble_cam_refine_3 + v_meas_pt_ensemble_cam_refine_2, optic_screen_dist + error_dist_optic_screen_3 = sp.distance_error( + orientation.v_cam_screen_cam, v_cam_ensemble_cam_refine_3 + v_meas_pt_ensemble_cam_refine_2, dist_optic_screen ) - data_error.error_optic_screen_dist_3 = error_optic_screen_dist_3 + data_error.error_dist_optic_screen_3 = error_dist_optic_screen_3 error_reprojection_3 = sp.reprojection_error( camera, v_ensemble_facet_corns_all, diff --git a/opencsp/app/sofast/lib/spatial_processing.py b/opencsp/app/sofast/lib/spatial_processing.py index a7e0484aa..96f72dde5 100644 --- a/opencsp/app/sofast/lib/spatial_processing.py +++ b/opencsp/app/sofast/lib/spatial_processing.py @@ -81,7 +81,7 @@ def r_from_position(v_cam_optic_cam: Vxyz, v_cam_screen_cam: Vxyz) -> Rotation: def refine_v_distance( - v_cam_optic_cam: Vxyz, optic_screen_dist: float, v_cam_screen_cam: Vxyz, v_meas_pt_optic_cam: Vxyz + v_cam_optic_cam: Vxyz, dist_optic_screen: float, v_cam_screen_cam: Vxyz, v_meas_pt_optic_cam: Vxyz ) -> Vxyz: """ Refines the camera to optic translation vector so that measured optic @@ -91,7 +91,7 @@ def refine_v_distance( ---------- v_cam_optic_cam : Vxyz Camera to optic vector in camera coordinates. - optic_screen_dist : float + dist_optic_screen : float Measured optic to screen distance. v_cam_screen_cam : Vxyz Camera to screen vector in camera coordinates. @@ -108,7 +108,7 @@ def refine_v_distance( def error_func(scale): # Calculate the distance error v_cam_meas_pt_cam = (v_cam_optic_cam * scale) + v_meas_pt_optic_cam - error = distance_error(v_cam_screen_cam, v_cam_meas_pt_cam, optic_screen_dist) + error = distance_error(v_cam_screen_cam, v_cam_meas_pt_cam, dist_optic_screen) return np.abs(error) # Perform optimization diff --git a/opencsp/app/sofast/test/test_ImageCalibrationGlobal.py b/opencsp/app/sofast/test/test_ImageCalibrationGlobal.py index 1c0382753..97b458919 100644 --- a/opencsp/app/sofast/test/test_ImageCalibrationGlobal.py +++ b/opencsp/app/sofast/test/test_ImageCalibrationGlobal.py @@ -8,6 +8,7 @@ from opencsp.app.sofast.lib.ImageCalibrationGlobal import ImageCalibrationGlobal from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe as Measurement +import opencsp.app.sofast.lib.DistanceOpticScreen as osd from opencsp.common.lib.geometry.Vxyz import Vxyz @@ -47,8 +48,15 @@ def test_apply_to_images(self): ) # Create measurement object + dist_optic_screen_measure = osd.DistanceOpticScreen(Vxyz((0, 0, 0)), 10) measurement = Measurement( - mask_images, fringe_images, np.array([0.0]), np.array([0.0]), Vxyz((0, 0, 0)), 10, dt.datetime.now(), 'Test' + mask_images, + fringe_images, + np.array([0.0]), + np.array([0.0]), + dist_optic_screen_measure, + dt.datetime.now(), + 'Test', ) # Calibrate diff --git a/opencsp/app/sofast/test/test_spatial_processing.py b/opencsp/app/sofast/test/test_spatial_processing.py index fa541b9d1..ba8be45b1 100644 --- a/opencsp/app/sofast/test/test_spatial_processing.py +++ b/opencsp/app/sofast/test/test_spatial_processing.py @@ -43,7 +43,7 @@ def test_t_from_distance(self): # Perform calculation v_cam_optic_cam_exp = sp.t_from_distance( Vxy(data['v_mask_centroid_image']), - measurement.optic_screen_dist, + measurement.dist_optic_screen, self.camera, self.orientation.v_cam_screen_cam, ).data.squeeze() @@ -104,7 +104,7 @@ def test_distance_error(self): # Perform calculation error_optic_screen_dist_2 = sp.distance_error( - self.orientation.v_cam_screen_cam, Vxyz(data['v_cam_optic_cam_refine_2']), measurement.optic_screen_dist + self.orientation.v_cam_screen_cam, Vxyz(data['v_cam_optic_cam_refine_2']), measurement.dist_optic_screen ) # Test @@ -147,10 +147,10 @@ def test_refine_v_distance(self): # Perform calculation r_cam_optic = Rotation.from_rotvec(data['r_optic_cam_refine_1']) - v_meas_pt_optic_cam = measurement.measure_point.rotate(r_cam_optic) + v_meas_pt_optic_cam = measurement.v_measure_point_facet.rotate(r_cam_optic) v_cam_optic_cam_refine_2 = sp.refine_v_distance( Vxyz(data['v_cam_optic_cam_refine_1']), - measurement.optic_screen_dist, + measurement.dist_optic_screen, self.orientation.v_cam_screen_cam, v_meas_pt_optic_cam, ).data.squeeze() diff --git a/opencsp/common/lib/camera/ImageAcquisitionAbstract.py b/opencsp/common/lib/camera/ImageAcquisitionAbstract.py index 1d2a08e3e..6fef4259f 100644 --- a/opencsp/common/lib/camera/ImageAcquisitionAbstract.py +++ b/opencsp/common/lib/camera/ImageAcquisitionAbstract.py @@ -100,10 +100,18 @@ def gain(self): pass @abstractproperty - def exposure_time(self): - """Camera exposure_time value (units of time)""" + def exposure_time(self) -> int: + """Camera exposure_time value (microseconds)""" pass + @property + def exposure_time_seconds(self) -> float: + return self.exposure_time / 1_000_000 + + @exposure_time_seconds.setter + def exposure_time_seconds(self, exposure_time_seconds: float): + self.exposure_time = int(exposure_time_seconds / 1_000_000) + @abstractproperty def frame_size(self): """camera frame size (X pixels by Y pixels)""" diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index afd307bc4..b3c07b258 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -9,6 +9,7 @@ import opencsp.common.lib.tool.exception_tools as et import opencsp.common.lib.tool.hdf5_tools as hdf5_tools +import opencsp.common.lib.tool.tk_tools as tkt class CalParams: @@ -126,7 +127,7 @@ def in_new_window(cls, display_data: dict): """ # Create new tkinter window - root = tkinter.Tk() + root = tkt.window() # Instantiate class return cls(root, display_data) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index a18da57ea..adb7c84de 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -10,6 +10,7 @@ from tkinter.filedialog import asksaveasfilename from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +import opencsp.common.lib.tool.tk_tools as tkt class ImageProjectionGUI: @@ -80,7 +81,7 @@ def __init__(self): self.display_data: dict # Create tkinter object - self.root = tkinter.Tk() + self.root = tkt.window() # Set title self.root.title('ImageProjection Setup') @@ -201,7 +202,7 @@ def show_projector(self): self.update_window_size() # Create a new Toplevel window - projector_root = tkinter.Toplevel(self.root) + projector_root = tkt.window(self.root, Toplevel=True) self.projector = ImageProjection(projector_root, self.display_data) # Activate buttons diff --git a/opencsp/common/lib/deflectometry/SlopeSolver.py b/opencsp/common/lib/deflectometry/SlopeSolver.py index b6701cdf5..8e535b4d3 100644 --- a/opencsp/common/lib/deflectometry/SlopeSolver.py +++ b/opencsp/common/lib/deflectometry/SlopeSolver.py @@ -150,7 +150,7 @@ def fit_surface(self) -> None: v_optic_screen_optic, v_meas_pts_surf_int_optic, ) - out = minimize(sf2.optic_screen_dist_error, np.array([1.0]), args=args) + out = minimize(sf2.dist_optic_screen_error, np.array([1.0]), args=args) scale = out.x[0] v_align_optic_step = (v_optic_cam_optic - v_align_point_optic) * (scale - 1) diff --git a/opencsp/common/lib/deflectometry/slope_fitting_2d.py b/opencsp/common/lib/deflectometry/slope_fitting_2d.py index dab700526..7197a902d 100644 --- a/opencsp/common/lib/deflectometry/slope_fitting_2d.py +++ b/opencsp/common/lib/deflectometry/slope_fitting_2d.py @@ -205,7 +205,7 @@ def coef_to_points(Pxy: Vxyz, coefficients: np.ndarray, poly_order: int) -> np.n return np.matmul(terms, coefficients.reshape((coefficients.size, -1))).squeeze() -def optic_screen_dist_error( +def dist_optic_screen_error( scale: float, dist_meas: float, v_align_point_optic: Vxyz, diff --git a/opencsp/common/lib/deflectometry/test/test_SlopeSolver.py b/opencsp/common/lib/deflectometry/test/test_SlopeSolver.py index 7d5b1dfc4..aae80105d 100644 --- a/opencsp/common/lib/deflectometry/test/test_SlopeSolver.py +++ b/opencsp/common/lib/deflectometry/test/test_SlopeSolver.py @@ -65,8 +65,8 @@ def setUpClass(cls): 'u_measure_pixel_pointing_optic': Uxyz(data['u_cam_measure_point_facet']), 'v_screen_points_facet': Vxyz(data['v_screen_points_facet']), 'v_optic_screen_optic': ori.v_optic_screen_optic, - 'v_align_point_optic': measurement.measure_point, - 'dist_optic_screen': measurement.optic_screen_dist, + 'v_align_point_optic': measurement.v_measure_point_facet, + 'dist_optic_screen': measurement.dist_optic_screen, 'surface': surface, } diff --git a/opencsp/common/lib/tool/hdf5_tools.py b/opencsp/common/lib/tool/hdf5_tools.py index 1505a84de..228664f98 100644 --- a/opencsp/common/lib/tool/hdf5_tools.py +++ b/opencsp/common/lib/tool/hdf5_tools.py @@ -254,8 +254,9 @@ def save_to_hdf(self, file: str, prefix: str = '') -> None: ---------- file : str HDF file to save to - prefix : str - Prefix to append to folder path within HDF file (folders must be separated by "/") + prefix : str, optional + Prefix to append to folder path within HDF file (folders must be separated by "/"). + Default is empty string ''. """ @@ -270,7 +271,8 @@ def load_from_hdf(cls, file: str, prefix: str = ''): Parameters ---------- file : str - HDF file to save to - prefix : str - Prefix to append to folder path within HDF file (folders must be separated by "/") + HDF file to load from + prefix : str, optional + Prefix to append to folder path within HDF file (folders must be separated by "/"). + Default is empty string ''. """ diff --git a/opencsp/common/lib/tool/TkToolTip.py b/opencsp/common/lib/tool/tk_tools.py similarity index 54% rename from opencsp/common/lib/tool/TkToolTip.py rename to opencsp/common/lib/tool/tk_tools.py index 94c0fc1e3..29fb5aae6 100644 --- a/opencsp/common/lib/tool/TkToolTip.py +++ b/opencsp/common/lib/tool/tk_tools.py @@ -1,57 +1,93 @@ -import tkinter as tk - - -class TkToolTip(object): - def __init__(self, widget, text='widget info'): - self.waittime = 500 # miliseconds - self.wraplength = 180 # pixels - self.widget = widget - self.text = text - self.widget.bind("", self.enter) - self.widget.bind("", self.leave) - self.widget.bind("", self.leave) - self.id_ = None - self.tw = None - - def enter(self, event=None): - self.schedule() - - def leave(self, event=None): - self.unschedule() - self.hidetip() - - def schedule(self): - self.unschedule() - self.id_ = self.widget.after(self.waittime, self.showtip) - - def unschedule(self): - id_ = self.id_ - self.id_ = None - if id_: - self.widget.after_cancel(id_) - - def showtip(self, event=None): - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() + self.widget.winfo_width() - y += self.widget.winfo_rooty() + self.widget.winfo_height() - # Creates a toplevel window - self.tw = tk.Toplevel(self.widget) - # Leaves only the label and removes the app window - self.tw.wm_overrideredirect(True) - self.tw.wm_geometry(f"+{x:d}+{y:d}") - label = tk.Label( - self.tw, - text=self.text, - justify='left', - background="#ffffff", - relief='solid', - borderwidth=1, - wraplength=self.wraplength, - ) - label.pack(ipadx=1) - - def hidetip(self): - tw = self.tw - self.tw = None - if tw: - tw.destroy() +import tkinter as tk + +import opencsp.common.lib.tool.log_tools as lt + + +class TkToolTip(object): + def __init__(self, widget, text='widget info'): + self.waittime = 500 # miliseconds + self.wraplength = 180 # pixels + self.widget = widget + self.text = text + self.widget.bind("", self.enter) + self.widget.bind("", self.leave) + self.widget.bind("", self.leave) + self.id_ = None + self.tw = None + + def enter(self, event=None): + self.schedule() + + def leave(self, event=None): + self.unschedule() + self.hidetip() + + def schedule(self): + self.unschedule() + self.id_ = self.widget.after(self.waittime, self.showtip) + + def unschedule(self): + id_ = self.id_ + self.id_ = None + if id_: + self.widget.after_cancel(id_) + + def showtip(self, event=None): + x, y, cx, cy = self.widget.bbox("insert") + x += self.widget.winfo_rootx() + self.widget.winfo_width() + y += self.widget.winfo_rooty() + self.widget.winfo_height() + # Creates a toplevel window + self.tw = window(self.widget, TopLevel=True) + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + self.tw.wm_geometry(f"+{x:d}+{y:d}") + label = tk.Label( + self.tw, + text=self.text, + justify='left', + background="#ffffff", + relief='solid', + borderwidth=1, + wraplength=self.wraplength, + ) + label.pack(ipadx=1) + + def hidetip(self): + tw = self.tw + self.tw = None + if tw: + tw.destroy() + + +def window(*vargs, TopLevel=False, **kwargs): + """Initializes and returns a new tkinter.Tk (or tkinter.TopLevel) instance. + + If creating the window fails, tries again (up to two more times). + + Sometimes initializing a tk window fails. But if you try to + initialize the window again, it seems to always succeed. + When it fails, it is often with an error about not being able to find a + file. Something like:: + + _tkinter.TclError: Can't find a usable init.tcl in the following directories + """ + window_class = tk.Tk + if TopLevel: + window_class = tk.Toplevel + + try: + # try to create a window + return window_class(*vargs, **kwargs) + except Exception: + try: + lt.warn("Failed to create a tkinter.Tk() window. Trying again (2nd attempt).") + # first attempt failed, try again + return window_class(*vargs, **kwargs) + except Exception: + # second attempt failed, give the system a second to stabalize and + # try a third time + lt.warn("Failed to create a tkinter.Tk() window. Trying again (3rd attempt).") + import time + + time.sleep(1) + return window_class(*vargs, **kwargs)