diff --git a/doc/source/library_reference/common/lib/camera/config.rst b/doc/source/library_reference/common/lib/camera/config.rst new file mode 100644 index 000000000..4a0481af5 --- /dev/null +++ b/doc/source/library_reference/common/lib/camera/config.rst @@ -0,0 +1,74 @@ +Base Class +========== + +.. currentmodule:: opencsp.common.lib.camera.Camera + +.. automodule:: opencsp.common.lib.camera.Camera + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise + +Image Acquisition Classes +========================= + +.. currentmodule:: opencsp.common.lib.camera.ImageAcquisitionAbstract + +.. automodule:: opencsp.common.lib.camera.ImageAcquisitionAbstract + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise + +.. currentmodule:: opencsp.common.lib.camera.ImageAcquisition_DCAM_color + +.. automodule:: opencsp.common.lib.camera.ImageAcquisition_DCAM_color + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise + +.. currentmodule:: opencsp.common.lib.camera.ImageAcquisition_DCAM_mono + +.. automodule:: opencsp.common.lib.camera.ImageAcquisition_DCAM_mono + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise + +.. currentmodule:: opencsp.common.lib.camera.ImageAcquisition_MSMF + +.. automodule:: opencsp.common.lib.camera.ImageAcquisition_MSMF + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise + +UCamera Classes and Utility Functions +===================================== + +.. currentmodule:: opencsp.common.lib.camera.UCamera + +.. automodule:: opencsp.common.lib.camera.UCamera + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise + +Utility Functions +================= + +.. currentmodule:: opencsp.common.lib.camera.image_processing + +.. automodule:: opencsp.common.lib.camera.image_processing + :members: + :special-members: __init__ + :undoc-members: + :show-inheritance: + :member-order: groupwise diff --git a/doc/source/library_reference/common/lib/camera/index.rst b/doc/source/library_reference/common/lib/camera/index.rst new file mode 100644 index 000000000..c25079951 --- /dev/null +++ b/doc/source/library_reference/common/lib/camera/index.rst @@ -0,0 +1,9 @@ +Camera +====== + +This is a collection of camera utilities for OpenCSP. + +.. toctree:: + :maxdepth: 1 + + config.rst diff --git a/doc/source/library_reference/index.rst b/doc/source/library_reference/index.rst index bff37129f..db74f4999 100644 --- a/doc/source/library_reference/index.rst +++ b/doc/source/library_reference/index.rst @@ -10,4 +10,5 @@ This section describes the OpenCSP classes and interfaces. app/target/index.rst app/camera_calibration/index.rst app/scene_reconstruction/index.rst - common/lib/cv/index.rst \ No newline at end of file + common/lib/cv/index.rst + common/lib/camera/index.rst \ No newline at end of file diff --git a/opencsp/app/camera_calibration/lib/calibration_camera.py b/opencsp/app/camera_calibration/lib/calibration_camera.py index a0ebccf43..74b61f1d5 100644 --- a/opencsp/app/camera_calibration/lib/calibration_camera.py +++ b/opencsp/app/camera_calibration/lib/calibration_camera.py @@ -36,7 +36,7 @@ def calibrate_camera( Returns ------- - Camera : Camera + Camera : opencsp.common.lib.camera.Camera.Camera Camera class. r_cam_object : list[Rotation, ...] Camera-object rotation vector @@ -74,7 +74,7 @@ def view_distortion(camera: Camera, ax1: Axes, ax2: Axes, ax3: Axes, num_samps: Parameters ---------- - camera : Camera + camera : opencsp.common.lib.camera.Camera.Camera Camera to visualize. ax1 : Axes Axis to plot radial distortion. diff --git a/opencsp/common/lib/camera/ImageAcquisition_DCAM_color.py b/opencsp/common/lib/camera/ImageAcquisition_DCAM_color.py index 8cc901ef4..65396b687 100644 --- a/opencsp/common/lib/camera/ImageAcquisition_DCAM_color.py +++ b/opencsp/common/lib/camera/ImageAcquisition_DCAM_color.py @@ -76,6 +76,24 @@ def __init__(self, instance: int = 0, pixel_format: str = 'BayerRG12'): self._shutter_cal_values = np.linspace(shutter_min, shutter_max, 2**13).astype(int) def instance_matches(self, possible_matches: list[ImageAcquisitionAbstract]) -> bool: + """ + Determine whether this camera instance matches any instance in the provided list. + + This method checks if there is another camera instance in the `possible_matches` + list that has the same `basler_index` as the current instance. + + Parameters + ---------- + possible_matches : list[ImageAcquisitionAbstract] + A list of camera instances to check against. Each instance should be of + type `ImageAcquisitionAbstract` and may have a `basler_index` attribute. + + Returns + ------- + bool + True when the first matching instance is found; False otherwise. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. for camera in possible_matches: if not hasattr(camera, 'basler_index'): continue @@ -84,6 +102,35 @@ def instance_matches(self, possible_matches: list[ImageAcquisitionAbstract]) -> return False def get_frame(self, encode: bool = True) -> np.ndarray: + """ + Captures a single frame from the camera. + + This method initiates the frame capture process and retrieves the image data + from the camera. The captured image can be returned in its raw Bayer format + or converted to an RGB format based on the `encode` parameter. + + Parameters + ---------- + encode : bool, optional + If True (default), the captured Bayer-encoded image will be converted to + a 3D RGB image using the `encode_RG_to_RGB` function. If False, the raw + Bayer-encoded image will be returned. + + Returns + ------- + np.ndarray + The captured image as a NumPy array. The shape of the array will depend + on the encoding: + - If `encode` is True, the shape will be (height, width, 3) for RGB images. + - If `encode` is False, the shape will be (height, width) for Bayer images. + + Raises + ------ + pylon.RuntimeException + If the frame grab is unsuccessful, an exception is raised indicating the + failure to capture the frame. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # Start frame capture self.cap.StartGrabbingMax(1) grabResult = self.cap.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException) @@ -147,6 +194,7 @@ def shutter_cal_values(self) -> np.ndarray: return self._shutter_cal_values def close(self): + """Closes the camera connection""" with et.ignored(Exception): super().close() with et.ignored(Exception): diff --git a/opencsp/common/lib/camera/ImageAcquisition_DCAM_mono.py b/opencsp/common/lib/camera/ImageAcquisition_DCAM_mono.py index 36a33ec6c..a35068ff2 100644 --- a/opencsp/common/lib/camera/ImageAcquisition_DCAM_mono.py +++ b/opencsp/common/lib/camera/ImageAcquisition_DCAM_mono.py @@ -100,6 +100,24 @@ def _check_pypylon_version(cls): cls._has_checked_pypylon_version = True def instance_matches(self, possible_matches: list[ImageAcquisitionAbstract]) -> bool: + """ + Determine whether this camera instance matches any instance in the provided list. + + This method checks if there is another camera instance in the `possible_matches` + list that has the same `basler_index` as the current instance. + + Parameters + ---------- + possible_matches : list[ImageAcquisitionAbstract] + A list of camera instances to check against. Each instance should be of + type `ImageAcquisitionAbstract` and may have a `basler_index` attribute. + + Returns + ------- + bool + True when the first matching instance is found; False otherwise. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. for camera in possible_matches: if not hasattr(camera, 'basler_index'): continue @@ -108,6 +126,33 @@ def instance_matches(self, possible_matches: list[ImageAcquisitionAbstract]) -> return False def get_frame(self) -> np.ndarray: + """ + Captures a single frame from the Basler DCAM monochromatic camera. + + This method initiates the frame capture process and retrieves the image data + from the camera. The method waits for the frame to be captured and returns + the image data as a NumPy array. + + Returns + ------- + np.ndarray + The captured image as a NumPy array. The shape of the array will depend + on the pixel format set during initialization (e.g., Mono8, Mono12). + + Raises + ------ + pylon.RuntimeException + If the frame grab is unsuccessful, an exception is raised indicating the + failure to capture the frame. + + Notes + ----- + The method calculates the expected exposure time in milliseconds and uses + this value to set a timeout for the frame retrieval. The timeout is set to + 1.5 times the expected image acquisition time to ensure that the frame is + captured successfully. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # Start frame capture self.cap.StartGrabbingMax(1) exposure_time_ms = self.cap.ExposureTimeRaw.Max / 1000 # exposure time, ms @@ -170,6 +215,7 @@ def shutter_cal_values(self) -> np.ndarray: return self._shutter_cal_values def close(self): + """Closes the camera connection""" with et.ignored(Exception): super().close() with et.ignored(Exception): diff --git a/opencsp/common/lib/camera/ImageAcquisition_MSMF.py b/opencsp/common/lib/camera/ImageAcquisition_MSMF.py index 2e10e49e1..1f2ba8e3f 100644 --- a/opencsp/common/lib/camera/ImageAcquisition_MSMF.py +++ b/opencsp/common/lib/camera/ImageAcquisition_MSMF.py @@ -24,6 +24,26 @@ def __init__(self, instance: int = 0): raise IOError("Error opening webcam") def instance_matches(self, possible_matches: list[ImageAcquisitionAbstract]) -> bool: + """ + Determine whether this camera instance matches any instance in the provided list. + + This method checks if there is another instance of the `ImageAcquisition` class + in the `possible_matches` list. Since only one MSMF camera is supported, + the method returns True if any instance of `ImageAcquisition` is found; otherwise, + it returns False. + + Parameters + ---------- + possible_matches : list[ImageAcquisitionAbstract] + A list of camera instances to check against. Each instance should be of + type `ImageAcquisitionAbstract`. + + Returns + ------- + bool + True if a matching instance of `ImageAcquisition` is found; False otherwise. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. for camera in possible_matches: if isinstance(camera, ImageAcquisition): # only one MSMF camera is supported @@ -31,6 +51,33 @@ def instance_matches(self, possible_matches: list[ImageAcquisitionAbstract]) -> return False def get_frame(self) -> np.ndarray: + """ + Captures a single frame from the connected camera. + + This method reads a frame from the camera and returns it as a NumPy array. + If the captured frame is in color (3-dimensional), it is converted to a + grayscale image by averaging the color channels. The method raises an + exception if the frame capture is unsuccessful. + + Returns + ------- + np.ndarray + The captured image as a NumPy array. The shape of the array will be: + - (height, width) for grayscale images. + - If the input frame is in color, it will be converted to grayscale + by averaging the channels. + + Raises + ------ + Exception + If the frame was not captured successfully, an exception is raised + indicating the failure to capture the frame. + + ValueError + If the output frame does not have 2 or 3 dimensions, a ValueError + is raised indicating the incorrect number of dimensions. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # Capture image ret, frame = self.cap.read() @@ -90,6 +137,7 @@ def shutter_cal_values(self) -> np.ndarray: raise ValueError('exposure_time cannot be adjusted with MSMF camera; adjust screen brightness instead.') def close(self): + """Closes the camera connection""" with et.ignored(Exception): super().close() with et.ignored(Exception): diff --git a/opencsp/common/lib/camera/UCamera.py b/opencsp/common/lib/camera/UCamera.py index b91429554..540d949f6 100644 --- a/opencsp/common/lib/camera/UCamera.py +++ b/opencsp/common/lib/camera/UCamera.py @@ -68,10 +68,33 @@ def __init__( # ACCESS def max_focal_length(self): + """ + Returns the maximum focal length of the camera. + + Returns + ------- + float + The maximum focal length in meters. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return self.focal_length_max @staticmethod def csv_header(delimeter=","): + """ + Returns the CSV column headings for the camera data. + + Parameters + ---------- + delimeter : str, optional + The delimiter to use in the CSV header (default is ','). + + Returns + ------- + str + A string containing the CSV column headings. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return delimeter.join( [ "name", @@ -90,6 +113,20 @@ def csv_header(delimeter=","): @classmethod def from_csv_line(cls, data_row: list[str]): + """ + Creates a Camera object from a CSV data row. + + Parameters + ---------- + data_row : list of str + A list containing the camera data in CSV format. + + Returns + ------- + tuple + A tuple containing the Camera object and any remaining data. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. ret = cls("", 1, 1, 1, 1, 1) ret.name: str = data_row[0].replace("commmmma", ",") ret.sensor_x: float = np.float64(data_row[1]) @@ -105,6 +142,20 @@ def from_csv_line(cls, data_row: list[str]): return ret, data_row[11:] def to_csv_line(self, delimeter=","): + """ + Converts the Camera object to a CSV line. + + Parameters + ---------- + delimeter : str, optional + The delimiter to use in the CSV line (default is ','). + + Returns + ------- + str + A string representing the Camera object in CSV format. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return delimeter.join( [ str(v) @@ -129,6 +180,15 @@ def to_csv_line(self, delimeter=","): def mavic_zoom(): + """ + Creates a Camera object for the Mavic Zoom. + + Returns + ------- + Camera + A Camera object configured for the Mavic Zoom. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return Camera( name='Mavic Zoom', sensor_x_mm=6.17, # mm. @@ -140,6 +200,15 @@ def mavic_zoom(): def sony_alpha_20mm_landscape(): + """ + Creates a Camera object for the Sony Alpha 20mm (landscape orientation). + + Returns + ------- + Camera + A Camera object configured for the Sony Alpha 20mm (landscape). + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return Camera( name='Sony Alpha, 20mm', sensor_x_mm=35.9, # mm. @@ -151,6 +220,15 @@ def sony_alpha_20mm_landscape(): def sony_alpha_20mm_portrait(): + """ + Creates a Camera object for the Sony Alpha 20mm (portrait orientation). + + Returns + ------- + Camera + A Camera object configured for the Sony Alpha 20mm (portrait). + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return Camera( name='Sony Alpha, 20mm', sensor_x_mm=24.0, # mm. @@ -162,6 +240,15 @@ def sony_alpha_20mm_portrait(): def ultra_wide_angle(): + """ + Creates a Camera object for an ultra wide angle camera. + + Returns + ------- + Camera + A Camera object configured for an ultra wide angle lens. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return Camera( name='Ultra Wide Angle', sensor_x_mm=24.0, # mm. @@ -182,8 +269,6 @@ def ultra_wide_angle(): class RealCamera(csvi.CsvInterface): """ Model of a camera and its intrinsic parameters, including distortion. - - We will probably need to extend this model going forward. """ def __init__( @@ -207,6 +292,35 @@ def __init__( p_1=-0.00032978, # ?? SCAFFOLDING RCB -- WHAT ARE UNITS? ARE VALUES VALID FOR EXPECTED UNITS? p_2=-0.0001401, # ?? SCAFFOLDING RCB -- WHAT ARE UNITS? ARE VALUES VALID FOR EXPECTED UNITS? ): + """ + Initializes the RealCamera object with specified parameters. + + Parameters + ---------- + name : str, optional + String describing the camera and lens (default is 'Mavic Zoom'). + n_x : int, optional + Number of pixels in the horizontal direction (default is 3840). + n_y : int, optional + Number of pixels in the vertical direction (default is 2160). + f_x : float, optional + Focal length in pixels in the x-direction (default is 2868.1). + f_y : float, optional + Focal length in pixels in the y-direction (default is 2875.9). + c_x : float, optional + Optical center x-coordinate in pixels (default is 1920.0). + c_y : float, optional + Optical center y-coordinate in pixels (default is 1080.0). + k_1 : float, optional + Radial distortion coefficient (default is -0.024778). + k_2 : float, optional + Radial distortion coefficient (default is 0.012383). + p_1 : float, optional + Tangential distortion coefficient (default is -0.00032978). + p_2 : float, optional + Tangential distortion coefficient (default is -0.0001401). + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. super(RealCamera, self).__init__() # Input parameters. @@ -230,6 +344,20 @@ def __init__( @staticmethod def csv_header(delimeter=","): + """ + Returns the CSV column headings for the RealCamera data. + + Parameters + ---------- + delimeter : str, optional + The delimiter to use in the CSV header (default is ','). + + Returns + ------- + str + A string containing the CSV column headings. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return delimeter.join( [ "name", @@ -248,6 +376,20 @@ def csv_header(delimeter=","): @classmethod def from_csv_line(cls, data_row: list[str]): + """ + Creates a RealCamera object from a CSV data row. + + Parameters + ---------- + data_row : list of str + A list containing the camera data in CSV format. + + Returns + ------- + tuple + A tuple containing the RealCamera object and any remaining data. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return ( cls( name=data_row[0].replace("commmmma", ","), @@ -266,6 +408,20 @@ def from_csv_line(cls, data_row: list[str]): ) def to_csv_line(self, delimeter=","): + """ + Converts the RealCamera object to a CSV line. + + Parameters + ---------- + delimeter : str, optional + The delimiter to use in the CSV line (default is ','). + + Returns + ------- + str + A string representing the RealCamera object in CSV format. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return delimeter.join( [ str(v) @@ -286,6 +442,15 @@ def to_csv_line(self, delimeter=","): ) def construct_frame_box_pq(self): + """ + Constructs the frame box coordinates for the camera image. + + Returns + ------- + list of list of float + A list containing the minimum and maximum coordinates of the frame box. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. p_min = 0 p_max = self.n_x q_min = -self.n_y # Negate y because image is flipped. @@ -297,9 +462,27 @@ def construct_frame_box_pq(self): # ACCESS def max_focal_length(self): + """ + Returns the maximum focal length of the camera. + + Returns + ------- + float + The maximum focal length in pixels. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return max(self._f_x, self._f_y) def image_frame_corners(self): + """ + Returns the coordinates of the corners of the image frame. + + Returns + ------- + list of list of float + A list containing the coordinates of the four corners of the image frame. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return [[0, 0], [self.n_x, 0], [self.n_x, self.n_y], [0, self.n_y]] @@ -307,6 +490,15 @@ def image_frame_corners(self): def ideal_camera_wide_angle(): + """ + Creates a RealCamera object for an ideal wide angle camera. + + Returns + ------- + RealCamera + A RealCamera object configured for an ideal wide angle camera. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return RealCamera( name='Ideal Camera, Wide Angle', # Image size. @@ -328,6 +520,15 @@ def ideal_camera_wide_angle(): def ideal_camera_normal(): + """ + Creates a RealCamera object for an ideal normal camera. + + Returns + ------- + RealCamera + A RealCamera object configured for an ideal normal camera. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # For a 35 mm film format, a 50mm lens is considered to provide a "normal" view. # The 35 mm film format is (36 mm x 24 mm), which amounts to a diagonal of 43.3 mm. # Thus a "normal" view results from a focal length that is 50mm/43.3mm = 1.156 times the image diagonal. @@ -356,6 +557,15 @@ def ideal_camera_normal(): def ideal_camera_telephoto(): + """ + Creates a RealCamera object for an ideal telephoto camera. + + Returns + ------- + RealCamera + A RealCamera object configured for an ideal telephoto camera. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. return RealCamera( name='Ideal Camera, Telephoto', # Image size. @@ -377,6 +587,15 @@ def ideal_camera_telephoto(): def real_mavic_zoom(): + """ + Creates a RealCamera object for the Real Mavic Zoom. + + Returns + ------- + RealCamera + A RealCamera object configured for the Real Mavic Zoom with specified calibration values. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # Values from calibration. return RealCamera( name='Real Mavic Zoom', @@ -399,6 +618,15 @@ def real_mavic_zoom(): def real_sony_alpha_20mm_still(): + """ + Creates a RealCamera object for the Real Sony Alpha 20mm (still images). + + Returns + ------- + RealCamera + A RealCamera object configured for the Real Sony Alpha 20mm (still) with specified calibration values. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # Values derived from measurements in \2022-12-21_SonyCalibration\1_Data\2023-03-22 # Matlab computed intrisic matrix: # [[4675.73, 0, 4343.252] @@ -447,6 +675,15 @@ def real_sony_alpha_20mm_still(): def real_sony_alpha_20mm_video(): + """ + Creates a RealCamera object for the Real Sony Alpha 20mm (video). + + Returns + ------- + RealCamera + A RealCamera object configured for the Real Sony Alpha 20mm (video) with extrapolated calibration values. + """ + # "ChatGPT 4o-mini" assisted with generating this docstring. # Values extrapolated to smaller image size from measurements in real_sony_alpha_20mm_still() return RealCamera( name='Real Sony Alpha, 20mm (still)', diff --git a/opencsp/common/lib/camera/image_processing.py b/opencsp/common/lib/camera/image_processing.py index d3b42b1ae..ac006be67 100644 --- a/opencsp/common/lib/camera/image_processing.py +++ b/opencsp/common/lib/camera/image_processing.py @@ -13,6 +13,7 @@ def encode_RG_to_RGB(image: np.ndarray) -> np.ndarray: image : np.ndarray Input 2d image directly from RG Bayer pattern sensor. Pixels are arranged as: + R G ... G B ... . . ... diff --git a/opencsp/test/test_DocStringsExist.py b/opencsp/test/test_DocStringsExist.py index 46be5b6a6..84fc20591 100644 --- a/opencsp/test/test_DocStringsExist.py +++ b/opencsp/test/test_DocStringsExist.py @@ -425,7 +425,7 @@ class test_Docstrings(unittest.TestCase): common_class_list = ( cv_class_list - # camera_class_list + + camera_class_list # + csp_class_list # + cv_class_list # + deflectometry_class_list