diff --git a/opencsp/common/lib/cv/fiducials/AbstractFiducials.py b/opencsp/common/lib/cv/fiducials/AbstractFiducials.py index ae67a41b5..190181a8f 100644 --- a/opencsp/common/lib/cv/fiducials/AbstractFiducials.py +++ b/opencsp/common/lib/cv/fiducials/AbstractFiducials.py @@ -23,72 +23,114 @@ class AbstractFiducials(ABC): def __init__(self, style: rcps.RenderControlPointSeq = None, pixels_to_meters: Callable[[p2.Pxy], v3.Vxyz] = None): """ + Initializes the AbstractFiducials with a specified rendering style and pixel-to-meter conversion function. + Parameters ---------- - style : RenderControlPointSeq, optional - How to render this fiducial when using the defaul render_to_plot() method. By default rcps.default(). + style : rcps.RenderControlPointSeq, optional + How to render this fiducial when using the default render_to_plot() method. Defaults to rcps.default(). pixels_to_meters : Callable[[p2.Pxy], v3.Vxyz], optional Conversion function to get the physical point in space for the given x/y position information. Used in the default self.scale implementation. A good implementation of this function will correct for many factors such as relative camera position and camera distortion. For extreme accuracy, this will also account for non-uniformity in the target surface. Defaults to a simple 1 meter per pixel model. """ + # "ChatGPT 4o" assisted with generating this docstring. self.style = style if style is not None else rcps.default() self.pixels_to_meters = pixels_to_meters @abstractmethod def get_bounding_box(self, index=0) -> reg.RegionXY: - """The X/Y bounding box(es) of this instance, in pixels.""" + """ + Get the X/Y bounding box(es) of this instance, in pixels. + + Parameters + ---------- + index : int, optional + The index of the fiducial for which to retrieve the bounding box. Defaults to 0. + + Returns + ------- + reg.RegionXY + The bounding box of the fiducial. + """ + + # "ChatGPT 4o" assisted with generating this docstring. @property @abstractmethod def origin(self) -> p2.Pxy: - """The origin point(s) of this instance, in pixels.""" + """ + Get the origin point(s) of this instance, in pixels. + + Returns + ------- + p2.Pxy + The origin point(s) of the fiducial. + """ + + # "ChatGPT 4o" assisted with generating this docstring. @property @abstractmethod def rotation(self) -> scipy.spatial.transform.Rotation: """ - The pointing of the normal vector(s) of this instance. - This is relative to the camera's reference frame, where x is positive - to the right, y is positive down, and z is positive in (away from the - camera). + Get the pointing of the normal vector(s) of this instance. + + Returns + ------- + scipy.spatial.transform.Rotation + The rotation of the fiducial relative to the camera's reference frame. - This can be used to describe the forward transformation from the - camera's perspective. For example, an aruco marker whose origin is in - the center of the image and is facing towards the camera could have the - rotation:: + Notes + ----- + This can be used to describe the forward transformation from the camera's perspective. For example, an ArUco + marker whose origin is in the center of the image and is facing towards the camera could have the rotation + defined as: - Rotation.from_euler('y', np.pi) + Rotation.from_euler('y', np.pi) - If that same aruco marker was also placed upside down, then it's - rotation could be:: + If that same ArUco marker was also placed upside down, then its rotation could be defined as: - Rotation.from_euler( - 'yz', - [ [np.pi, 0], - [0, np.pi] ] - ) + Rotation.from_euler( + 'yz', + [[np.pi, 0], + [0, np.pi]] + ) - Not that this just describes rotation, and not the translation. We call - the rotation and translation together the orientation. + Note that this just describes rotation, and not the translation. We call the rotation and translation together + the orientation. """ + # "ChatGPT 4o" assisted with generating this docstring. + @property @abstractmethod def size(self) -> list[float]: - """The scale(s) of this fiducial, in pixels, relative to its longest axis. - For example, if the fiducial is a square QR-code and is oriented tangent - to the camera, then the scale will be the number of pixels from one - corner to the other.""" # TODO is this a good definition? + """ + Get the scale(s) of this fiducial, in pixels, relative to its longest axis. + + Returns + ------- + list[float] + The sizes of the fiducial in pixels. For example, if the fiducial is a square QR-code and is oriented tangent + to the camera, then the scale will be the number of pixels from one corner to the other. + """ + + # "ChatGPT 4o" assisted with generating this docstring. @property def scale(self) -> list[float]: """ - The scale(s) of this fiducial, in meters, relative to its longest axis. - This can be used to determine the distance and rotation of the - fiducial relative to the camera. + Get the scale(s) of this fiducial, in meters, relative to its longest axis. + + Returns + ------- + list[float] + The scales of the fiducial in meters. This can be used to determine the distance and rotation of the + fiducial relative to the camera. """ + # "ChatGPT 4o" assisted with generating this docstring. ret = [] for i in range(len(self.origin)): @@ -102,9 +144,13 @@ def scale(self) -> list[float]: return ret def _render(self, axes: matplotlib.axes.Axes): - """ - Called from render(). The parameters are always guaranteed to be set. - """ + # Render the fiducial on the given axes. + # + # Parameters + # ---------- + # axes : matplotlib.axes.Axes + # The axes on which to render the fiducial. + axes.scatter( self.origin.x, self.origin.y, @@ -123,19 +169,36 @@ def render(self, axes: matplotlib.axes.Axes = None): Parameters ---------- - axes: matplotlib.axes.Axes, optional - The plot to render to. Uses the active plot if None. Default is None. + axes : matplotlib.axes.Axes, optional + The plot to render to. Uses the active plot if None. Defaults to None. """ + # "ChatGPT 4o" assisted with generating this docstring. if axes is None: axes = plt.gca() self._render(axes) def render_to_image(self, image: np.ndarray) -> np.ndarray: """ - Renders this fiducial to the a new image on top of the given image. + Renders this fiducial to a new image on top of the given image. - The default implementation creates a new matplotlib plot, and then renders to it with self.render_to_plot(). + The default implementation creates a new matplotlib plot, and then renders to it with self.render(). + + Parameters + ---------- + image : np.ndarray + The original image to which the fiducial will be rendered. + + Returns + ------- + np.ndarray + The updated image with the fiducial rendered on top. + + Raises + ------ + Exception + If an error occurs during the rendering process. """ + # "ChatGPT 4o" assisted with generating this docstring. # Create the figure to plot to dpi = 300 width = image.shape[1] diff --git a/opencsp/common/lib/cv/fiducials/BcsFiducial.py b/opencsp/common/lib/cv/fiducials/BcsFiducial.py index 9584e128e..d9bc45ed1 100644 --- a/opencsp/common/lib/cv/fiducials/BcsFiducial.py +++ b/opencsp/common/lib/cv/fiducials/BcsFiducial.py @@ -11,50 +11,121 @@ class BcsFiducial(AbstractFiducials): + """ + Fiducial for indicating where the BCS target is in an image. + """ + def __init__( self, origin_px: p2.Pxy, radius_px: float, style: rcb.RenderControlBcs = None, pixels_to_meters: float = 0.1 ): """ - Fiducial for indicating where the BCS target is in an image. + Initializes the BcsFiducial with the specified origin, radius, style, and pixel-to-meter conversion. Parameters ---------- - origin_px : Pxy - The center point of the BCS target, in pixels + origin_px : p2.Pxy + The center point of the BCS target, in pixels. radius_px : float - The radius of the BCS target, in pixels - style : RenderControlBcs, optional - The rendering style, by default None + The radius of the BCS target, in pixels. + style : rcb.RenderControlBcs, optional + The rendering style for the fiducial. Defaults to None. pixels_to_meters : float, optional - A simple conversion method for how many meters a pixel represents, for use in scale(). by default 0.1 + A conversion factor for how many meters a pixel represents, for use in scale(). Defaults to 0.1. """ + # "ChatGPT 4o" assisted with generating this docstring. + super().__init__(style=style) self.origin_px = origin_px self.radius_px = radius_px self.pixels_to_meters = pixels_to_meters def get_bounding_box(self, index=0) -> reg.RegionXY: + """ + Get the bounding box of the BCS target. + + Parameters + ---------- + index : int, optional + The index of the target for which to retrieve the bounding box. Defaults to 0. + + Returns + ------- + reg.RegionXY + The bounding box as a RegionXY object. + + Notes + ----- + The bounding box is calculated based on the origin and radius of the BCS target. + """ + # "ChatGPT 4o" assisted with generating this docstring. x1, x2 = self.origin.x[0] - self.radius_px, self.origin.x[0] + self.radius_px y1, y2 = self.origin.y[0] - self.radius_px, self.origin.y[0] + self.radius_px return reg.RegionXY(loop.LoopXY.from_rectangle(x1, y1, x2 - x1, y2 - y1)) @property def origin(self) -> p2.Pxy: + """ + Get the origin of the BCS fiducial. + + Returns + ------- + p2.Pxy + The center point of the BCS target in pixels. + """ + # "ChatGPT 4o" assisted with generating this docstring. return self.origin_px @property def rotation(self) -> scipy.spatial.transform.Rotation: + """ + Get the rotation of the BCS fiducial. + + Raises + ------ + NotImplementedError + If the rotation is not yet implemented for BcsFiducial. + """ + # "ChatGPT 4o" assisted with generating this docstring. raise NotImplementedError("rotation is not yet implemented for PointFiducials") @property def size(self) -> list[float]: + """ + Get the size of the BCS fiducial. + + Returns + ------- + list[float] + A list containing the diameter of the BCS target (radius * 2). + """ + # "ChatGPT 4o" assisted with generating this docstring. return [self.radius_px * 2] @property def scale(self) -> list[float]: + """ + Get the scale of the BCS fiducial. + + Returns + ------- + list[float] + A list containing the scaled size of the BCS target in meters. + """ + # "ChatGPT 4o" assisted with generating this docstring. return [self.size * self.pixels_to_meters] def _render(self, axes: matplotlib.axes.Axes): + # Render the BCS fiducial on the given axes. + # + # Parameters + # ---------- + # axes : matplotlib.axes.Axes + # The axes on which to render the BCS fiducial. + # + # Notes + # ----- + # This method adds a circle and a marker to the axes based on the style defined for the fiducial. + # "ChatGPT 4o" assisted with generating this doc. if self.style.linestyle is not None: circ = matplotlib.patches.Circle( self.origin.data.tolist(), diff --git a/opencsp/common/lib/cv/fiducials/PointFiducials.py b/opencsp/common/lib/cv/fiducials/PointFiducials.py index 190292fdf..f5cce1852 100644 --- a/opencsp/common/lib/cv/fiducials/PointFiducials.py +++ b/opencsp/common/lib/cv/fiducials/PointFiducials.py @@ -7,31 +7,106 @@ class PointFiducials(AbstractFiducials): + """ + A collection of pixel locations where points of interest are located in an image. + """ + def __init__(self, style: rcps.RenderControlPointSeq = None, points: p2.Pxy = None): """ - A collection of pixel locations where points of interest are located in an image. + Initializes the PointFiducials with a specified style and points. + + Parameters + ---------- + style : rcps.RenderControlPointSeq, optional + The rendering style for the control points. Defaults to None. + points : p2.Pxy, optional + The pixel locations of the points of interest. Defaults to None. """ + # "ChatGPT 4o" assisted with generating this docstring. super().__init__(style) self.points = points def get_bounding_box(self, index=0) -> reg.RegionXY: + """ + Get the bounding box for a specific point by index. + + Parameters + ---------- + index : int, optional + The index of the point for which to retrieve the bounding box. Defaults to 0. + + Returns + ------- + reg.RegionXY + The bounding box as a RegionXY object. + + Notes + ----- + This method is untested and may require validation. + """ + # "ChatGPT 4o" assisted with generating this docstring. # TODO untested return reg.RegionXY.from_vertices(p2.Pxy((self.points.x[index], self.points.y[index]))) @property def origin(self) -> p2.Pxy: + """ + Get the origin of the fiducials. + + Returns + ------- + p2.Pxy + The pixel locations of the points of interest. + """ + # "ChatGPT 4o" assisted with generating this docstring. + return return self.points @property def rotation(self) -> scipy.spatial.transform.Rotation: + """ + Get the rotation of the fiducials. + + Raises + ------ + NotImplementedError + If the orientation is not yet implemented for PointFiducials. + """ + # "ChatGPT 4o" assisted with generating this docstring. raise NotImplementedError("Orientation is not yet implemented for PointFiducials") @property def size(self) -> list[float]: + """ + Get the size of the fiducials. + + Returns + ------- + list[float] + A list of sizes for each point. Currently returns a list of zeros. + + Notes + ----- + This property is untested and may require validation. + """ + # "ChatGPT 4o" assisted with generating this docstring. # TODO untested return [0] * len(self.points) @property def scale(self) -> list[float]: + """ + Get the scale of the fiducials. + + Returns + ------- + list[float] + A list of scales for each point. Currently returns a list of zeros. + + Notes + ----- + This property is untested and may require validation. + """ + # "ChatGPT 4o" assisted with generating this docstring. # TODO untested return [0] * len(self.points) diff --git a/opencsp/test/test_DocStringsExist.py b/opencsp/test/test_DocStringsExist.py index 4e0b3c6d6..8f56d3cef 100644 --- a/opencsp/test/test_DocStringsExist.py +++ b/opencsp/test/test_DocStringsExist.py @@ -178,6 +178,9 @@ def test_docstrings_exist_for_methods(): opencsp.common.lib.cv.annotations.AbstractAnnotations, opencsp.common.lib.cv.annotations.HotspotAnnotation, opencsp.common.lib.cv.annotations.PointAnnotations, + opencsp.common.lib.cv.fiducials.AbstractFiducials, + opencsp.common.lib.cv.fiducials.BcsFiducial, + opencsp.common.lib.cv.fiducials.PointFiducials, ] deflectometry_class_list = [