Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sofast fixed cal update #193

Merged
merged 19 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8ed62e7
Added function to plot all corners of facet ensemble in DefinitionEns…
braden6521 Dec 2, 2024
e32afca
Added debug class to BlobIndex and added more visualization functions.
braden6521 Dec 2, 2024
e21e563
Fixed error in CalibrateSofastFixedDots.py where y/x axes were switch…
braden6521 Dec 2, 2024
e5819f5
Added Blob Index debug option to ProcessSofastFixed.
braden6521 Dec 2, 2024
04f9fa0
Updated find_blobs in PSF. Can filter by loopXY
braden6521 Dec 2, 2024
960db44
Removes NaNs in points in PSF
braden6521 Dec 2, 2024
c2e5c96
Added shape_ys_data_mat and pts_index_to_mat_index to BlobIndex
braden6521 Dec 3, 2024
e41f0dc
Added CalculationBlobAssignment data class to ProcessSofastFixed. Sav…
braden6521 Dec 3, 2024
2a5905a
Update SofastConfiguration class to calculate average xy sample dista…
braden6521 Dec 3, 2024
6b5f058
Updated SofastConfiguration to plot screen points for SofastFixed cases.
braden6521 Dec 3, 2024
c864aac
Changed find_blobs() to private _find_blobs() in PSF. Modified proces…
braden6521 Dec 3, 2024
4ffbf37
Moved processing single facet geometry data classes out of _process_o…
braden6521 Dec 3, 2024
7b23662
Improved single facet processing code readability in PSF.
braden6521 Dec 3, 2024
847b5cb
Updated plotting of Sofast Fringe/Fixed setups.
braden6521 Dec 3, 2024
ff55f1a
Moved common geometry processing code into separate common function.
braden6521 Dec 3, 2024
520ebcd
Added documentation to ParamsOpticGeometry parameters.
braden6521 Dec 10, 2024
7ed0195
Added more context to warnings in PSF.
braden6521 Dec 10, 2024
c2788fc
Added AbstractPlotHandler to BlobIndex.
braden6521 Dec 13, 2024
f26cde1
Added units to new docs.
braden6521 Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 97 additions & 6 deletions opencsp/app/sofast/lib/BlobIndex.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from opencsp.common.lib.geometry.Vxy import Vxy
from opencsp.common.lib.geometry.LoopXY import LoopXY
from opencsp.common.lib.render.lib.AbstractPlotHandler import AbstractPlotHandler
from opencsp.common.lib.tool import log_tools as lt


Expand All @@ -19,7 +20,30 @@ class Step(Enum):
LEFT_OR_UP = -1


class BlobIndex:
class DebugBlobIndex:
"""Debug class for BlobIndex

Parameters
----------
debug_active : bool
Flag to turn on debugging, default false
figures : list[Figure]
Container to hold debug figures
bg_image : ndarray
2d ndarray, by default None. Background image (i.e. the image with the blobs present)
to plot in debugging plots. If None, background image is not plotted.
name : str
Name of current instance to prepend to plot titles
"""

def __init__(self):
self.debug_active: bool = False
self.figures: list = []
self.bg_image: np.ndarray = None
self.name: str = ''

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding a destructor method that closes all the figures held onto by this instance. Maybe consider extending https://github.com/sandialabs/OpenCSP/blob/develop/opencsp/common/lib/render/lib/AbstractPlotHandler.py for this functionality.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


class BlobIndex(AbstractPlotHandler):
"""Class containing blob indexing algorithms to assign indices to blobs in a rough grid pattern.
X/Y axes correspond to image axes; +x is to right, +y is down. Class takes in points (in units
of pixels) that have been previously found with a blob detector and attempts to assign all found
Expand Down Expand Up @@ -51,6 +75,8 @@ def __init__(self, points: Vxy, x_min: int, x_max: int, y_min: int, y_max: int)
x_min/x_max/y_min/y_max : int
Expected min/max of blob indices in x/y directions
"""
super().__init__()

self._points = points

self._num_pts = len(points)
Expand All @@ -70,14 +96,18 @@ def __init__(self, points: Vxy, x_min: int, x_max: int, y_min: int, y_max: int)
"""Ratio of point distances: (perpendicular to axis) / (along axis) used to search for points. Default 3.0"""
self.apply_filter: bool = False
"""To filter bad points (experimental, not implemented yet). Default False"""
self.debug: DebugBlobIndex = DebugBlobIndex()
"""BlobIndex debug object"""
self.shape_yx_data_mat: tuple[int, int] = (y_max - y_min + 1, x_max - x_min + 1)
"""The yx shape of the internal data matrix that holds the point pixel locations and xy indices"""

self._offset_x = -x_min # index
self._offset_y = -y_min # index
idx_x_vec = np.arange(x_min, x_max + 1) # index
idx_y_vec = np.arange(y_min, y_max + 1) # index
self._idx_x_mat, self._idx_y_mat = np.meshgrid(idx_x_vec, idx_y_vec) # index
self._points_mat = np.zeros((y_max - y_min + 1, x_max - x_min + 1, 2)) * np.nan # pixels
self._point_indices_mat = np.zeros((y_max - y_min + 1, x_max - x_min + 1)) * np.nan # index
self._points_mat = np.zeros(self.shape_yx_data_mat + (2,)) * np.nan # pixels
self._point_indices_mat = np.zeros(self.shape_yx_data_mat) * np.nan # index

def _get_assigned_point_indices(self) -> np.ndarray[int]:
"""Returns found point indices"""
Expand Down Expand Up @@ -238,7 +268,6 @@ def _find_nearest_in_direction(
mask = np.logical_and(unassigned_deltas.y > 0, unassigned_deltas.y > (2 * np.abs(unassigned_deltas.x)))
idx_x_out = idx_x
idx_y_out = idx_y + 1
# Down
elif direction == 'down':
mask = np.logical_and(unassigned_deltas.y < 0, -unassigned_deltas.y > (2 * np.abs(unassigned_deltas.x)))
idx_x_out = idx_x
Expand Down Expand Up @@ -406,10 +435,42 @@ def run(self, pt_known: Vxy, x_known: int, y_known: int) -> None:
x/y_known : int
XY indies of known points
"""
# Plot all input blobs
if self.debug.debug_active:
fig = plt.figure()
self._register_plot(fig)
if self.debug.bg_image is not None:
plt.imshow(self.debug.bg_image)
self.plot_all_points()
plt.title('1: All found dots in image' + self.debug.name)
self.debug.figures.append(fig)

# Assign center point
self._assign_center(pt_known, x_known, y_known)

# Plot assigned known center point
if self.debug.debug_active:
fig = plt.figure()
self._register_plot(fig)
if self.debug.bg_image is not None:
plt.imshow(self.debug.bg_image)
self.plot_assigned_points_labels(labels=True)
plt.title('2: Locations of known, center dot' + self.debug.name)
self.debug.figures.append(fig)

# Find 3x3 core point block
self._find_3x3_center_block(x_known, y_known)

# Plot 3x3 core block
if self.debug.debug_active:
fig = plt.figure()
self._register_plot(fig)
if self.debug.bg_image is not None:
plt.imshow(self.debug.bg_image)
self.plot_assigned_points_labels(labels=True)
plt.title('3: Locations of 3x3 center block' + self.debug.name)
self.debug.figures.append(fig)

# Extend rows
prev_num_unassigned = self._num_unassigned()
for idx in range(self.max_num_iters):
Expand All @@ -429,8 +490,18 @@ def run(self, pt_known: Vxy, x_known: int, y_known: int) -> None:
break
prev_num_unassigned = cur_num_unassigned

def plot_points_labels(self, labels: bool = False) -> None:
"""Plots points and labels
# Plot all found points
if self.debug.debug_active:
fig = plt.figure()
self._register_plot(fig)
if self.debug.bg_image is not None:
plt.imshow(self.debug.bg_image)
self.plot_points_connections()
plt.title('4: Row assignments' + self.debug.name)
self.debug.figures.append(fig)

def plot_assigned_points_labels(self, labels: bool = False) -> None:
"""Plots all assigned points with [optionally] labels

Parameters
----------
Expand All @@ -444,6 +515,10 @@ def plot_points_labels(self, labels: bool = False) -> None:
):
plt.text(*pt.data, f'({x:.0f}, {y:.0f})')

def plot_all_points(self) -> None:
"""Plots all points"""
plt.scatter(*self._points.data, color='red')

def plot_points_connections(self, labels: bool = False) -> None:
"""Plots points and connections for rows/collumns

Expand Down Expand Up @@ -497,6 +572,22 @@ def get_data(self) -> tuple[Vxy, Vxy]:
points = Vxy((x_pts[mask_assigned], y_pts[mask_assigned]))
return points, indices

def pts_index_to_mat_index(self, pts_index: Vxy) -> tuple[np.ndarray, np.ndarray]:
"""Returns corresponding matrix indices (see self.get_data_mat) given point
indices (assigned x/y indies of points.)

Parameters
----------
pts_index : Vxy
Assigned point xy indices, length N.

Returns
-------
x, y
Length N 1d arrays of corresponding data matrix indices (see self.get_data_mat)
"""
return pts_index.x - self._offset_x, pts_index.y - self._offset_y

def get_data_in_region(self, loop: LoopXY) -> tuple[Vxy, Vxy]:
"""Returns found points and indices within given region

Expand Down
2 changes: 1 addition & 1 deletion opencsp/app/sofast/lib/CalibrateSofastFixedDots.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def __init__(
self._dot_image_points_indices: Vxy
self._dot_image_points_indices_x: ndarray
self._dot_image_points_indices_y: ndarray
self._dot_points_xyz_mat = np.ndarray((x_max - x_min + 1, y_max - y_min + 1, 3)) * np.nan
self._dot_points_xyz_mat = np.ndarray((y_max - y_min + 1, x_max - x_min + 1, 3)) * np.nan
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own edification, what does multiplying by np.nan accomplish?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those matrices get filled in with data as the algorithm finds dots. Since some values may actually be zero, we need some other value to define "no data." NaN seemed appropriate.

self._num_dots: int
self._rots_cams: list[Rotation] = []
self._vecs_cams: list[Vxyz] = []
Expand Down
10 changes: 10 additions & 0 deletions opencsp/app/sofast/lib/DefinitionEnsemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from copy import deepcopy
import json

import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial.transform import Rotation

from opencsp.app.sofast.lib.DefinitionFacet import DefinitionFacet
from opencsp.common.lib.geometry.Vxyz import Vxyz
from opencsp.common.lib.tool import hdf5_tools

Expand Down Expand Up @@ -164,6 +166,14 @@ def load_from_hdf(cls, file: str, prefix: str = '') -> 'DefinitionEnsemble':
v_centroid_ensemble = Vxyz(data['v_centroid_ensemble'])
return cls(v_facet_locations, r_facet_ensemble, ensemble_perimeter, v_centroid_ensemble)

def plot_facet_corners_xy_proj(self, facets: list[DefinitionFacet]) -> None:
"""Plots the xy projection of all facet corners given accompanying facet definitions"""
for idx_facet, (facet, R, T) in enumerate(zip(facets, self.r_facet_ensemble, self.v_facet_locations)):
facet: DefinitionFacet
corners_cur_facet = facet.v_facet_corners
corners_cur_ensemble: Vxyz = corners_cur_facet.rotate(R) + T
plt.scatter(corners_cur_ensemble.x, corners_cur_ensemble.y, label=f'Facet {idx_facet:d}')


def _Vxyz_to_dict(V: Vxyz) -> dict:
d = {'x': V.x.tolist(), 'y': V.y.tolist(), 'z': V.z.tolist()}
Expand Down
9 changes: 9 additions & 0 deletions opencsp/app/sofast/lib/ParamsOpticGeometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ class ParamsOpticGeometry(hdf5_tools.HDF5_IO_Abstract):
"""Parameter dataclass for processing optic geometry"""

perimeter_refine_axial_search_dist: float = 50.0
"""The length of the search box (along the search direction) to use when finding optic
perimeter. Units pixels. Default 50.0"""
perimeter_refine_perpendicular_search_dist: float = 50.0
"""The half-width of the search box (perpendicular to the search direction) to use when finding
optic perimeter. Units pixels. Default 50.0"""
facet_corns_refine_step_length: float = 10.0
"""The length of the search box (along the search direction) to use when refining facet corner
locations (when processing a facet ensemble). Units pixels. Default 10.0"""
facet_corns_refine_perpendicular_search_dist: float = 10.0
"""The half-width of the search box (perpendicular to the search direction) to use when
refining facet corner locations (when processing a facet ensemble). Units pixels. Default 10.0"""
facet_corns_refine_frac_keep: float = 0.5
"""The fraction of pixels to consider within search box when finding optic edges. Default 0.5"""

def save_to_hdf(self, file: str, prefix: str = ''):
"""Saves data to given HDF5 file. Data is stored in PREFIX + ParamsOpticGeometry/...
Expand Down
Loading
Loading