From c351bf7b0655ccd347479af819f95c1f16d19aa6 Mon Sep 17 00:00:00 2001 From: Braden Date: Fri, 24 May 2024 14:44:06 -0600 Subject: [PATCH 01/36] Added dataclass to IP --- .../lib/deflectometry/ImageProjection.py | 342 +++++++++--------- 1 file changed, 178 insertions(+), 164 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index c2b9c28e..7ce8a772 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -1,6 +1,7 @@ """Class handling the projection of images on a monitor/projector """ +from dataclasses import dataclass import tkinter from typing import Callable, Optional @@ -11,6 +12,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 +from opencsp.common.lib.tool.hdf5_tools import HDF5_IO_Abstract class CalParams: @@ -61,18 +63,81 @@ def __init__(self, size_x: int, size_y: int) -> 'CalParams': self.index: np.ndarray[int] = np.arange(self.x_pixel.size, dtype=int) +@dataclass +class ImageProjectionData(HDF5_IO_Abstract): + """Dataclass containting ImageProjection parameters. All position/size units are screen pixels""" + + name: str + main_window_size_x: int + main_window_size_y: int + main_window_position_x: int + main_window_position_y: int + active_area_size_x: int + active_area_size_y: int + active_area_position_x: int + active_area_position_y: int + projector_data_type: str + projector_max_int: int + image_delay_ms: float + shift_red_x: int + shift_red_y: int + shift_blue_x: int + shift_blue_y: int + ui_position_x: int + + def save_to_hdf(self, file: str, prefix: str = '') -> None: + datasets = [] + data = [] + for name, value in self.__dict__.items(): + datasets.append(prefix + 'ImageProjection/' + name) + data.append(value) + + # Save data + hdf5_tools.save_hdf5_datasets(data, datasets, file) + + @classmethod + def load_from_hdf(cls, file: str, prefix: str = ''): + # Load data + datasets = [ + 'ImageProjection/name', + 'ImageProjection/main_window_size_x', + 'ImageProjection/main_window_size_y', + 'ImageProjection/main_window_position_x', + 'ImageProjection/main_window_position_y', + 'ImageProjection/active_area_size_x', + 'ImageProjection/active_area_size_y', + 'ImageProjection/active_area_position_x', + 'ImageProjection/active_area_position_y', + 'ImageProjection/projector_data_type', + 'ImageProjection/projector_max_int', + 'ImageProjection/image_delay_ms', + 'ImageProjection/shift_red_x', + 'ImageProjection/shift_red_y', + 'ImageProjection/shift_blue_x', + 'ImageProjection/shift_blue_y', + 'ImageProjection/ui_position_x', + ] + for dataset in datasets: + dataset = prefix + dataset + + kwargs = hdf5_tools.load_hdf5_datasets(datasets, file) + + return cls(**kwargs) + + class ImageProjection(hdf5_tools.HDF5_SaveAbstract): + """Controls projecting an image on a computer display (projector, monitor, etc.)""" + _instance: 'ImageProjection' = None - def __init__(self, root: tkinter.Tk, display_data: dict): - """ - Image projection control. + def __init__(self, root: tkinter.Tk, display_data: ImageProjectionData): + """Instantiates class Parameters ---------- root : tkinter.Tk Tk window root. - display_data : dict + display_data : ImageProjectionData Display geometry parameters """ @@ -93,7 +158,15 @@ def __init__(self, root: tkinter.Tk, display_data: dict): self.canvas.configure(background='black', highlightthickness=0) # Save active area data - self.upate_window(display_data) + self._x_active_1: int + self._x_active_2: int + self._y_active_1: int + self._y_active_2: int + self._x_active_mid: int + self._y_active_mid: int + + self._display_data = display_data + self.update_window() # Make window frameless self.root.overrideredirect(1) @@ -106,7 +179,10 @@ def __init__(self, root: tkinter.Tk, display_data: dict): # Create black image image = self._format_image( - np.zeros((self.win_size_y, self.win_size_x, 3), dtype=self.display_data['projector_data_type']) + np.zeros( + (self._display_data.main_window_size_y, self._display_data.main_window_size_x, 3), + dtype=self._display_data.projector_data_type, + ) ) self.canvas_image = self.canvas.create_image(0, 0, image=image, anchor='nw') @@ -126,18 +202,18 @@ def instance(cls) -> Optional["ImageProjection"]: """ return cls._instance - def run(self): + def run(self) -> None: """Runs the Tkinter instance""" self.root.mainloop() @classmethod - def in_new_window(cls, display_data: dict): + def in_new_window(cls, display_data: ImageProjectionData) -> 'ImageProjection': """ Instantiates ImageProjection object in new window. Parameters ---------- - display_data : dict + display_data : ImageProjectionData Display data used to create window. Returns @@ -150,52 +226,26 @@ def in_new_window(cls, display_data: dict): # Instantiate class return cls(root, display_data) - def upate_window(self, display_data: dict) -> None: - """ - Updates window display data. - - Parameters - ---------- - display_data : dict - Input data for position/shift/timing/etc. - - """ - # Save display_data - self.display_data = display_data - - # Save window position data - self.win_size_x = display_data['win_size_x'] - self.win_size_y = display_data['win_size_y'] - self.win_position_x = display_data['win_position_x'] - self.win_position_y = display_data['win_position_y'] - - # Save active area position data - self.size_x = display_data['size_x'] - self.size_y = display_data['size_y'] - self.position_x = display_data['position_x'] - self.position_y = display_data['position_y'] - - # Save maximum projector data integer - self.max_int = display_data['projector_max_int'] - self.dtype = display_data['projector_data_type'] - + def update_window(self) -> None: + """Updates window display data.""" # Calculate active area extents - self.x_active_1 = self.position_x - self.x_active_2 = self.position_x + self.size_x - self.y_active_1 = self.position_y - self.y_active_2 = self.position_y + self.size_y + self._x_active_1 = self._display_data.active_area_position_x + self._x_active_2 = self._display_data.active_area_position_x + self._display_data.active_area_size_x + self._y_active_1 = self._display_data.active_area_position_y + self._y_active_2 = self._display_data.active_area_position_y + self._display_data.active_area_size_y # Calculate center of active area - self.x_active_mid = int(self.size_x / 2) - self.y_active_mid = int(self.size_y / 2) + self._x_active_mid = int(self._display_data.active_area_size_x / 2) + self._y_active_mid = int(self._display_data.active_area_size_y / 2) # Resize window self.root.geometry( - '{:d}x{:d}+{:d}+{:d}'.format(self.win_size_x, self.win_size_y, self.win_position_x, self.win_position_y) + f'{self._display_data.main_window_size_x:d}x{self._display_data.main_window_size_y:d}' + + f'+{self._display_data.main_window_position_x:d}+{self._display_data.main_window_position_y:d}' ) # Resize canvas size - self.canvas.configure(width=self.win_size_x, height=self.win_size_y) + self.canvas.configure(width=self._display_data.main_window_size_x, height=self._display_data.main_window_size_y) def show_crosshairs(self) -> None: """ @@ -203,18 +253,24 @@ def show_crosshairs(self) -> None: """ # Add white active region - array = np.ones((self.size_y, self.size_x, 3), dtype=self.display_data['projector_data_type']) * self.max_int + array = ( + np.ones( + (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), + dtype=self._display_data.projector_data_type, + ) + * self._display_data.projector_max_int + ) # Add crosshairs vertical - array[:, self.x_active_mid, :] = 0 - array[self.y_active_mid, :, :] = 0 + array[:, self._x_active_mid, :] = 0 + array[self._y_active_mid, :, :] = 0 # Add crosshairs diagonal - width = np.min([self.size_x, self.size_y]) - xd1 = int(self.x_active_mid - width / 4) - xd2 = int(self.x_active_mid + width / 4) - yd1 = int(self.y_active_mid - width / 4) - yd2 = int(self.y_active_mid + width / 4) + width = np.min([self._display_data.active_area_size_x, self._display_data.active_area_size_y]) + xd1 = int(self._x_active_mid - width / 4) + xd2 = int(self._x_active_mid + width / 4) + yd1 = int(self._y_active_mid - width / 4) + yd2 = int(self._y_active_mid + width / 4) xds = np.arange(xd1, xd2, dtype=int) yds = np.arange(yd1, yd2, dtype=int) array[yds, xds, :] = 0 @@ -229,22 +285,28 @@ def show_axes(self) -> None: """ # Add white active region - array = np.ones((self.size_y, self.size_x, 3), dtype=self.display_data['projector_data_type']) * self.max_int + array = ( + np.ones( + (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), + dtype=self._display_data.projector_data_type, + ) + * self._display_data.projector_max_int + ) # Add arrows - width = int(np.min([self.size_x, self.size_y]) / 4) + width = int(np.min([self._display_data.active_area_size_x, self._display_data.active_area_size_y]) / 4) thickness = 5 # Add green X axis arrow - start_point = (self.x_active_mid, self.y_active_mid) - end_point = (self.x_active_mid - width, self.y_active_mid) - color = (int(self.max_int), 0, 0) # RGB + start_point = (self._x_active_mid, self._y_active_mid) + end_point = (self._x_active_mid - width, self._y_active_mid) + color = (int(self._display_data.projector_max_int), 0, 0) # RGB array = cv.arrowedLine(array, start_point, end_point, color, thickness) # Add red Y axis arrow - start_point = (self.x_active_mid, self.y_active_mid) - end_point = (self.x_active_mid, self.y_active_mid + width) - color = (0, int(self.max_int), 0) # RGB + start_point = (self._x_active_mid, self._y_active_mid) + end_point = (self._x_active_mid, self._y_active_mid + width) + color = (0, int(self._display_data.projector_max_int), 0) # RGB array = cv.arrowedLine(array, start_point, end_point, color, thickness) # Add X text @@ -252,10 +314,10 @@ def show_axes(self) -> None: array = cv.putText( array, 'X', - (self.x_active_mid - width - 20, self.y_active_mid + 20), + (self._x_active_mid - width - 20, self._y_active_mid + 20), font, 6, - (int(self.max_int), 0, 0), + (int(self._display_data.projector_max_int), 0, 0), 2, bottomLeftOrigin=True, ) @@ -263,7 +325,13 @@ def show_axes(self) -> None: # Add Y text font = cv.FONT_HERSHEY_PLAIN array = cv.putText( - array, 'Y', (self.x_active_mid + 20, self.y_active_mid + width + 20), font, 6, (0, int(self.max_int), 0), 2 + array, + 'Y', + (self._x_active_mid + 20, self._y_active_mid + width + 20), + font, + 6, + (0, int(self._display_data.projector_max_int), 0), + 2, ) # Display image @@ -274,18 +342,21 @@ def show_calibration_image(self): on a white background. Fiducial locations measured from center of dots. """ # Create base image to show - array = np.ones((self.size_y, self.size_x, 3), dtype=self.dtype) + array = np.ones( + (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), + dtype=self._display_data.projector_data_type, + ) # Get calibration pattern parameters - pattern_params = CalParams(self.size_x, self.size_y) + pattern_params = CalParams(self._display_data.active_area_size_x, self._display_data.active_area_size_y) # Add fiducials for x_loc, y_loc, idx in zip(pattern_params.x_pixel, pattern_params.y_pixel, pattern_params.index): # Place fiducial - array[y_loc, x_loc, 1] = self.max_int + array[y_loc, x_loc, 1] = self._display_data.projector_max_int # Place label (offset so label is in view) - x_pt_to_center = float(self.size_x) / 2 - x_loc - y_pt_to_center = float(self.size_y) / 2 - y_loc + x_pt_to_center = float(self._display_data.active_area_size_x) / 2 - x_loc + y_pt_to_center = float(self._display_data.active_area_size_y) / 2 - y_loc if x_pt_to_center >= 0: dx = 15 else: @@ -302,15 +373,18 @@ def show_calibration_image(self): def get_black_array_active_area(self) -> np.ndarray: """ - Creates a black image to fill the active area of self.size_y by self.size_x pixels. + Creates a black image to fill the active area of self._display_data.active_area_size_y by self._display_data.active_area_size_x pixels. Returns: -------- image : np.ndarray - A 2D image with shape (self.size_y, self.size_x, 3), filled with zeros + A 2D image with shape (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), filled with zeros """ # Create black image - black_image = np.zeros((self.size_y, self.size_x, 3), dtype=self.display_data['projector_data_type']) + black_image = np.zeros( + (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), + dtype=self._display_data.projector_data_type, + ) return black_image @@ -321,7 +395,7 @@ def display_image(self, array: np.ndarray) -> None: Parameters ---------- array : ndarray - NxMx3 image array. Data must be int ranging from 0 to self.max_int. + NxMx3 image array. Data must be int ranging from 0 to self._display_data.projector_max_int. Array XY shape must match window size in pixels. """ @@ -330,29 +404,32 @@ def display_image(self, array: np.ndarray) -> None: raise ValueError('Input array must have 3 dimensions and dimension 2 must be length 3.') # Check array is correct xy shape - if array.shape[0] != self.win_size_y or array.shape[1] != self.win_size_x: + if ( + array.shape[0] != self._display_data.main_window_size_y + or array.shape[1] != self._display_data.main_window_size_x + ): raise ValueError( - 'Input image incorrect size. Input image size is {}, but frame size is {}.'.format( - array.shape[:2], (self.win_size_y, self.win_size_x) - ) + f'Input image incorrect size. Input image size is {array.shape[:2]:d},' + + f' but frame size is {self._display_data.main_window_size_y:d}x' + + f'{self._display_data.main_window_size_x:d}.' ) # Format image image = self._format_image(array) # Display image - self.canvas.imgref = image + self.canvas.imgref = image # So garbage collector doesn't collect `image` self.canvas.itemconfig(self.canvas_image, image=image) def display_image_in_active_area(self, array: np.ndarray) -> None: """Formats and displays input numpy array in active area only. Input - array must have size (self.size_y, self.size_x, 3) and is displayed + array must have size (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3) and is displayed with a black border to fill entire window area. Parameters ---------- array : ndarray - NxMx3 image array. Data must be int ranging from 0 to self.max_int. + NxMx3 image array. Data must be int ranging from 0 to self._display_data.projector_max_int. Array XY shape must match active window area size in pixels. """ @@ -361,16 +438,22 @@ def display_image_in_active_area(self, array: np.ndarray) -> None: raise ValueError('Input array must have 3 dimensions and dimension 2 must be length 3.') # Check array is correct xy shape - if array.shape[0] != self.size_y or array.shape[1] != self.size_x: + if ( + array.shape[0] != self._display_data.active_area_size_y + or array.shape[1] != self._display_data.active_area_size_x + ): raise ValueError( - 'Input image incorrect size. Input image size is {}, but frame size is {}.'.format( - array.shape[:2], (self.size_y, self.size_x) - ) + f'Input image incorrect size. Input image size is {array.shape[:2]:d},' + + f' but frame size is {self._display_data.active_area_size_y:d}x' + + f'{self._display_data.active_area_size_x:d}.' ) # Create black image and place array in correct position - array_out = np.zeros((self.win_size_y, self.win_size_x, 3), dtype=self.display_data['projector_data_type']) - array_out[self.y_active_1 : self.y_active_2, self.x_active_1 : self.x_active_2, :] = array + array_out = np.zeros( + (self._display_data.main_window_size_y, self._display_data.main_window_size_x, 3), + dtype=self._display_data.projector_data_type, + ) + array_out[self._y_active_1 : self._y_active_2, self._x_active_1 : self._x_active_2, :] = array # Display image self.display_image(array_out) @@ -391,64 +474,19 @@ def _format_image(self, array: np.ndarray) -> ImageTk.PhotoImage: """ # Shift red channel - array[..., 0] = np.roll(array[..., 0], self.display_data['shift_red_x'], 1) - array[..., 0] = np.roll(array[..., 0], self.display_data['shift_red_y'], 0) + array[..., 0] = np.roll(array[..., 0], self._display_data.shift_red_x, 1) + array[..., 0] = np.roll(array[..., 0], self._display_data.shift_red_y, 0) # Shift blue channel - array[..., 2] = np.roll(array[..., 2], self.display_data['shift_blue_x'], 1) - array[..., 2] = np.roll(array[..., 2], self.display_data['shift_blue_y'], 0) + array[..., 2] = np.roll(array[..., 2], self._display_data.shift_blue_x, 1) + array[..., 2] = np.roll(array[..., 2], self._display_data.shift_blue_y, 0) # Format array into tkinter format image = Image.fromarray(array, 'RGB') return ImageTk.PhotoImage(image) @classmethod - def load_from_hdf(cls, file: str, prefix: str = '') -> dict[str, int | str]: - """ - Loads ImageProjection data from the given HDF file. Assumes data is stored as: PREFIX + Folder/Field_1 - - Note: does NOT return an ImageProjection instance, since that would require creating a new tkinter window. - - 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 ''. - - Returns - ------- - display_data: dict[str, int | str] - The display data which can be used to create an instance of the ImageProjection class. - - """ - # Load data - datasets = [ - 'ImageProjection/name', - 'ImageProjection/win_size_x', - 'ImageProjection/win_size_y', - 'ImageProjection/win_position_x', - 'ImageProjection/win_position_y', - 'ImageProjection/size_x', - 'ImageProjection/size_y', - 'ImageProjection/position_x', - 'ImageProjection/position_y', - 'ImageProjection/projector_data_type', - 'ImageProjection/projector_max_int', - 'ImageProjection/image_delay', - 'ImageProjection/shift_red_x', - 'ImageProjection/shift_red_y', - 'ImageProjection/shift_blue_x', - 'ImageProjection/shift_blue_y', - 'ImageProjection/ui_position_x', - ] - for i in range(len(datasets)): - datasets[i] = prefix + datasets[i] - return hdf5_tools.load_hdf5_datasets(datasets, file) - - @classmethod - def load_from_hdf_and_display(cls, file: str, prefix: str = '') -> 'ImageProjection': + def load_from_hdf(cls, file: str, prefix: str = '') -> 'ImageProjection': """Loads display_data from the given file into a new image_projection window. Assumes data is stored as: PREFIX + Folder/Field_1 Parameters @@ -465,36 +503,12 @@ def load_from_hdf_and_display(cls, file: str, prefix: str = '') -> 'ImageProject The new ImageProjection instance, opened in a new tkinter window. """ # Load data - display_data = cls.load_from_hdf(file, prefix) + display_data = ImageProjectionData.load_from_hdf(file, prefix) # Open window return cls.in_new_window(display_data) - @staticmethod - def save_params_to_hdf(display_data: dict, file: str, prefix: str = ''): - """Saves image projection display_data parameters to given file. Data is stored as: PREFIX + Folder/Field_1 - - Parameters - ---------- - display_data: dict - The values to save to the given file. Should be the display_data dict from an ImageProjection instance. - 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 ''. - """ - # Extract data entries - datasets = [] - data = [] - for field in display_data.keys(): - datasets.append(prefix + 'ImageProjection/' + field) - data.append(display_data[field]) - - # Save data - hdf5_tools.save_hdf5_datasets(data, datasets, file) - - def save_to_hdf(self, file: str, prefix: str = ''): + def save_to_hdf(self, file: str, prefix: str = '') -> None: """Saves image projection display_data parameters to given file. Data is stored as: PREFIX + Folder/Field_1 Parameters @@ -505,7 +519,7 @@ def save_to_hdf(self, file: str, prefix: str = ''): Prefix to append to folder path within HDF file (folders must be separated by "/"). Default is empty string ''. """ - self.save_params_to_hdf(self.display_data, file, prefix) + self._display_data.save_to_hdf(file, prefix) def close(self): """Closes all windows""" From 17be7ce58d868d63466d9752678d56a455f1dd0d Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 09:39:06 -0600 Subject: [PATCH 02/36] Updated test_ImageProjection to work with ImageProjection changes. --- .../test/test_ImageProjection.py | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py index aca00941..14c70e28 100644 --- a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py +++ b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py @@ -1,12 +1,13 @@ import os +import unittest import numpy as np import pytest -import unittest import opencsp.common.lib.deflectometry.ImageProjection as ip import opencsp.common.lib.tool.exception_tools as et import opencsp.common.lib.tool.file_tools as ft +from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir @pytest.mark.no_xvfb @@ -15,6 +16,9 @@ def setUp(self) -> None: path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "ImageProjection") self.out_dir = os.path.join(path, "data", "output", "ImageProjection") + self.file_image_projection_input = os.path.join( + opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' + ) ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) @@ -26,7 +30,7 @@ def test_set_image_projection(self): self.assertIsNone(ip.ImageProjection.instance()) # Create a mock ImageProjection object - image_projection = _ImageProjection.in_new_window(_ImageProjection.display_dict) + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) # Test that the instance was set self.assertEqual(image_projection, ip.ImageProjection.instance()) @@ -44,74 +48,54 @@ def close_count_inc(image_projection): close_count += 1 # Create a mock ImageProjection object with single on_close callback - image_projection = _ImageProjection.in_new_window(_ImageProjection.display_dict) + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) image_projection.on_close.append(close_count_inc) image_projection.close() self.assertEqual(close_count, 1) # Create a mock ImageProjection object with multiple on_close callback - image_projection = _ImageProjection.in_new_window(_ImageProjection.display_dict) + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) image_projection.on_close.append(close_count_inc) image_projection.on_close.append(close_count_inc) image_projection.close() self.assertEqual(close_count, 3) # Create a mock ImageProjection object without an on_close callback - image_projection = _ImageProjection.in_new_window(_ImageProjection.display_dict) + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) image_projection.close() self.assertEqual(close_count, 3) def test_zeros(self): # Create a mock ImageProjection object - image_projection = _ImageProjection.in_new_window(_ImageProjection.display_dict) + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection_data = ip.ImageProjectionData.load_from_hdf(self.file_image_projection_input) # Get the zeros array and verify its shape and values zeros = image_projection.get_black_array_active_area() - self.assertEqual((480, 640, 3), zeros.shape) + self.assertEqual( + (image_projection_data.active_area_size_y, image_projection_data.active_area_size_x, 3), zeros.shape + ) ones = zeros + 1 - self.assertEqual(np.sum(ones), 640 * 480 * 3) + self.assertEqual( + np.sum(ones), image_projection_data.active_area_size_y * image_projection_data.active_area_size_x * 3 + ) def test_to_from_hdf(self): - h5file = os.path.join(self.out_dir, "test_to_from_hdf.h5") + # Load from HDF + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) - # Create a mock ImageProjection object - image_projection = _ImageProjection.in_new_window(_ImageProjection.display_dict) + # Create output file + file_h5_output = os.path.join(self.out_dir, "test_to_from_hdf.h5") # Save to HDF - ft.delete_file(h5file, error_on_not_exists=False) - self.assertFalse(ft.file_exists(h5file)) - image_projection.save_to_hdf(h5file) - self.assertTrue(ft.file_exists(h5file)) + ft.delete_file(file_h5_output, error_on_not_exists=False) + self.assertFalse(ft.file_exists(file_h5_output)) + image_projection.save_to_hdf(file_h5_output) + self.assertTrue(ft.file_exists(file_h5_output)) # Close, so that we don't have multiple windows open at a time image_projection.close() - # Load from HDF - image_projection2 = _ImageProjection.load_from_hdf_and_display(h5file) - self.assertEqual(image_projection2.display_data, _ImageProjection.display_dict) - - -class _ImageProjection(ip.ImageProjection): - display_dict = { - 'name': 'smoll_for_unit_tests', - 'win_size_x': 640, - 'win_size_y': 480, - 'win_position_x': 0, - 'win_position_y': 0, - 'size_x': 640, - 'size_y': 480, - 'position_x': 0, - 'position_y': 0, - 'projector_max_int': 255, - 'projector_data_type': "uint8", - 'shift_red_x': 0, - 'shift_red_y': 0, - 'shift_blue_x': 0, - 'shift_blue_y': 0, - 'image_delay': 1, - 'ui_position_x': 0, - } - if __name__ == '__main__': unittest.main() From 277bb1729ccac4187496152d6da82f7337511db9 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 09:40:20 -0600 Subject: [PATCH 03/36] Updated image_projection_test.h5 to new format. --- .../sofast_common/image_projection_test.h5 | Bin 14368 -> 15072 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/opencsp/test/data/sofast_common/image_projection_test.h5 b/opencsp/test/data/sofast_common/image_projection_test.h5 index c2f66fdd74e46e7cf65fa0ebb1de7e2d05642ead..9f3665707d6c077cea72a3c7dda9b2d6075999d8 100644 GIT binary patch delta 593 zcmZ2b@St>p2Gawpjan+qj4YG=nH7y97#YAo!vI2aKq!V8Mi9mYD1U=Cl)<=h<1t1l z4_C(kkiY>Zi0}retb)bFjd7bFuqH5Z2|!JrpgD1)9Fqp)}VB*>HnjFY2u)Fx+eXiPj1z^E{Bqd3rrpvkukk1(=Lo^SLKD7yK9u>;G*100hL z3^?jDa}(23<5N;|5-a0#iy0USpl&yS(lEXPlwY2i7hjNHoLQ2YpBG=rz)+l7l^S2c zz>u2=lmJSm}>l2ep zGRsoq6N^$4QOv?B4s-*uILtjD>oCNDYC!<2TY+*=abvJ2kPSlC02YTD1oayO2jj$z F{{ZS~o<;xw delta 350 zcmaD*x}ac!29tutMlBU)M&`->%!-Ny3=lBG2tr3d`5Uw$426vwuQN({xH<-a_z8>< zQ2{8eVK8xH+~x4PyS(`!Ic0}!!SW};zl{90*1+19BP{%a9mSjWSzWS zTa(dXa-@MemjhJQ0U3}g0fxz|4AdqU7;-Qzkeax0+T;n6N}Hv0`Isjj;4sTe%uNNU zF3-%1FV3t=jjw>{h4U*B{DS=A%#zIfy!eXAaTZcaVC6t*gfK)YoL@P4mW3I2W^Q77 zYJ5s+PGaTc+ZLgmQ13AWOx)-WbV|_VTZTs%IVR6H`p5+i2cRJhav*m{FihSB@}02* F3joccT$um> From 85614448a7aafbceea6d8b8f981835b58d5169be Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 09:55:08 -0600 Subject: [PATCH 04/36] Added button to SofastGUI --- opencsp/app/sofast/SofastGUI.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/opencsp/app/sofast/SofastGUI.py b/opencsp/app/sofast/SofastGUI.py index f6a6a39c..63c97420 100644 --- a/opencsp/app/sofast/SofastGUI.py +++ b/opencsp/app/sofast/SofastGUI.py @@ -46,7 +46,7 @@ def __init__(self) -> 'SofastGUI': self.root.title('SOFAST') # Set size of GUI - self.root.geometry('600x640+200+100') + self.root.geometry('600x680+200+100') # Add all buttons/widgets to window self._create_layout() @@ -136,6 +136,7 @@ def _plot_image(self, ax: plt.Axes, image: np.ndarray, title: str = '', xlabel: @functools.cached_property def cam_options(self): + """Returns all camera (ImageAcquisition) options that are compatible with Sofast""" all_cam_options = ImageAcquisitionAbstract.cam_options() ret: dict[str, type[ImageAcquisitionAbstract]] = {} @@ -187,10 +188,17 @@ def _create_layout(self) -> None: # =============== First Column - Projection controls =============== r = 0 self.btn_show_cal_image = tkinter.Button( - label_frame_projector, text='Show Calibration Image', command=self.show_calibration_image + label_frame_projector, text='Show Cal Fiducial Image', command=self.show_calibration_fiducial_image ) self.btn_show_cal_image.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') - tkt.TkToolTip(self.btn_show_cal_image, 'Shows calibration image on projection window.') + tkt.TkToolTip(self.btn_show_cal_image, 'Shows calibration fiducial image on projection window.') + r += 1 + + self.btn_show_cal_image = tkinter.Button( + label_frame_projector, text='Show Cal Marker Image', command=self.show_calibration_marker_image + ) + self.btn_show_cal_image.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') + tkt.TkToolTip(self.btn_show_cal_image, 'Shows calibration marker image on projection window.') r += 1 self.btn_show_axes = tkinter.Button(label_frame_projector, text='Show Screen Axes', command=self.show_axes) @@ -565,13 +573,21 @@ def load_image_projection(self) -> None: lt.info(f'ImageProjection loaded:\n {file}') - def show_calibration_image(self) -> None: - """Shows calibration image""" + def show_calibration_fiducial_image(self) -> None: + """Shows calibration image with fiducials""" + # Check that we have a projector + if not self._check_projector_loaded('show_calibration_fiducial_image'): + return + + ImageProjection.instance().show_calibration_fiducial_image() + + def show_calibration_marker_image(self) -> None: + """Shows calibration image with Aruco markers""" # Check that we have a projector - if not self._check_projector_loaded('show_calibration_image'): + if not self._check_projector_loaded('show_calibration_marker_image'): return - ImageProjection.instance().show_calibration_image() + ImageProjection.instance().show_calibration_marker_image() def show_crosshairs(self) -> None: """Shows crosshairs""" @@ -800,6 +816,7 @@ def _projector_closed(self, image_projection: ImageProjection): self._camera_closed(None) def close_image_acquisition(self) -> None: + """Closes image acquisition""" # Close the camera with et.ignored(Exception): ImageAcquisitionAbstract.instance().close() From 6e436352eea7defaebd809ce62e0609d7c640a25 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 09:55:29 -0600 Subject: [PATCH 05/36] Changed name of calibration image in ImageProjectionSetupGUI --- .../lib/deflectometry/ImageProjectionSetupGUI.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index cd4e2049..6b43990d 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -143,7 +143,9 @@ def create_layout(self): self.btn_axes.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') # Show calibration image button - self.btn_calib = tkinter.Button(self.root, text='Show calibration image', command=self.show_calibration_image) + self.btn_calib = tkinter.Button( + self.root, text='Show calibration image', command=self.show_calibration_fiducial_image + ) r += 1 self.btn_calib.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') @@ -234,13 +236,21 @@ def show_axes(self): # Show X/Y axes self.projector.show_axes() - def show_calibration_image(self): + def show_calibration_fiducial_image(self): """Shows calibration image""" # Update active area of projector self.update_windows() # Show cal image - self.projector.show_calibration_image() + self.projector.show_calibration_fiducial_image() + + def show_calibration_marker_image(self): + """Shows calibration Aruco marker image""" + # Update active area of projection + self.update_windows() + + # Show cal image + self.projector.show_calibration_marker_image() def close_projector(self): """ From e4d9816304ed2a256a1630a143d168d6dfe0f92f Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 09:55:55 -0600 Subject: [PATCH 06/36] Added show_calibration_marker_image placeholder function im ImageProjection --- opencsp/common/lib/deflectometry/ImageProjection.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index 7ce8a772..c0f7d1cc 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -337,8 +337,8 @@ def show_axes(self) -> None: # Display image self.display_image_in_active_area(array) - def show_calibration_image(self): - """Shows a calibration image with N fiducials. Fiducials are black dots + def show_calibration_fiducial_image(self): + """Shows a calibration image with N fiducials. Fiducials are green dots on a white background. Fiducial locations measured from center of dots. """ # Create base image to show @@ -371,6 +371,12 @@ def show_calibration_image(self): # Display with black border self.display_image_in_active_area(array) + def show_calibration_marker_image(self): + """Shows a calibration image with N Aruco markers. Markers are black + on a white background. + """ + print('Test: show calibration marker image') + def get_black_array_active_area(self) -> np.ndarray: """ Creates a black image to fill the active area of self._display_data.active_area_size_y by self._display_data.active_area_size_x pixels. From 0e3e47fc10da0e03f457afdda9071c37088f2ecc Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 10:10:36 -0600 Subject: [PATCH 07/36] Added a run test to testImageProjection --- .../lib/deflectometry/test/test_ImageProjection.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py index 14c70e28..7a6e922f 100644 --- a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py +++ b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py @@ -27,6 +27,7 @@ def tearDown(self): ip.ImageProjection.instance().close() def test_set_image_projection(self): + """Test setting/unsetting ImageProjection instances""" self.assertIsNone(ip.ImageProjection.instance()) # Create a mock ImageProjection object @@ -40,6 +41,7 @@ def test_set_image_projection(self): self.assertIsNone(ip.ImageProjection.instance()) def test_on_close(self): + """Tests closing callback""" global close_count close_count = 0 @@ -66,6 +68,7 @@ def close_count_inc(image_projection): self.assertEqual(close_count, 3) def test_zeros(self): + """Tests the shape of the 'zeros' array to fill active area""" # Create a mock ImageProjection object image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) image_projection_data = ip.ImageProjectionData.load_from_hdf(self.file_image_projection_input) @@ -81,6 +84,7 @@ def test_zeros(self): ) def test_to_from_hdf(self): + """Loads and saves from/to HDF5 files""" # Load from HDF image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) @@ -96,6 +100,13 @@ def test_to_from_hdf(self): # Close, so that we don't have multiple windows open at a time image_projection.close() + def test_run_wait_close(self): + """Loads, waits, closes ImageProjection window""" + # Load from HDF + image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection.root.after(500, image_projection.close) + image_projection.run() + if __name__ == '__main__': unittest.main() From 6cc07660d9de3bdf72cb8d699ad90af36700e467 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 10:51:52 -0600 Subject: [PATCH 08/36] Updated ImageProjection references in SystemSofastFringe. --- opencsp/app/sofast/lib/SystemSofastFringe.py | 31 +++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/opencsp/app/sofast/lib/SystemSofastFringe.py b/opencsp/app/sofast/lib/SystemSofastFringe.py index d4e8197b..028b90bd 100644 --- a/opencsp/app/sofast/lib/SystemSofastFringe.py +++ b/opencsp/app/sofast/lib/SystemSofastFringe.py @@ -206,7 +206,9 @@ def _create_mask_images_to_display(self) -> None: array = np.array(image_projection.get_black_array_active_area()) self._mask_images_to_display.append(array) # Create white image - array = np.array(image_projection.get_black_array_active_area()) + image_projection.max_int + array = ( + np.array(image_projection.get_black_array_active_area()) + image_projection.display_data.projector_max_int + ) self._mask_images_to_display.append(array) def _measure_sequence_display( @@ -233,7 +235,7 @@ def _measure_sequence_display( # Wait, then capture image self.root.after( - image_projection.display_data['image_delay'], + image_projection.display_data.image_delay_ms, lambda: self._measure_sequence_capture(im_disp_list, im_cap_list, run_next), ) @@ -306,13 +308,13 @@ def create_fringe_images_from_image_calibration(self): min_display_value = self._calibration.calculate_min_display_camera_values()[0] # Get fringe range - fringe_range = (min_display_value, image_projection.display_data['projector_max_int']) + fringe_range = (min_display_value, image_projection.display_data.projector_max_int) # Get fringe base images fringe_images_base = self.fringes.get_frames( - image_projection.size_x, - image_projection.size_y, - image_projection.display_data['projector_data_type'], + image_projection.display_data.active_area_size_x, + image_projection.display_data.active_area_size_y, + image_projection.display_data.projector_data_type, fringe_range, ) @@ -541,11 +543,14 @@ def run_display_camera_response_calibration(self, res: int = 10, run_next: Calla # Generate grayscale values self._calibration_display_values = np.arange( - 0, image_projection.max_int + 1, res, dtype=image_projection.display_data['projector_data_type'] + 0, + image_projection.display_data.projector_max_int + 1, + res, + dtype=image_projection.display_data.projector_data_type, ) - if self._calibration_display_values[-1] != image_projection.max_int: + if self._calibration_display_values[-1] != image_projection.display_data.projector_max_int: self._calibration_display_values = np.concatenate( - (self._calibration_display_values, [image_projection.max_int]) + (self._calibration_display_values, [image_projection.display_data.projector_max_int]) ) # Generate grayscale images @@ -554,8 +559,12 @@ def run_display_camera_response_calibration(self, res: int = 10, run_next: Calla # Create image array = ( np.zeros( - (image_projection.size_y, image_projection.size_x, 3), - dtype=image_projection.display_data['projector_data_type'], + ( + image_projection.display_data.active_area_size_y, + image_projection.display_data.active_area_size_x, + 3, + ), + dtype=image_projection.display_data.projector_data_type, ) + dn ) From 0facd47ac12b11a787a4496745e6357ebe5ef16c Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 10:52:26 -0600 Subject: [PATCH 09/36] Updated ImageProjection loading func in SofastGUI --- opencsp/app/sofast/SofastGUI.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/opencsp/app/sofast/SofastGUI.py b/opencsp/app/sofast/SofastGUI.py index 63c97420..4910410c 100644 --- a/opencsp/app/sofast/SofastGUI.py +++ b/opencsp/app/sofast/SofastGUI.py @@ -20,7 +20,7 @@ from opencsp.common.lib.camera.ImageAcquisitionAbstract import ImageAcquisitionAbstract from opencsp.common.lib.camera.image_processing import highlight_saturation from opencsp.common.lib.camera.LiveView import LiveView -from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection, ImageProjectionData from opencsp.common.lib.geometry.Vxyz import Vxyz import opencsp.common.lib.opencsp_path.opencsp_root_path as orp import opencsp.common.lib.tool.exception_tools as et @@ -317,7 +317,10 @@ def _create_layout(self) -> None: # Camera type dropdown 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())) + cam_options = list(self.cam_options.keys()) + drop_camera_type = tkinter.OptionMenu( + label_frame_settings, self.var_cam_select, cam_options[0], *cam_options[1:] + ) 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') @@ -555,13 +558,13 @@ def load_image_projection(self) -> None: # Load file and display if file != '': - # Load data - image_projection_data = ImageProjection.load_from_hdf(file) # Close the previous projector instance with et.ignored(Exception): ImageProjection.instance().close() # Create new window projector_root = tkt.window(self.root, TopLevel=True) + # Load data + image_projection_data = ImageProjectionData.load_from_hdf(file) # Show window ImageProjection(projector_root, image_projection_data) # Build system instance From 0b65a83f733e8e0d087ea9978bdc272971742474 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 10:52:48 -0600 Subject: [PATCH 10/36] Made display_data not private in ImageProjection --- .../lib/deflectometry/ImageProjection.py | 111 +++++++++--------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index c0f7d1cc..7f02fb50 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -12,7 +12,6 @@ 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 -from opencsp.common.lib.tool.hdf5_tools import HDF5_IO_Abstract class CalParams: @@ -64,7 +63,7 @@ def __init__(self, size_x: int, size_y: int) -> 'CalParams': @dataclass -class ImageProjectionData(HDF5_IO_Abstract): +class ImageProjectionData(hdf5_tools.HDF5_IO_Abstract): """Dataclass containting ImageProjection parameters. All position/size units are screen pixels""" name: str @@ -125,12 +124,12 @@ def load_from_hdf(cls, file: str, prefix: str = ''): return cls(**kwargs) -class ImageProjection(hdf5_tools.HDF5_SaveAbstract): +class ImageProjection(hdf5_tools.HDF5_IO_Abstract): """Controls projecting an image on a computer display (projector, monitor, etc.)""" _instance: 'ImageProjection' = None - def __init__(self, root: tkinter.Tk, display_data: ImageProjectionData): + def __init__(self, root: tkinter.Tk, display_data: ImageProjectionData) -> 'ImageProjection': """Instantiates class Parameters @@ -165,7 +164,7 @@ def __init__(self, root: tkinter.Tk, display_data: ImageProjectionData): self._x_active_mid: int self._y_active_mid: int - self._display_data = display_data + self.display_data = display_data self.update_window() # Make window frameless @@ -180,8 +179,8 @@ def __init__(self, root: tkinter.Tk, display_data: ImageProjectionData): # Create black image image = self._format_image( np.zeros( - (self._display_data.main_window_size_y, self._display_data.main_window_size_x, 3), - dtype=self._display_data.projector_data_type, + (self.display_data.main_window_size_y, self.display_data.main_window_size_x, 3), + dtype=self.display_data.projector_data_type, ) ) self.canvas_image = self.canvas.create_image(0, 0, image=image, anchor='nw') @@ -229,23 +228,23 @@ def in_new_window(cls, display_data: ImageProjectionData) -> 'ImageProjection': def update_window(self) -> None: """Updates window display data.""" # Calculate active area extents - self._x_active_1 = self._display_data.active_area_position_x - self._x_active_2 = self._display_data.active_area_position_x + self._display_data.active_area_size_x - self._y_active_1 = self._display_data.active_area_position_y - self._y_active_2 = self._display_data.active_area_position_y + self._display_data.active_area_size_y + self._x_active_1 = self.display_data.active_area_position_x + self._x_active_2 = self.display_data.active_area_position_x + self.display_data.active_area_size_x + self._y_active_1 = self.display_data.active_area_position_y + self._y_active_2 = self.display_data.active_area_position_y + self.display_data.active_area_size_y # Calculate center of active area - self._x_active_mid = int(self._display_data.active_area_size_x / 2) - self._y_active_mid = int(self._display_data.active_area_size_y / 2) + self._x_active_mid = int(self.display_data.active_area_size_x / 2) + self._y_active_mid = int(self.display_data.active_area_size_y / 2) # Resize window self.root.geometry( - f'{self._display_data.main_window_size_x:d}x{self._display_data.main_window_size_y:d}' - + f'+{self._display_data.main_window_position_x:d}+{self._display_data.main_window_position_y:d}' + f'{self.display_data.main_window_size_x:d}x{self.display_data.main_window_size_y:d}' + + f'+{self.display_data.main_window_position_x:d}+{self.display_data.main_window_position_y:d}' ) # Resize canvas size - self.canvas.configure(width=self._display_data.main_window_size_x, height=self._display_data.main_window_size_y) + self.canvas.configure(width=self.display_data.main_window_size_x, height=self.display_data.main_window_size_y) def show_crosshairs(self) -> None: """ @@ -255,10 +254,10 @@ def show_crosshairs(self) -> None: # Add white active region array = ( np.ones( - (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), - dtype=self._display_data.projector_data_type, + (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), + dtype=self.display_data.projector_data_type, ) - * self._display_data.projector_max_int + * self.display_data.projector_max_int ) # Add crosshairs vertical @@ -266,7 +265,7 @@ def show_crosshairs(self) -> None: array[self._y_active_mid, :, :] = 0 # Add crosshairs diagonal - width = np.min([self._display_data.active_area_size_x, self._display_data.active_area_size_y]) + width = np.min([self.display_data.active_area_size_x, self.display_data.active_area_size_y]) xd1 = int(self._x_active_mid - width / 4) xd2 = int(self._x_active_mid + width / 4) yd1 = int(self._y_active_mid - width / 4) @@ -287,26 +286,26 @@ def show_axes(self) -> None: # Add white active region array = ( np.ones( - (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), - dtype=self._display_data.projector_data_type, + (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), + dtype=self.display_data.projector_data_type, ) - * self._display_data.projector_max_int + * self.display_data.projector_max_int ) # Add arrows - width = int(np.min([self._display_data.active_area_size_x, self._display_data.active_area_size_y]) / 4) + width = int(np.min([self.display_data.active_area_size_x, self.display_data.active_area_size_y]) / 4) thickness = 5 # Add green X axis arrow start_point = (self._x_active_mid, self._y_active_mid) end_point = (self._x_active_mid - width, self._y_active_mid) - color = (int(self._display_data.projector_max_int), 0, 0) # RGB + color = (int(self.display_data.projector_max_int), 0, 0) # RGB array = cv.arrowedLine(array, start_point, end_point, color, thickness) # Add red Y axis arrow start_point = (self._x_active_mid, self._y_active_mid) end_point = (self._x_active_mid, self._y_active_mid + width) - color = (0, int(self._display_data.projector_max_int), 0) # RGB + color = (0, int(self.display_data.projector_max_int), 0) # RGB array = cv.arrowedLine(array, start_point, end_point, color, thickness) # Add X text @@ -317,7 +316,7 @@ def show_axes(self) -> None: (self._x_active_mid - width - 20, self._y_active_mid + 20), font, 6, - (int(self._display_data.projector_max_int), 0, 0), + (int(self.display_data.projector_max_int), 0, 0), 2, bottomLeftOrigin=True, ) @@ -330,7 +329,7 @@ def show_axes(self) -> None: (self._x_active_mid + 20, self._y_active_mid + width + 20), font, 6, - (0, int(self._display_data.projector_max_int), 0), + (0, int(self.display_data.projector_max_int), 0), 2, ) @@ -343,20 +342,20 @@ def show_calibration_fiducial_image(self): """ # Create base image to show array = np.ones( - (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), - dtype=self._display_data.projector_data_type, + (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), + dtype=self.display_data.projector_data_type, ) # Get calibration pattern parameters - pattern_params = CalParams(self._display_data.active_area_size_x, self._display_data.active_area_size_y) + pattern_params = CalParams(self.display_data.active_area_size_x, self.display_data.active_area_size_y) # Add fiducials for x_loc, y_loc, idx in zip(pattern_params.x_pixel, pattern_params.y_pixel, pattern_params.index): # Place fiducial - array[y_loc, x_loc, 1] = self._display_data.projector_max_int + array[y_loc, x_loc, 1] = self.display_data.projector_max_int # Place label (offset so label is in view) - x_pt_to_center = float(self._display_data.active_area_size_x) / 2 - x_loc - y_pt_to_center = float(self._display_data.active_area_size_y) / 2 - y_loc + x_pt_to_center = float(self.display_data.active_area_size_x) / 2 - x_loc + y_pt_to_center = float(self.display_data.active_area_size_y) / 2 - y_loc if x_pt_to_center >= 0: dx = 15 else: @@ -379,17 +378,17 @@ def show_calibration_marker_image(self): def get_black_array_active_area(self) -> np.ndarray: """ - Creates a black image to fill the active area of self._display_data.active_area_size_y by self._display_data.active_area_size_x pixels. + Creates a black image to fill the active area of self.display_data.active_area_size_y by self.display_data.active_area_size_x pixels. Returns: -------- image : np.ndarray - A 2D image with shape (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), filled with zeros + A 2D image with shape (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), filled with zeros """ # Create black image black_image = np.zeros( - (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3), - dtype=self._display_data.projector_data_type, + (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), + dtype=self.display_data.projector_data_type, ) return black_image @@ -401,7 +400,7 @@ def display_image(self, array: np.ndarray) -> None: Parameters ---------- array : ndarray - NxMx3 image array. Data must be int ranging from 0 to self._display_data.projector_max_int. + NxMx3 image array. Data must be int ranging from 0 to self.display_data.projector_max_int. Array XY shape must match window size in pixels. """ @@ -411,13 +410,13 @@ def display_image(self, array: np.ndarray) -> None: # Check array is correct xy shape if ( - array.shape[0] != self._display_data.main_window_size_y - or array.shape[1] != self._display_data.main_window_size_x + array.shape[0] != self.display_data.main_window_size_y + or array.shape[1] != self.display_data.main_window_size_x ): raise ValueError( f'Input image incorrect size. Input image size is {array.shape[:2]:d},' - + f' but frame size is {self._display_data.main_window_size_y:d}x' - + f'{self._display_data.main_window_size_x:d}.' + + f' but frame size is {self.display_data.main_window_size_y:d}x' + + f'{self.display_data.main_window_size_x:d}.' ) # Format image @@ -429,13 +428,13 @@ def display_image(self, array: np.ndarray) -> None: def display_image_in_active_area(self, array: np.ndarray) -> None: """Formats and displays input numpy array in active area only. Input - array must have size (self._display_data.active_area_size_y, self._display_data.active_area_size_x, 3) and is displayed + array must have size (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3) and is displayed with a black border to fill entire window area. Parameters ---------- array : ndarray - NxMx3 image array. Data must be int ranging from 0 to self._display_data.projector_max_int. + NxMx3 image array. Data must be int ranging from 0 to self.display_data.projector_max_int. Array XY shape must match active window area size in pixels. """ @@ -445,19 +444,19 @@ def display_image_in_active_area(self, array: np.ndarray) -> None: # Check array is correct xy shape if ( - array.shape[0] != self._display_data.active_area_size_y - or array.shape[1] != self._display_data.active_area_size_x + array.shape[0] != self.display_data.active_area_size_y + or array.shape[1] != self.display_data.active_area_size_x ): raise ValueError( f'Input image incorrect size. Input image size is {array.shape[:2]:d},' - + f' but frame size is {self._display_data.active_area_size_y:d}x' - + f'{self._display_data.active_area_size_x:d}.' + + f' but frame size is {self.display_data.active_area_size_y:d}x' + + f'{self.display_data.active_area_size_x:d}.' ) # Create black image and place array in correct position array_out = np.zeros( - (self._display_data.main_window_size_y, self._display_data.main_window_size_x, 3), - dtype=self._display_data.projector_data_type, + (self.display_data.main_window_size_y, self.display_data.main_window_size_x, 3), + dtype=self.display_data.projector_data_type, ) array_out[self._y_active_1 : self._y_active_2, self._x_active_1 : self._x_active_2, :] = array @@ -480,12 +479,12 @@ def _format_image(self, array: np.ndarray) -> ImageTk.PhotoImage: """ # Shift red channel - array[..., 0] = np.roll(array[..., 0], self._display_data.shift_red_x, 1) - array[..., 0] = np.roll(array[..., 0], self._display_data.shift_red_y, 0) + array[..., 0] = np.roll(array[..., 0], self.display_data.shift_red_x, 1) + array[..., 0] = np.roll(array[..., 0], self.display_data.shift_red_y, 0) # Shift blue channel - array[..., 2] = np.roll(array[..., 2], self._display_data.shift_blue_x, 1) - array[..., 2] = np.roll(array[..., 2], self._display_data.shift_blue_y, 0) + array[..., 2] = np.roll(array[..., 2], self.display_data.shift_blue_x, 1) + array[..., 2] = np.roll(array[..., 2], self.display_data.shift_blue_y, 0) # Format array into tkinter format image = Image.fromarray(array, 'RGB') @@ -525,7 +524,7 @@ def save_to_hdf(self, file: str, prefix: str = '') -> None: Prefix to append to folder path within HDF file (folders must be separated by "/"). Default is empty string ''. """ - self._display_data.save_to_hdf(file, prefix) + self.display_data.save_to_hdf(file, prefix) def close(self): """Closes all windows""" From 342810d7bb61b7abfd051b95c6edda825ea1473d Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 10:54:09 -0600 Subject: [PATCH 11/36] Added docstring to SystemSofastFringe --- opencsp/app/sofast/lib/SystemSofastFringe.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/opencsp/app/sofast/lib/SystemSofastFringe.py b/opencsp/app/sofast/lib/SystemSofastFringe.py index 028b90bd..ef553474 100644 --- a/opencsp/app/sofast/lib/SystemSofastFringe.py +++ b/opencsp/app/sofast/lib/SystemSofastFringe.py @@ -1,6 +1,3 @@ -"""Class for controlling displaying Sofast patterns and capturing images -""" - import copy import datetime as dt from typing import Callable @@ -21,6 +18,8 @@ class SystemSofastFringe: + """Class for controlling/displaying Sofast patterns and capturing images""" + def __init__( self, image_acquisition: ImageAcquisitionAbstract | list[ImageAcquisitionAbstract] = None ) -> 'SystemSofastFringe': @@ -40,7 +39,6 @@ def __init__( Either the image_acquisition or the image_projection has not been loaded. TypeError: The image_acquisition is not the correct type. - """ # Import here to avoid circular dependencies import opencsp.app.sofast.lib.sofast_common_functions as scf From d343f0ee31865b268746cf2281a3600ff4ed35f3 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 10:57:39 -0600 Subject: [PATCH 12/36] Updated SystemSofastFixed to use new ImageProjection data structure. --- opencsp/app/sofast/lib/SystemSofastFixed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opencsp/app/sofast/lib/SystemSofastFixed.py b/opencsp/app/sofast/lib/SystemSofastFixed.py index 443d3076..977c8325 100644 --- a/opencsp/app/sofast/lib/SystemSofastFixed.py +++ b/opencsp/app/sofast/lib/SystemSofastFixed.py @@ -58,7 +58,7 @@ def __init__(self, image_acquisition: ImageAcquisitionAbstract) -> 'SystemSofast """The value that corresponds to pure white""" self.dot_shape: Literal['circle', 'square'] = 'circle' """The shape of the fixed pattern dots""" - self.image_delay = image_projection.display_data['image_delay'] # ms + self.image_delay = image_projection.display_data.image_delay_ms # ms """The delay after displaying the image to capturing an image, ms""" def set_pattern_parameters(self, width_pattern: int, spacing_pattern: int): @@ -76,8 +76,8 @@ def set_pattern_parameters(self, width_pattern: int, spacing_pattern: int): lt.debug(f'SystemSofastFixed fixed pattern dot width set to {width_pattern:d} pixels') lt.debug(f'SystemSofastFixed fixed pattern dot spacing set to {spacing_pattern:d} pixels') image_projection = ImageProjection.instance() - size_x = image_projection.size_x - size_y = image_projection.size_y + size_x = image_projection.display_data.active_area_size_x + size_y = image_projection.display_data.active_area_size_y self.pattern_sofast_fixed = PatternSofastFixed(size_x, size_y, width_pattern, spacing_pattern) self.pattern_image = self.pattern_sofast_fixed.get_image(self.dtype, self.max_int, self.dot_shape) From 8e7dd67213deecb42a64fbfdf13b14b3f8ce8924 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:16:11 -0600 Subject: [PATCH 13/36] ImageProjectionSetupGUI is now compatible with new imageProjection class. --- .../deflectometry/ImageProjectionSetupGUI.py | 120 ++++++------------ 1 file changed, 38 insertions(+), 82 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index 6b43990d..f52b0155 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -1,54 +1,33 @@ -"""Graphical User Interface (GUI) used for setting up the physical layout -of a computer display (ImageProjection class). To run GUI, use the following: -```python ImageProjectionGUI.py``` - -""" - import tkinter from tkinter import messagebox from tkinter.filedialog import askopenfilename from tkinter.filedialog import asksaveasfilename -from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection, ImageProjectionData import opencsp.common.lib.tool.tk_tools as tkt +from opencsp.common.lib.tool.hdf5_tools import HDF5_IO_Abstract -class ImageProjectionGUI: - """ - GUI for setting the physical layout parameters: - 'name' - 'win_size_x' - 'win_size_y' - 'win_position_x' - 'win_position_y' - 'size_x' - 'size_y' - 'position_x' - 'position_y' - 'projector_data_type' - 'projector_max_int' - 'shift_red_x' - 'shift_red_y' - 'shift_blue_x' - 'shift_blue_y' +class ImageProjectionGUI(HDF5_IO_Abstract): + """Graphical User Interface (GUI) used for setting up the physical layout + of a computer display (ImageProjection class). To run GUI, run `python ImageProjectionGUI.py` """ def __init__(self): - """Instantiates a new instance of the ImageProjection setup GUI in a new window""" # Define data names self.data_names = [ 'name', - 'win_size_x', - 'win_size_y', - 'win_position_x', - 'win_position_y', - 'size_x', - 'size_y', - 'position_x', - 'position_y', + 'main_window_size_x', + 'main_window_size_y', + 'main_window_position_x', + 'main_window_position_y', + 'active_area_size_x', + 'active_area_size_y', + 'active_area_position_x', + 'active_area_position_y', 'projector_data_type', 'projector_max_int', - 'image_delay', + 'image_delay_ms', 'shift_red_x', 'shift_red_y', 'shift_blue_x', @@ -78,7 +57,7 @@ def __init__(self): # Declare variables self.projector: ImageProjection - self.display_data: dict + self.display_data: ImageProjectionData # Create tkinter object self.root = tkt.window() @@ -104,7 +83,7 @@ def __init__(self): def update_window_size(self): """Updates the window size to current set value""" # Set size and position of window - self.root.geometry(f'500x650+{self.display_data["ui_position_x"]:d}+100') + self.root.geometry(f'500x650+{self.display_data.ui_position_x:d}+100') def create_layout(self): """Creates GUI widgets""" @@ -220,7 +199,8 @@ def update_windows(self): self.get_user_data() # Update size of window - self.projector.upate_window(self.display_data) + self.projector.display_data = self.display_data + self.projector.update_window() # Show crosshairs self.projector.show_crosshairs() @@ -289,8 +269,7 @@ def load_from(self): def get_user_data(self): """ - Reads user input data and saves in dictionary. - + Reads user input data and saves in ImageProjectionData class (`self.display_data`). """ # Check inputs format if not self.check_inputs(): @@ -300,7 +279,7 @@ def get_user_data(self): data = {} for dtype, name, entry in zip(self.data_types, self.data_names, self.data_cells): data.update({name: dtype(entry.get())}) - self.display_data = data + self.display_data = ImageProjectionData(**data) def check_inputs(self) -> bool: """ @@ -322,71 +301,48 @@ def check_inputs(self) -> bool: return True def set_user_data(self): - """ - Sets the loaded user data in the user input boxes - - """ + """Sets the loaded user data in the user input boxes""" for name, entry in zip(self.data_names, self.data_cells): entry.delete(0, tkinter.END) - entry.insert(0, self.display_data[name]) + entry.insert(0, self.display_data.__dict__[name]) def load_defaults(self): - """ - Sets default values. - - """ - self.display_data = { + """Sets default values.""" + kwargs = { 'name': 'Default Image Projection', - 'win_size_x': 800, - 'win_size_y': 500, - 'win_position_x': 0, - 'win_position_y': 0, - 'size_x': 700, - 'size_y': 400, - 'position_x': 50, - 'position_y': 50, + 'main_window_size_x': 800, + 'main_window_size_y': 500, + 'main_window_position_x': 0, + 'main_window_position_y': 0, + 'active_area_size_x': 700, + 'active_area_size_y': 400, + 'active_area_position_x': 50, + 'active_area_position_y': 50, 'projector_data_type': 'uint8', 'projector_max_int': 255, - 'image_delay': 400, + 'image_delay_ms': 400, 'shift_red_x': 0, 'shift_red_y': 0, 'shift_blue_x': 0, 'shift_blue_y': 0, 'ui_position_x': 100, } + self.display_data = ImageProjectionData(**kwargs) self.set_user_data() - def load_from_hdf(self, file: str): - """ - Loads saved user data from HDF file - - Parameters - ---------- - file : str - File to load. - - """ + def load_from_hdf(self, file: str, prefix: str = ''): # Load data - self.display_data = ImageProjection.load_from_hdf(file) + self.display_data = ImageProjectionData.load_from_hdf(file, prefix) # Set data in input fields self.set_user_data() - def save_to_hdf(self, file: str): - """ - Saves user data to HDF file. - - Parameters - ---------- - file : str - File to save to. - - """ + def save_to_hdf(self, file: str, prefix: str = ''): # Load user data self.get_user_data() # Save as HDF file - ImageProjection.save_params_to_hdf(self.display_data, file) + self.display_data.save_to_hdf(file, prefix) def close(self): """ From 1343f8320b58bce5e6105de007e3fdb82d1216b1 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:21:54 -0600 Subject: [PATCH 14/36] Added marker calibration image button to IP setup GUI --- .../deflectometry/ImageProjectionSetupGUI.py | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index f52b0155..fd9ece85 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -75,15 +75,15 @@ def __init__(self): self.load_defaults() # Place window - self.update_window_size() + self.set_window_size() # Run window infinitely self.root.mainloop() - def update_window_size(self): + def set_window_size(self): """Updates the window size to current set value""" # Set size and position of window - self.root.geometry(f'500x650+{self.display_data.ui_position_x:d}+100') + self.root.geometry(f'500x670+{self.display_data.ui_position_x:d}+100') def create_layout(self): """Creates GUI widgets""" @@ -97,12 +97,12 @@ def create_layout(self): self.data_cells.append(e) # Show projector button - self.btn_show_proj = tkinter.Button(self.root, text='Show Display', command=self.show_projector) + self.btn_show_proj = tkinter.Button(self.root, text='Show Display', command=self.show_projection_window) r += 1 self.btn_show_proj.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') # Update projector button - self.btn_update_proj = tkinter.Button(self.root, text='Update All', command=self.update_windows) + self.btn_update_proj = tkinter.Button(self.root, text='Update All', command=self.update_projection_window) r += 1 self.btn_update_proj.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') @@ -112,7 +112,7 @@ def create_layout(self): self.btn_close_proj.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') # Show crosshairs - self.btn_crosshairs = tkinter.Button(self.root, text='Show Crosshairs', command=self.update_windows) + self.btn_crosshairs = tkinter.Button(self.root, text='Show Crosshairs', command=self.update_projection_window) r += 1 self.btn_crosshairs.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') @@ -123,7 +123,14 @@ def create_layout(self): # Show calibration image button self.btn_calib = tkinter.Button( - self.root, text='Show calibration image', command=self.show_calibration_fiducial_image + self.root, text='Show calibration fiducial image', command=self.show_calibration_fiducial_image + ) + r += 1 + self.btn_calib.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') + + # Show calibration image button + self.btn_calib = tkinter.Button( + self.root, text='Show calibration marker image', command=self.show_calibration_marker_image ) r += 1 self.btn_calib.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') @@ -171,17 +178,11 @@ def activate_btns(self, active: bool): self.btn_crosshairs['state'] = active_projector self.btn_calib['state'] = active_projector - def show_projector(self): - """ - Opens the projector window. - - """ + def show_projection_window(self): + """Opens the ImageProjection window.""" # Get user data self.get_user_data() - # Update GUI size - self.update_window_size() - # Create a new Toplevel window projector_root = tkt.window(self.root, TopLevel=True) self.projector = ImageProjection(projector_root, self.display_data) @@ -189,12 +190,8 @@ def show_projector(self): # Activate buttons self.activate_btns(True) - def update_windows(self): - """ - Updates the projector active area data, updates the window size, and - shows crosshairs. - - """ + def update_projection_window(self): + """Updates the projector active area data, updates the window size, and shows crosshairs.""" # Read data from entry cells self.get_user_data() @@ -205,13 +202,10 @@ def update_windows(self): # Show crosshairs self.projector.show_crosshairs() - # Update position of UI window - self.update_window_size() - def show_axes(self): """Shows axis labels.""" # Update active area of projector - self.update_windows() + self.update_projection_window() # Show X/Y axes self.projector.show_axes() @@ -219,7 +213,7 @@ def show_axes(self): def show_calibration_fiducial_image(self): """Shows calibration image""" # Update active area of projector - self.update_windows() + self.update_projection_window() # Show cal image self.projector.show_calibration_fiducial_image() @@ -227,7 +221,7 @@ def show_calibration_fiducial_image(self): def show_calibration_marker_image(self): """Shows calibration Aruco marker image""" # Update active area of projection - self.update_windows() + self.update_projection_window() # Show cal image self.projector.show_calibration_marker_image() From 95c8a56a3d2ff379077fae7705bc871159ef9069 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:29:05 -0600 Subject: [PATCH 15/36] Updated test_SystemSofastFringe to use new ImageProjection class --- .../sofast/test/test_SystemSofastFringe.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/opencsp/app/sofast/test/test_SystemSofastFringe.py b/opencsp/app/sofast/test/test_SystemSofastFringe.py index a8560c27..7a2f1677 100644 --- a/opencsp/app/sofast/test/test_SystemSofastFringe.py +++ b/opencsp/app/sofast/test/test_SystemSofastFringe.py @@ -1,10 +1,10 @@ """Unit test suite to test the System class """ -import numpy as np import os import unittest +import numpy as np import pytest import opencsp.app.sofast.lib.ImageCalibrationGlobal as icg @@ -13,7 +13,6 @@ from opencsp.app.sofast.lib.SystemSofastFringe import SystemSofastFringe from opencsp.app.sofast.test.ImageAcquisition_no_camera import ImageAcquisition from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection -import opencsp.common.lib.deflectometry.test.test_ImageProjection as test_ip from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir import opencsp.common.lib.tool.exception_tools as et import opencsp.common.lib.tool.file_tools as ft @@ -25,6 +24,9 @@ def setUp(self): path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "SystemSofastFringe") self.out_dir = os.path.join(path, "data", "output", "SystemSofastFringe") + self.file_image_projection_input = os.path.join( + opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' + ) ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) @@ -53,7 +55,7 @@ def test_SystemSofastFringe(self): fringes = Fringes(periods_x, periods_y) # Instantiate image projection class - im_proj = ImageProjection.load_from_hdf_and_display(file_im_proj) + im_proj = ImageProjection.load_from_hdf(file_im_proj) # Instantiate image acquisition class im_aq = ImageAcquisition() @@ -88,7 +90,7 @@ def f2(): def test_system_all_prereqs(self): # Create mock ImageProjection and ImageAcquisition objects - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) ia = ImageAcquisition() # Create the system instance @@ -96,7 +98,7 @@ def test_system_all_prereqs(self): def test_system_some_prereqs(self): # With just a projector - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) with self.assertRaises(RuntimeError): sys = SystemSofastFringe() ip.close() @@ -113,7 +115,7 @@ def test_system_no_prereqs(self): sys = SystemSofastFringe() # More interesting case, things are set and then unset - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) ia = ImageAcquisition() ip.close() ia.close() @@ -121,7 +123,7 @@ def test_system_no_prereqs(self): sys = SystemSofastFringe() def test_run_measurement_no_calibration(self): - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) ia = ImageAcquisition() sys = SystemSofastFringe(ia) sys.set_fringes(Fringes.from_num_periods()) @@ -129,7 +131,7 @@ def test_run_measurement_no_calibration(self): sys.run_measurement() def test_run_measurement_without_on_done(self): - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) ia = ImageAcquisition() sys = SystemSofastFringe(ia) sys.set_fringes(self.fringes) @@ -150,7 +152,7 @@ def on_done(): sys.close_all() # create the prerequisites and the system - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) ia = ImageAcquisition() sys = SystemSofastFringe(ia) sys.set_fringes(self.fringes) @@ -170,9 +172,7 @@ def on_done(): def test_close_all_closes_acquisition_projections(self): # build system, including multiple image_acquisitions - d = test_ip._ImageProjection.display_dict - d['win_position_x'] = -640 - ip = test_ip._ImageProjection.in_new_window(d) + ip = ImageProjection.load_from_hdf(self.file_image_projection_input) ia1 = ImageAcquisition() ia2 = ImageAcquisition() sys = SystemSofastFringe([ia1, ia2]) From 4b1cfe115301fc171ded778918f9a0f5ea10c839 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:30:14 -0600 Subject: [PATCH 16/36] Updated test_SystemSofastFixed to use new IP class. --- opencsp/app/sofast/test/test_SystemSofastFixed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencsp/app/sofast/test/test_SystemSofastFixed.py b/opencsp/app/sofast/test/test_SystemSofastFixed.py index 7b30b220..7911bc9b 100644 --- a/opencsp/app/sofast/test/test_SystemSofastFixed.py +++ b/opencsp/app/sofast/test/test_SystemSofastFixed.py @@ -20,7 +20,7 @@ def test_system(self): file_im_proj = join(opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5') # Instantiate image projection class - im_proj = ImageProjection.load_from_hdf_and_display(file_im_proj) + im_proj = ImageProjection.load_from_hdf(file_im_proj) # Instantiate image acquisition class im_aq = ImageAcquisition() From 5c449862b6b90d6fcae8e39430b188d2be43c92c Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:31:06 -0600 Subject: [PATCH 17/36] Updated TestProjectFixedPatternTarget to use new IP class. --- .../sofast/test/test_project_fixed_pattern_target.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/opencsp/app/sofast/test/test_project_fixed_pattern_target.py b/opencsp/app/sofast/test/test_project_fixed_pattern_target.py index f32d0b0f..2b363fdb 100644 --- a/opencsp/app/sofast/test/test_project_fixed_pattern_target.py +++ b/opencsp/app/sofast/test/test_project_fixed_pattern_target.py @@ -18,9 +18,14 @@ def test_project_fixed_pattern_target(self): file_image_projection = os.path.join(opencsp_code_dir(), "test/data/sofast_common/image_projection_test.h5") # Load ImageProjection - im_proj = ImageProjection.load_from_hdf_and_display(file_image_projection) - - fixed_pattern = PatternSofastFixed(im_proj.size_x, im_proj.size_y, width_pattern=3, spacing_pattern=6) + im_proj = ImageProjection.load_from_hdf(file_image_projection) + + fixed_pattern = PatternSofastFixed( + im_proj.display_data.active_area_size_x, + im_proj.display_data.active_area_size_y, + width_pattern=3, + spacing_pattern=6, + ) image = fixed_pattern.get_image('uint8', 255, 'square') # Project image From 45474e2832caac43e409f081d365dbd9deab563f Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:40:05 -0600 Subject: [PATCH 18/36] Removed extraneous calls to IP.load_from_hdf --- .../sofast/test/test_SystemSofastFringe.py | 27 ++++++++++--------- .../test/test_ImageProjection.py | 19 +++++++------ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/opencsp/app/sofast/test/test_SystemSofastFringe.py b/opencsp/app/sofast/test/test_SystemSofastFringe.py index 7a2f1677..eda62d3c 100644 --- a/opencsp/app/sofast/test/test_SystemSofastFringe.py +++ b/opencsp/app/sofast/test/test_SystemSofastFringe.py @@ -12,7 +12,7 @@ from opencsp.app.sofast.lib.Fringes import Fringes from opencsp.app.sofast.lib.SystemSofastFringe import SystemSofastFringe from opencsp.app.sofast.test.ImageAcquisition_no_camera import ImageAcquisition -from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection, ImageProjectionData from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir import opencsp.common.lib.tool.exception_tools as et import opencsp.common.lib.tool.file_tools as ft @@ -24,15 +24,18 @@ def setUp(self): path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "SystemSofastFringe") self.out_dir = os.path.join(path, "data", "output", "SystemSofastFringe") - self.file_image_projection_input = os.path.join( - opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' - ) ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) # Create fringe object self.fringes = Fringes.from_num_periods() + # Load ImageProjectionData + self.file_image_projection_input = os.path.join( + opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' + ) + self.image_projection_data = ImageProjectionData.load_from_hdf(self.file_image_projection_input) + # Create calibration objects projector_values = np.arange(0, 255, (255 - 0) / 9) camera_response = np.arange(5, 50, (50 - 5) / 9) @@ -55,7 +58,7 @@ def test_SystemSofastFringe(self): fringes = Fringes(periods_x, periods_y) # Instantiate image projection class - im_proj = ImageProjection.load_from_hdf(file_im_proj) + im_proj = ImageProjection.in_new_window(self.image_projection_data) # Instantiate image acquisition class im_aq = ImageAcquisition() @@ -90,7 +93,7 @@ def f2(): def test_system_all_prereqs(self): # Create mock ImageProjection and ImageAcquisition objects - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) ia = ImageAcquisition() # Create the system instance @@ -98,7 +101,7 @@ def test_system_all_prereqs(self): def test_system_some_prereqs(self): # With just a projector - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) with self.assertRaises(RuntimeError): sys = SystemSofastFringe() ip.close() @@ -115,7 +118,7 @@ def test_system_no_prereqs(self): sys = SystemSofastFringe() # More interesting case, things are set and then unset - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) ia = ImageAcquisition() ip.close() ia.close() @@ -123,7 +126,7 @@ def test_system_no_prereqs(self): sys = SystemSofastFringe() def test_run_measurement_no_calibration(self): - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) ia = ImageAcquisition() sys = SystemSofastFringe(ia) sys.set_fringes(Fringes.from_num_periods()) @@ -131,7 +134,7 @@ def test_run_measurement_no_calibration(self): sys.run_measurement() def test_run_measurement_without_on_done(self): - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) ia = ImageAcquisition() sys = SystemSofastFringe(ia) sys.set_fringes(self.fringes) @@ -152,7 +155,7 @@ def on_done(): sys.close_all() # create the prerequisites and the system - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) ia = ImageAcquisition() sys = SystemSofastFringe(ia) sys.set_fringes(self.fringes) @@ -172,7 +175,7 @@ def on_done(): def test_close_all_closes_acquisition_projections(self): # build system, including multiple image_acquisitions - ip = ImageProjection.load_from_hdf(self.file_image_projection_input) + ip = ImageProjection.in_new_window(self.image_projection_data) ia1 = ImageAcquisition() ia2 = ImageAcquisition() sys = SystemSofastFringe([ia1, ia2]) diff --git a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py index 7a6e922f..2ee92f6a 100644 --- a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py +++ b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py @@ -16,11 +16,14 @@ def setUp(self) -> None: path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "ImageProjection") self.out_dir = os.path.join(path, "data", "output", "ImageProjection") + ft.create_directories_if_necessary(self.data_dir) + ft.create_directories_if_necessary(self.out_dir) + + # Load display data self.file_image_projection_input = os.path.join( opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' ) - ft.create_directories_if_necessary(self.data_dir) - ft.create_directories_if_necessary(self.out_dir) + self.image_projection_data = ip.ImageProjectionData.load_from_hdf(self.file_image_projection_input) def tearDown(self): with et.ignored(Exception): @@ -31,7 +34,7 @@ def test_set_image_projection(self): self.assertIsNone(ip.ImageProjection.instance()) # Create a mock ImageProjection object - image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) # Test that the instance was set self.assertEqual(image_projection, ip.ImageProjection.instance()) @@ -50,27 +53,27 @@ def close_count_inc(image_projection): close_count += 1 # Create a mock ImageProjection object with single on_close callback - image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) image_projection.on_close.append(close_count_inc) image_projection.close() self.assertEqual(close_count, 1) # Create a mock ImageProjection object with multiple on_close callback - image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) image_projection.on_close.append(close_count_inc) image_projection.on_close.append(close_count_inc) image_projection.close() self.assertEqual(close_count, 3) # Create a mock ImageProjection object without an on_close callback - image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) image_projection.close() self.assertEqual(close_count, 3) def test_zeros(self): """Tests the shape of the 'zeros' array to fill active area""" # Create a mock ImageProjection object - image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) image_projection_data = ip.ImageProjectionData.load_from_hdf(self.file_image_projection_input) # Get the zeros array and verify its shape and values @@ -103,7 +106,7 @@ def test_to_from_hdf(self): def test_run_wait_close(self): """Loads, waits, closes ImageProjection window""" # Load from HDF - image_projection = ip.ImageProjection.load_from_hdf(self.file_image_projection_input) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) image_projection.root.after(500, image_projection.close) image_projection.run() From 4a00af1db567ddb03940f0446e22028041691b95 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:45:05 -0600 Subject: [PATCH 19/36] Updated sofast_common_functions and test_scf to use new ImageProjection class --- .../app/sofast/lib/sofast_common_functions.py | 4 +++- .../test/test_sofast_common_functions.py | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/opencsp/app/sofast/lib/sofast_common_functions.py b/opencsp/app/sofast/lib/sofast_common_functions.py index 8b0e1caf..c5c0c895 100644 --- a/opencsp/app/sofast/lib/sofast_common_functions.py +++ b/opencsp/app/sofast/lib/sofast_common_functions.py @@ -186,7 +186,9 @@ def run_cal(): if on_done is not None: on_done() - white_image = np.array(image_projection.get_black_array_active_area()) + image_projection.max_int + white_image = ( + np.array(image_projection.get_black_array_active_area()) + image_projection.display_data.projector_max_int + ) image_projection.display_image_in_active_area(white_image) image_projection.root.after(100, run_cal) diff --git a/opencsp/app/sofast/test/test_sofast_common_functions.py b/opencsp/app/sofast/test/test_sofast_common_functions.py index 663fe2c3..aa8647a1 100644 --- a/opencsp/app/sofast/test/test_sofast_common_functions.py +++ b/opencsp/app/sofast/test/test_sofast_common_functions.py @@ -1,8 +1,8 @@ import os +import unittest import numpy as np import pytest -import unittest import opencsp.app.sofast.lib.Fringes as fr import opencsp.app.sofast.lib.ImageCalibrationGlobal as icg @@ -11,7 +11,7 @@ import opencsp.app.sofast.lib.SystemSofastFringe as ssf import opencsp.app.sofast.test.ImageAcquisition_no_camera as ianc import opencsp.common.lib.deflectometry.ImageProjection as ip -import opencsp.common.lib.deflectometry.test.test_ImageProjection as test_ip +from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir import opencsp.common.lib.tool.exception_tools as et import opencsp.common.lib.tool.file_tools as ft @@ -25,6 +25,12 @@ def setUp(self) -> None: ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) + # Load ImageProjectionData + self.file_image_projection_input = os.path.join( + opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' + ) + self.image_projection_data = ip.ImageProjectionData.load_from_hdf(self.file_image_projection_input) + # Create fringe object periods_x = [4**idx for idx in range(4)] periods_y = [4**idx for idx in range(4)] @@ -51,7 +57,7 @@ def test_check_projector_loaded(self): scf.check_projector_loaded('test_check_projector_loaded') # Create a mock ImageProjection object - image_projection = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + image_projection = ip.ImageProjection.in_new_window(self.image_projection_data) # No more error! self.assertTrue(scf.check_projector_loaded('test_check_projector_loaded')) @@ -74,7 +80,7 @@ def test_check_system_loaded(self): scf.check_system_fringe_loaded(None, 'test_check_projector_loaded') # Create the prerequisites - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + im_proj = ip.ImageProjection.in_new_window(self.image_projection_data) ia = ianc.ImageAcquisition() # Still no instance loaded, should throw an error @@ -88,7 +94,7 @@ def test_check_system_loaded(self): self.assertTrue(scf.check_system_fringe_loaded(sys, 'test_check_projector_loaded')) # Release the prerequisites, should throw an error again - ip.close() + im_proj.close() ia.close() with self.assertRaises(RuntimeError): scf.check_system_fringe_loaded(None, 'test_check_projector_loaded') @@ -100,7 +106,7 @@ def test_check_calibration_loaded(self): scf.check_calibration_loaded(None, 'test_check_calibration_loaded') # Create the prerequisites and system instance - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + im_proj = ip.ImageProjection.in_new_window(self.image_projection_data) ia = ianc.ImageAcquisition() sys = ssf.SystemSofastFringe() @@ -116,7 +122,7 @@ def test_run_exposure_cal(self): global sys # Create the prerequisites and system instance - ip = test_ip._ImageProjection.in_new_window(test_ip._ImageProjection.display_dict) + im_proj = ip.ImageProjection.in_new_window(self.image_projection_data) ia = ianc.IA_No_Calibrate() sys = ssf.SystemSofastFringe() From 6c7ad6384feabd79e27b6ca03576098923e80f55 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:45:22 -0600 Subject: [PATCH 20/36] Removed gui_x position from ImageProjection --- opencsp/common/lib/deflectometry/ImageProjection.py | 2 -- opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index 7f02fb50..5be45fde 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -82,7 +82,6 @@ class ImageProjectionData(hdf5_tools.HDF5_IO_Abstract): shift_red_y: int shift_blue_x: int shift_blue_y: int - ui_position_x: int def save_to_hdf(self, file: str, prefix: str = '') -> None: datasets = [] @@ -114,7 +113,6 @@ def load_from_hdf(cls, file: str, prefix: str = ''): 'ImageProjection/shift_red_y', 'ImageProjection/shift_blue_x', 'ImageProjection/shift_blue_y', - 'ImageProjection/ui_position_x', ] for dataset in datasets: dataset = prefix + dataset diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index fd9ece85..08372d58 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -32,7 +32,6 @@ def __init__(self): 'shift_red_y', 'shift_blue_x', 'shift_blue_y', - 'ui_position_x', ] self.data_labels = [ 'Name', @@ -51,7 +50,6 @@ def __init__(self): 'Red Shift Y', 'Blue Shift X', 'Blue Shift Y', - 'GUI X Position', ] self.data_types = [str, int, int, int, int, int, int, int, int, str, int, int, int, int, int, int, int] From 1652b1deb70bb7b718116925d7f55c75fdc6b58e Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:53:29 -0600 Subject: [PATCH 21/36] Updated HDF5 test files to new IP format --- .../data/sofast_common/image_projection.h5 | Bin 18464 -> 19168 bytes .../sofast_common/image_projection_test.h5 | Bin 15072 -> 15072 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/opencsp/test/data/sofast_common/image_projection.h5 b/opencsp/test/data/sofast_common/image_projection.h5 index 7e811bcecda2586b85a2c6a05b086e1be6ee0351..51b6000cc4bc40ee57b4e374b9ac5fd01dfcbd70 100644 GIT binary patch delta 585 zcmZ25f$_mq#t9ls54<*NxiKpiFfxFFh5>{YfbwS;LHHLoPh`H&D0F}cstiIgD0ocV z7`OQWYa9~~3xft!&jihh8|Anhpu7cIKptbnredYFrDTf(K+kf;$)|N9m|B3QTU) zQJjSa)S=X#2o@aqk<;iGCab_Hu<>GN1*uT2gVK}6Ay4qHZb7O%T3J8i!aa2 zOUW;fFV3t=jjvJ9-0hRKf&Tm;>Nhm z4_M=vc$gVBK$J2_=ug}zH~EKw23G=9WP;|zjn6hWaGEJGvoR=4-pQfC2{k~#9H^Gb zfMIf!ff|@bl_zH+tIKL9XFUT*>EXmBzi?5g*=b^-f#O?BMiOCZ=)Hgpcb`Svo*IQVn diff --git a/opencsp/test/data/sofast_common/image_projection_test.h5 b/opencsp/test/data/sofast_common/image_projection_test.h5 index 9f3665707d6c077cea72a3c7dda9b2d6075999d8..9c448a4baa1546dcc9ef673598f3134499653735 100644 GIT binary patch delta 48 xcmaD*`k-`!A2TEKWPfJQjXN$dPR?KzU}9k0T*bP~n0EsM1PFkr&AOJG83BX{4A1}o delta 61 zcmaD*`k-`!A2TD%WPfH)&JEfO5HR_n<^PEXPE5{V6=2%Hu(^tLnK5r^W_&?@ab`(o NeqMaVW?jq8i~x@q6}SKZ From 283de18c87e2856b0b184c16526c8396d019f948 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:53:40 -0600 Subject: [PATCH 22/36] Updated CalibrateDisplayShape to use new IP class --- opencsp/app/sofast/lib/CalibrateDisplayShape.py | 9 +++++---- opencsp/app/sofast/test/test_CalibrateDisplayShape.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/opencsp/app/sofast/lib/CalibrateDisplayShape.py b/opencsp/app/sofast/lib/CalibrateDisplayShape.py index e8c43f4e..b90e33cc 100644 --- a/opencsp/app/sofast/lib/CalibrateDisplayShape.py +++ b/opencsp/app/sofast/lib/CalibrateDisplayShape.py @@ -17,7 +17,7 @@ import opencsp.app.sofast.lib.image_processing as ip from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe from opencsp.common.lib.camera.Camera import Camera -from opencsp.common.lib.deflectometry.ImageProjection import CalParams +from opencsp.common.lib.deflectometry.ImageProjection import CalParams, ImageProjectionData from opencsp.common.lib.geometry.Vxy import Vxy from opencsp.common.lib.geometry.Vxyz import Vxyz import opencsp.common.lib.photogrammetry.photogrammetry as ph @@ -41,7 +41,7 @@ class DataInput: Aruco marker corners, meters camera : Camera Camera object used to capture screen distortion data - image_projection_data : dict + image_projection_data : ImageProjectionData Image projection parameters measurements_screen : list[MeasurementSofastFringe] Screen shape Sofast measurement objects @@ -56,7 +56,7 @@ class DataInput: resolution_xy: tuple[int, int] pts_xyz_marker: Vxyz camera: Camera - image_projection_data: dict + image_projection_data: ImageProjectionData measurements_screen: list[MeasurementSofastFringe] assume_located_points: bool = True ray_intersection_threshold: float = 0.001 @@ -113,7 +113,8 @@ def __init__(self, data_input: DataInput) -> 'CalibrateDisplayShape': # Load cal params cal_pattern_params = CalParams( - self.data_input.image_projection_data['size_x'], self.data_input.image_projection_data['size_y'] + self.data_input.image_projection_data.active_area_size_x, + self.data_input.image_projection_data.active_area_size_y, ) # Initialize calculation data structure diff --git a/opencsp/app/sofast/test/test_CalibrateDisplayShape.py b/opencsp/app/sofast/test/test_CalibrateDisplayShape.py index 62d100a2..e7a0e2cf 100644 --- a/opencsp/app/sofast/test/test_CalibrateDisplayShape.py +++ b/opencsp/app/sofast/test/test_CalibrateDisplayShape.py @@ -13,7 +13,7 @@ from opencsp.app.sofast.lib.CalibrateDisplayShape import CalibrateDisplayShape, DataInput from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe from opencsp.common.lib.camera.Camera import Camera -from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjectionData from opencsp.common.lib.geometry.Vxyz import Vxyz from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir from opencsp.common.lib.tool.hdf5_tools import load_hdf5_datasets @@ -47,7 +47,7 @@ def setUpClass(cls): corner_ids = pts_marker_data[:, 1] screen_cal_point_pairs = np.loadtxt(file_screen_cal_point_pairs, delimiter=',', skiprows=1).astype(int) camera = Camera.load_from_hdf(file_camera_distortion) - image_projection_data = ImageProjection.load_from_hdf(file_image_projection) + image_projection_data = ImageProjectionData.load_from_hdf(file_image_projection) # Store input data in data class data_input = DataInput( From 06aee78650b60c51b91321b874b55b33b1274b51 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 12:03:16 -0600 Subject: [PATCH 23/36] Removed UI_x input from IP setup GUI --- opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index 08372d58..a3fab786 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -81,7 +81,7 @@ def __init__(self): def set_window_size(self): """Updates the window size to current set value""" # Set size and position of window - self.root.geometry(f'500x670+{self.display_data.ui_position_x:d}+100') + self.root.geometry('500x670+100+100') def create_layout(self): """Creates GUI widgets""" @@ -317,7 +317,6 @@ def load_defaults(self): 'shift_red_y': 0, 'shift_blue_x': 0, 'shift_blue_y': 0, - 'ui_position_x': 100, } self.display_data = ImageProjectionData(**kwargs) self.set_user_data() From a8b560881c693c9969a35d7974ac8ee038492d7b Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 12:03:46 -0600 Subject: [PATCH 24/36] Made calibration fiducial image zeros base instead of ones --- .../lib/deflectometry/ImageProjection.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index 5be45fde..f8eade4f 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -339,7 +339,7 @@ def show_calibration_fiducial_image(self): on a white background. Fiducial locations measured from center of dots. """ # Create base image to show - array = np.ones( + array = np.zeros( (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), dtype=self.display_data.projector_data_type, ) @@ -372,7 +372,35 @@ def show_calibration_marker_image(self): """Shows a calibration image with N Aruco markers. Markers are black on a white background. """ - print('Test: show calibration marker image') + # Create base image to show + array = np.ones( + (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), + dtype=self.display_data.projector_data_type, + ) + + # Get calibration pattern parameters + pattern_params = CalParams(self.display_data.active_area_size_x, self.display_data.active_area_size_y) + + # Add markers + for x_loc, y_loc, idx in zip(pattern_params.x_pixel, pattern_params.y_pixel, pattern_params.index): + # Place fiducial + array[y_loc, x_loc, 1] = self.display_data.projector_max_int + # Place label (offset so label is in view) + x_pt_to_center = float(self.display_data.active_area_size_x) / 2 - x_loc + y_pt_to_center = float(self.display_data.active_area_size_y) / 2 - y_loc + if x_pt_to_center >= 0: + dx = 15 + else: + dx = -35 + if y_pt_to_center >= 0: + dy = 20 + else: + dy = -10 + # Draw text + cv.putText(array, f'{idx:d}', (x_loc + dx, y_loc + dy), cv.FONT_HERSHEY_PLAIN, 1, (0, 255, 0)) + + # Display with black border + self.display_image_in_active_area(array) def get_black_array_active_area(self) -> np.ndarray: """ From a934b5ac173bcfb156ddd4cf267212539138b110 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 12:34:42 -0600 Subject: [PATCH 25/36] Fixed deactivate bussons in IP setup GUI --- .../lib/deflectometry/ImageProjectionSetupGUI.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index a3fab786..df9a2c55 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -120,18 +120,18 @@ def create_layout(self): self.btn_axes.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') # Show calibration image button - self.btn_calib = tkinter.Button( + self.btn_calib_fid = tkinter.Button( self.root, text='Show calibration fiducial image', command=self.show_calibration_fiducial_image ) r += 1 - self.btn_calib.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') + self.btn_calib_fid.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') # Show calibration image button - self.btn_calib = tkinter.Button( + self.btn_calib_mkr = tkinter.Button( self.root, text='Show calibration marker image', command=self.show_calibration_marker_image ) r += 1 - self.btn_calib.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') + self.btn_calib_mkr.grid(row=r, column=0, pady=2, padx=2, sticky='nesw') # Save as button self.btn_save = tkinter.Button(self.root, text='Save as HDF...', command=self.save_as) @@ -174,7 +174,8 @@ def activate_btns(self, active: bool): self.btn_close_proj['state'] = active_projector self.btn_axes['state'] = active_projector self.btn_crosshairs['state'] = active_projector - self.btn_calib['state'] = active_projector + self.btn_calib_fid['state'] = active_projector + self.btn_calib_mkr['state'] = active_projector def show_projection_window(self): """Opens the ImageProjection window.""" From f95575b68d30424118b4abadff58139fd957b650 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 12:34:53 -0600 Subject: [PATCH 26/36] Added marker calibration image to IP --- .../lib/deflectometry/ImageProjection.py | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index f8eade4f..5187b250 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -60,6 +60,8 @@ def __init__(self, size_x: int, size_y: int) -> 'CalParams': self.y_screen: np.ndarray[float] = y_mat_screen.flatten() # Point index self.index: np.ndarray[int] = np.arange(self.x_pixel.size, dtype=int) + # Width of Aruco markers + self.marker_width: int = int(min(size_x, size_y) * 0.2) @dataclass @@ -154,6 +156,9 @@ def __init__(self, root: tkinter.Tk, display_data: ImageProjectionData) -> 'Imag self.canvas.pack() self.canvas.configure(background='black', highlightthickness=0) + # Save aruco marker dictionary for calibration image generation + self.aruco_dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000) + # Save active area data self._x_active_1: int self._x_active_2: int @@ -373,31 +378,44 @@ def show_calibration_marker_image(self): on a white background. """ # Create base image to show - array = np.ones( - (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), - dtype=self.display_data.projector_data_type, + array = ( + np.ones( + (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), + dtype=self.display_data.projector_data_type, + ) + * self.display_data.projector_max_int ) # Get calibration pattern parameters pattern_params = CalParams(self.display_data.active_area_size_x, self.display_data.active_area_size_y) + w = pattern_params.marker_width # Add markers for x_loc, y_loc, idx in zip(pattern_params.x_pixel, pattern_params.y_pixel, pattern_params.index): - # Place fiducial - array[y_loc, x_loc, 1] = self.display_data.projector_max_int - # Place label (offset so label is in view) + # Make marker + img_mkr = self._make_aruco_marker(w, idx) + # Place marker and label x_pt_to_center = float(self.display_data.active_area_size_x) / 2 - x_loc y_pt_to_center = float(self.display_data.active_area_size_y) / 2 - y_loc - if x_pt_to_center >= 0: - dx = 15 - else: - dx = -35 - if y_pt_to_center >= 0: - dy = 20 - else: - dy = -10 + + if (x_pt_to_center >= 0) and (y_pt_to_center >= 0): # upper left quadrant + array[y_loc : y_loc + w, x_loc : x_loc + w, :] = img_mkr + dy = int(w / 2) + dx = w + 5 + elif x_pt_to_center >= 0: # lower left quadrant + array[y_loc - w : y_loc, x_loc : x_loc + w, :] = np.rot90(img_mkr, 1) + dy = -int(w / 2) + dx = w + 5 + elif y_pt_to_center >= 0: # top right quadrant + array[y_loc : y_loc + w, x_loc - w : x_loc, :] = np.rot90(img_mkr, 3) + dy = int(w / 2) + dx = -w - 15 + else: # bottom right quadrant + array[y_loc - w : y_loc, x_loc - w : x_loc, :] = np.rot90(img_mkr, 2) + dy = -int(w / 2) + dx = -w - 15 # Draw text - cv.putText(array, f'{idx:d}', (x_loc + dx, y_loc + dy), cv.FONT_HERSHEY_PLAIN, 1, (0, 255, 0)) + cv.putText(array, f'{idx:d}', (x_loc + dx, y_loc + dy), cv.FONT_HERSHEY_PLAIN, 1, (0, 0, 0)) # Display with black border self.display_image_in_active_area(array) @@ -570,3 +588,12 @@ def close(self): with et.ignored(Exception): self.root.destroy() + + def _make_aruco_marker(self, width: int, id_: int) -> np.ndarray: + """Returns NxMx3 aruco marker array""" + # Define marker image + img_2d = np.ones((width, width), dtype='uint8') * self.display_data.projector_max_int + + # Create marker image + cv.aruco.drawMarker(self.aruco_dictionary, id_, width, img_2d) + return np.concatenate([img_2d[:, :, None]] * 3, axis=2) From 7241326d9a395b059f286e7b339012873009aadf Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 12:36:51 -0600 Subject: [PATCH 27/36] Removed extraneous data_type input in IP setup GUI --- opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py index df9a2c55..4cdb4fad 100644 --- a/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py +++ b/opencsp/common/lib/deflectometry/ImageProjectionSetupGUI.py @@ -51,7 +51,7 @@ def __init__(self): 'Blue Shift X', 'Blue Shift Y', ] - self.data_types = [str, int, int, int, int, int, int, int, int, str, int, int, int, int, int, int, int] + self.data_types = [str, int, int, int, int, int, int, int, int, str, int, int, int, int, int, int] # Declare variables self.projector: ImageProjection @@ -270,7 +270,7 @@ def get_user_data(self): # Gets data from user input boxes and saves in class data = {} - for dtype, name, entry in zip(self.data_types, self.data_names, self.data_cells): + for dtype, name, entry in zip(self.data_types, self.data_names, self.data_cells, strict=True): data.update({name: dtype(entry.get())}) self.display_data = ImageProjectionData(**data) @@ -285,7 +285,7 @@ def check_inputs(self) -> bool: """ # Checks inputs are correct - for name, dtype, entry in zip(self.data_labels, self.data_types, self.data_cells): + for name, dtype, entry in zip(self.data_labels, self.data_types, self.data_cells, strict=True): try: dtype(entry.get()) except ValueError: From 37abcbaf7087a308d9b74ac0a63f6a37715c63f8 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 14:21:16 -0600 Subject: [PATCH 28/36] Updated example calibration screen shape to use new IP class --- .../sofast_calibration/example_calibration_screen_shape.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/sofast_calibration/example_calibration_screen_shape.py b/example/sofast_calibration/example_calibration_screen_shape.py index 62999552..ff13d415 100644 --- a/example/sofast_calibration/example_calibration_screen_shape.py +++ b/example/sofast_calibration/example_calibration_screen_shape.py @@ -6,7 +6,7 @@ from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe from opencsp.app.sofast.lib.CalibrateDisplayShape import CalibrateDisplayShape, DataInput from opencsp.common.lib.camera.Camera import Camera -from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjectionData from opencsp.common.lib.geometry.Vxyz import Vxyz from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir import opencsp.common.lib.tool.file_tools as ft @@ -61,7 +61,7 @@ def example_screen_shape_calibration(): # Load input data camera = Camera.load_from_hdf(file_camera_distortion) - image_projection_data = ImageProjection.load_from_hdf(file_image_projection) + image_projection_data = ImageProjectionData.load_from_hdf(file_image_projection) screen_cal_point_pairs = np.loadtxt(file_screen_cal_point_pairs, delimiter=',', skiprows=1, dtype=int) # Store input data in data class From 2608793677c48cc790be7bc02f98bef9390e2e62 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 14:23:17 -0600 Subject: [PATCH 29/36] Updated example_calculate_dot_locations_from_display_shape --- .../example_create_dot_pattern_from_display_shape.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/example/sofast_fixed/example_create_dot_pattern_from_display_shape.py b/example/sofast_fixed/example_create_dot_pattern_from_display_shape.py index 30c51d47..04ae1ab7 100644 --- a/example/sofast_fixed/example_create_dot_pattern_from_display_shape.py +++ b/example/sofast_fixed/example_create_dot_pattern_from_display_shape.py @@ -2,8 +2,8 @@ from opencsp.app.sofast.lib.DotLocationsFixedPattern import DotLocationsFixedPattern from opencsp.app.sofast.lib.DisplayShape import DisplayShape -from opencsp.app.sofast.lib.SystemSofastFixed import SystemSofastFixed -from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.app.sofast.lib.PatternSofastFixed import PatternSofastFixed +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjectionData from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir import opencsp.common.lib.tool.file_tools as ft import opencsp.common.lib.tool.log_tools as lt @@ -33,7 +33,7 @@ def example_calculate_dot_locations_from_display_shape(): # Load data display = DisplayShape.load_from_hdf(file_display) - im_proj_params = ImageProjection.load_from_hdf(file_image_projection) + im_proj_params = ImageProjectionData.load_from_hdf(file_image_projection) # 2. Define dot projection object # =============================== @@ -41,7 +41,9 @@ def example_calculate_dot_locations_from_display_shape(): spacing_dot = 6 # pixels # Create fixed pattern projection object - projection = SystemSofastFixed(im_proj_params['size_x'], im_proj_params['size_y'], width_dot, spacing_dot) + projection = PatternSofastFixed( + im_proj_params.active_area_size_x, im_proj_params.active_area_size_y, width_dot, spacing_dot + ) # 3. Define DotLocationsFixedPattern object # ========================================= From 647370d199d30b75a28af3e45dfc8b216baa6587 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 16:59:04 -0600 Subject: [PATCH 30/36] Updated Sofast CLI --- contrib/app/sofast/SofastCommandLineInterface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/app/sofast/SofastCommandLineInterface.py b/contrib/app/sofast/SofastCommandLineInterface.py index 02fc143c..19ded032 100644 --- a/contrib/app/sofast/SofastCommandLineInterface.py +++ b/contrib/app/sofast/SofastCommandLineInterface.py @@ -485,8 +485,8 @@ def _run_given_input(self, retval: str) -> None: image_acquisition_in.frame_size = (1626, 1236) image_acquisition_in.gain = 230 - image_projection_in = ImageProjection.load_from_hdf_and_display(file_image_projection) - image_projection_in.display_data['image_delay'] = 200 + image_projection_in = ImageProjection.load_from_hdf(file_image_projection) + image_projection_in.display_data.image_delay_ms = 200 camera_in = Camera.load_from_hdf(file_camera) facet_definition_in = DefinitionFacet.load_from_json(file_facet_definition_json) From 715703004059a0b82e476d5ffff73fa5d882fb7c Mon Sep 17 00:00:00 2001 From: Braden Date: Fri, 31 May 2024 14:49:02 -0600 Subject: [PATCH 31/36] Added doc strings to ImageProjection --- .../lib/deflectometry/ImageProjection.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index 5187b250..fb2f9954 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -10,7 +10,7 @@ from PIL import Image, ImageTk import opencsp.common.lib.tool.exception_tools as et -import opencsp.common.lib.tool.hdf5_tools as hdf5_tools +from opencsp.common.lib.tool import hdf5_tools import opencsp.common.lib.tool.tk_tools as tkt @@ -66,24 +66,57 @@ def __init__(self, size_x: int, size_y: int) -> 'CalParams': @dataclass class ImageProjectionData(hdf5_tools.HDF5_IO_Abstract): - """Dataclass containting ImageProjection parameters. All position/size units are screen pixels""" + """Dataclass containting ImageProjection parameters used to define the image/screen + geometry when projecting an image on a display. + * All position/size/shift units are screen pixels. + * The *active_area* is the region which is actively used to project images, as opposed to the + black background. + * The *main_window* is the tkinter window which holds the *active_area* region. Typically, the + main_window is the size of the screen being projected onto. Any area of the main_window + that is not filled by the active_area is filled with black. This is useful with reducing + light. + * When defining window positions, the reference point is the upper-left corner of the + window/screen. + """ name: str + """The name of the ImageProjection configuration""" main_window_size_x: int + """The width of the main window. (For more information on main_window, see class docstring)""" main_window_size_y: int + """The height of the main window. (For more information on main_window, see class docstring)""" main_window_position_x: int + """The x position of the upper-left corner of the main window relative to the upper-left corner + of the projection screen. Right is positive. (For more information on main_window, see class docstring)""" main_window_position_y: int + """The y position of the upper-left corner of the main window relative to the upper-left corner + of the projection screen. Down is positive. (For more information on main_window, see class docstring)""" active_area_size_x: int + """The width of the active area within the main window. (For more information on active_area, see class docstring)""" active_area_size_y: int + """The height of the active area within the main window. (For more information on active_area, see class docstring)""" active_area_position_x: int + """The x position of the upper-left corner of the active area relative to the upper-left corner + of the main window. Right is positive (For more information on main_window and active_area, see class docstring)""" active_area_position_y: int + """The y position of the upper-left corner of the active area relative to the upper-left corner + of the main window. Down is positive. (For more information on main_window and active_area, see class docstring)""" projector_data_type: str + """The data type string to be sent to the projector. In most cases, this will be an unsigned 8-bit integer ('uint8')""" projector_max_int: int + """The integer value that corresponds to a perfectly white image on the screen. In most cases + this will be 255.""" image_delay_ms: float + """The delay between the display being sent an image to display, and when the camera should start recording. This + is specific to each display/camera/computer setup.""" shift_red_x: int + """The red channel x shift in an RGB display relative to green in pixels. Right is positive.""" shift_red_y: int + """The red channel y shift in an RGB display relative to green in pixels. Down is positive.""" shift_blue_x: int + """The blue channel x shift in an RGB display relative to green in pixels. Right is positive.""" shift_blue_y: int + """The blue channel y shift in an RGB display relative to green in pixels. Down is positive.""" def save_to_hdf(self, file: str, prefix: str = '') -> None: datasets = [] From eccf8f70469fb451cb20fc3af78681556a34aae5 Mon Sep 17 00:00:00 2001 From: Braden Date: Fri, 24 May 2024 14:44:06 -0600 Subject: [PATCH 32/36] Added dataclass to IP --- opencsp/common/lib/deflectometry/ImageProjection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index fb2f9954..4dccc630 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -377,7 +377,7 @@ def show_calibration_fiducial_image(self): on a white background. Fiducial locations measured from center of dots. """ # Create base image to show - array = np.zeros( + array = np.ones( (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), dtype=self.display_data.projector_data_type, ) From 118c78f3755bb77c6f06e449aeed99e432833bea Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 09:39:06 -0600 Subject: [PATCH 33/36] Updated test_ImageProjection to work with ImageProjection changes. --- opencsp/common/lib/deflectometry/test/test_ImageProjection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py index 2ee92f6a..d432b74b 100644 --- a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py +++ b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py @@ -16,6 +16,9 @@ def setUp(self) -> None: path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "ImageProjection") self.out_dir = os.path.join(path, "data", "output", "ImageProjection") + self.file_image_projection_input = os.path.join( + opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' + ) ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) From 0e23cf7746b63c2b70898c86333b8ca4c9534c75 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:29:05 -0600 Subject: [PATCH 34/36] Updated test_SystemSofastFringe to use new ImageProjection class --- opencsp/app/sofast/test/test_SystemSofastFringe.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opencsp/app/sofast/test/test_SystemSofastFringe.py b/opencsp/app/sofast/test/test_SystemSofastFringe.py index eda62d3c..51138928 100644 --- a/opencsp/app/sofast/test/test_SystemSofastFringe.py +++ b/opencsp/app/sofast/test/test_SystemSofastFringe.py @@ -24,6 +24,9 @@ def setUp(self): path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "SystemSofastFringe") self.out_dir = os.path.join(path, "data", "output", "SystemSofastFringe") + self.file_image_projection_input = os.path.join( + opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' + ) ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) From 8f3532f5c0121e61eac494fbbcd14911adbf9aab Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 11:40:05 -0600 Subject: [PATCH 35/36] Removed extraneous calls to IP.load_from_hdf --- opencsp/app/sofast/test/test_SystemSofastFringe.py | 3 --- .../common/lib/deflectometry/test/test_ImageProjection.py | 7 +++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/opencsp/app/sofast/test/test_SystemSofastFringe.py b/opencsp/app/sofast/test/test_SystemSofastFringe.py index 51138928..eda62d3c 100644 --- a/opencsp/app/sofast/test/test_SystemSofastFringe.py +++ b/opencsp/app/sofast/test/test_SystemSofastFringe.py @@ -24,9 +24,6 @@ def setUp(self): path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "SystemSofastFringe") self.out_dir = os.path.join(path, "data", "output", "SystemSofastFringe") - self.file_image_projection_input = os.path.join( - opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' - ) ft.create_directories_if_necessary(self.data_dir) ft.create_directories_if_necessary(self.out_dir) diff --git a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py index d432b74b..14bc45a9 100644 --- a/opencsp/common/lib/deflectometry/test/test_ImageProjection.py +++ b/opencsp/common/lib/deflectometry/test/test_ImageProjection.py @@ -16,11 +16,14 @@ def setUp(self) -> None: path, _, _ = ft.path_components(__file__) self.data_dir = os.path.join(path, "data", "input", "ImageProjection") self.out_dir = os.path.join(path, "data", "output", "ImageProjection") + ft.create_directories_if_necessary(self.data_dir) + ft.create_directories_if_necessary(self.out_dir) + + # Load display data self.file_image_projection_input = os.path.join( opencsp_code_dir(), 'test/data/sofast_common/image_projection_test.h5' ) - ft.create_directories_if_necessary(self.data_dir) - ft.create_directories_if_necessary(self.out_dir) + self.image_projection_data = ip.ImageProjectionData.load_from_hdf(self.file_image_projection_input) # Load display data self.file_image_projection_input = os.path.join( From ded3a3c3f0545a7294b093db12bacd933ff39389 Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 28 May 2024 12:03:46 -0600 Subject: [PATCH 36/36] Made calibration fiducial image zeros base instead of ones --- opencsp/common/lib/deflectometry/ImageProjection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencsp/common/lib/deflectometry/ImageProjection.py b/opencsp/common/lib/deflectometry/ImageProjection.py index 4dccc630..fb2f9954 100644 --- a/opencsp/common/lib/deflectometry/ImageProjection.py +++ b/opencsp/common/lib/deflectometry/ImageProjection.py @@ -377,7 +377,7 @@ def show_calibration_fiducial_image(self): on a white background. Fiducial locations measured from center of dots. """ # Create base image to show - array = np.ones( + array = np.zeros( (self.display_data.active_area_size_y, self.display_data.active_area_size_x, 3), dtype=self.display_data.projector_data_type, )