-
Notifications
You must be signed in to change notification settings - Fork 11
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
Support SofastFixed point light #66
Changes from 14 commits
db2a586
6afecc8
d138c54
7a62ea7
c7a8dd1
6e88398
af0eefe
ced6c2f
8c9a943
5f8ef89
bcb9ca5
74c2a99
a4f0f78
f4a7ed1
2842d3b
d4221a7
7f95e0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from os.path import join, dirname | ||
|
||
import cv2 as cv | ||
|
||
import opencsp.app.sofast.lib.image_processing as ip | ||
from opencsp.app.sofast.lib.MeasurementSofastFixed import MeasurementSofastFixed | ||
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.render.figure_management as fm | ||
import opencsp.common.lib.render_control.RenderControlAxis as rca | ||
import opencsp.common.lib.render_control.RenderControlFigure as rcfg | ||
import opencsp.common.lib.tool.file_tools as ft | ||
import opencsp.common.lib.tool.log_tools as lt | ||
|
||
|
||
def example_create_measurement_file_from_image(): | ||
"""Example that creates a SofastFixed measurement file from an image. The image has | ||
a point LED light near origin dot. | ||
1. Load image | ||
2. Define measurement parameters | ||
3. Find location of origin point | ||
4. Create measurement object | ||
5. Save measurement as HDF5 file | ||
6. Plot image with annotated origin dot | ||
""" | ||
# General setup | ||
# ============= | ||
dir_save = join(dirname(__file__), 'data/output/measurement_file') | ||
ft.create_directories_if_necessary(dir_save) | ||
lt.logger(join(dir_save, 'log.txt'), lt.log.INFO) | ||
|
||
# 1. Load image | ||
# ============= | ||
file_meas = join(opencsp_code_dir(), 'test/data/sofast_fixed/data_measurement/measurement_facet.h5') | ||
measurement_old = MeasurementSofastFixed.load_from_hdf(file_meas) | ||
image = measurement_old.image | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it's silly, but it might make this more useful as an example if there were some commented out code for loading the image using opencv or pillow, as if you have an image jpg file instead of a measurement h5 file. # image = cv2.imread(file_jpg, cv2.IMREAD_GRAYSCALE) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like it! Added! |
||
|
||
# 2. Define measurement parameters | ||
# ================================ | ||
v_measure_point_facet = Vxyz((0, 0, 0)) # meters | ||
dist_optic_screen = 10.008 # meters | ||
name = 'NSTTF Facet' | ||
|
||
# 3. Find location of origin point | ||
# ================================ | ||
|
||
# Find point light | ||
params = cv.SimpleBlobDetector_Params() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idea time! It would be nice if you could retrieve these parameters from image_processing with some values such as params = ip.dot_target_blob_params(minDistBetweenBlobs=2, minArea=3, maxArea=30)
def dot_target_blob_params(
minDistBetweenBlobs=2,
filterByArea=True,
minArea=3,
maxArea=30,
filterByCircularity=True,
minCircularity=0.8,
filterByConvexity=False,
filterByInertia=False,
):
params = cv.SimpleBlobDetector_Params()
params.minDistBetweenBlobs = minDistBetweenBlobs
params.filterByArea = filterByArea
params.minArea = minArea
params.maxArea = maxArea
params.filterByCircularity = filterByCircularity
params.minCircularity = minCircularity
params.filterByConvexity = filterByConvexity
params.filterByInertia = filterByInertia
return params To be clear, not necessary for this PR, and not necessary generally. Just an idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an interesting idea! There are other inputs that SimpleBlobDetector_Params takes that I haven't typically used. Perhaps have a |
||
params.minDistBetweenBlobs = 2 | ||
params.filterByArea = True | ||
params.minArea = 3 | ||
params.maxArea = 30 | ||
params.filterByCircularity = True | ||
params.minCircularity = 0.8 | ||
params.filterByConvexity = False | ||
params.filterByInertia = False | ||
origin = ip.detect_blobs_inverse(image, params) | ||
|
||
# Check only one origin point was found | ||
if len(origin) != 1: | ||
lt.error_and_raise(ValueError, f'Expected 1 origin point, found {len(origin):d}.') | ||
|
||
# 4. Create measurement object | ||
# ============================ | ||
measurement = MeasurementSofastFixed(image, v_measure_point_facet, dist_optic_screen, origin, name=name) | ||
|
||
# 5. Save measurement as HDF5 file | ||
# ================================ | ||
measurement.save_to_hdf(join(dir_save, 'measurement.h5')) | ||
|
||
# 6. Plot image with annotated origin dot | ||
# ======================================= | ||
image_annotated = ip.detect_blobs_inverse_annotate(image, params) | ||
|
||
figure_control = rcfg.RenderControlFigure(tile_array=(1, 1), tile_square=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unified render control style! 👍 |
||
axis_control = rca.image() | ||
fig_record = fm.setup_figure(figure_control, axis_control, title='Annotated Origin Point') | ||
fig_record.axis.imshow(image_annotated) | ||
fig_record.save(dir_save, 'annotated_image_with_origin_point', 'png') | ||
|
||
|
||
if __name__ == '__main__': | ||
example_create_measurement_file_from_image() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,7 +64,9 @@ def calc_mask_raw( | |
raise ValueError('Not enough distinction between dark and light pixels in mask images.') | ||
|
||
# Calculate minimum between two peaks | ||
# fmt: off | ||
idx_hist_min = np.argmin(hist[peaks[0] : peaks[1]]) + peaks[0] | ||
# fmt: on | ||
|
||
# Find index of histogram that is "hist_thresh" the way between the min and max | ||
thresh_hist_min = edges[idx_hist_min + 1] | ||
|
@@ -422,8 +424,8 @@ def rectangle_loop_from_two_points(p1: Vxy, p2: Vxy, d_ax: float, d_perp: float) | |
# Calculate axial and perpendicular directions | ||
v_axial = (p2 - p1).normalize() | ||
|
||
R = np.array([[0, 1], [-1, 0]]) | ||
v_perp = v_axial.rotate(R) | ||
rot_mat_90 = np.array([[0, 1], [-1, 0]]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🥳 |
||
v_perp = v_axial.rotate(rot_mat_90) | ||
|
||
# Create points | ||
points = [] | ||
|
@@ -440,7 +442,8 @@ def rectangle_loop_from_two_points(p1: Vxy, p2: Vxy, d_ax: float, d_perp: float) | |
|
||
|
||
def detect_blobs(image: np.ndarray, params: cv.SimpleBlobDetector_Params) -> Vxy: | ||
"""Detects blobs in image | ||
"""Detects blobs in image. Blobs are defined as local dark regions in | ||
neighboring light background. | ||
|
||
Parameters | ||
---------- | ||
|
@@ -462,8 +465,35 @@ def detect_blobs(image: np.ndarray, params: cv.SimpleBlobDetector_Params) -> Vxy | |
return Vxy(np.array(pts).T) | ||
|
||
|
||
def detect_blobs_inverse(image: np.ndarray, params: cv.SimpleBlobDetector_Params) -> Vxy: | ||
"""Detect blobs in image. Blobs are defined as local light regions in | ||
neighboring dark background. | ||
|
||
NOTE: This definition of blobs is the inverse as in `image_processing.detect_blobs()` | ||
|
||
Parameters | ||
---------- | ||
image : np.ndarray | ||
Input image, uint8 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Presumably this should be a 2d image with a single color channel? AKA:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! Changed! |
||
params : cv.SimpleBlobDetector_Params | ||
Blob parameters | ||
|
||
Returns | ||
------- | ||
Vxy | ||
Centroids of blobs | ||
""" | ||
keypoints = _detect_blobs_keypoints(image.max() - image, params) | ||
|
||
pts = [] | ||
for pt in keypoints: | ||
pts.append(pt.pt) | ||
return Vxy(np.array(pts).T) | ||
|
||
|
||
def detect_blobs_annotate(image: np.ndarray, params: cv.SimpleBlobDetector_Params) -> np.ndarray: | ||
"""Detects blobs in image | ||
"""Detects blobs in image and annotates locations. Blobs are defined as local dark regions in | ||
neighboring light background. | ||
|
||
Parameters | ||
---------- | ||
|
@@ -481,6 +511,28 @@ def detect_blobs_annotate(image: np.ndarray, params: cv.SimpleBlobDetector_Param | |
return cv.drawKeypoints(image, keypoints, np.array([]), (0, 0, 255), cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) | ||
|
||
|
||
def detect_blobs_inverse_annotate(image: np.ndarray, params: cv.SimpleBlobDetector_Params) -> np.ndarray: | ||
"""Detects blobs in image and annotates locations. Blobs are defined as local light regions in | ||
neighboring dark background. | ||
|
||
NOTE: This definition of blobs is the inverse as in `image_processing.detect_blobs()` | ||
|
||
Parameters | ||
---------- | ||
image : np.ndarray | ||
Input image, uint8 | ||
params : cv.SimpleBlobDetector_Params | ||
Blob parameters | ||
|
||
Returns | ||
------- | ||
ndarray | ||
Annotated image of blobs | ||
""" | ||
keypoints = _detect_blobs_keypoints(image.max() - image, params) | ||
return cv.drawKeypoints(image, keypoints, np.array([]), (0, 0, 255), cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) | ||
|
||
|
||
def _detect_blobs_keypoints(image: np.ndarray, params: cv.SimpleBlobDetector_Params) -> list[cv.KeyPoint]: | ||
"""Detects blobs in image | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,13 @@ | |
from os.path import join | ||
import unittest | ||
|
||
import cv2 as cv | ||
import numpy as np | ||
from scipy.spatial.transform import Rotation | ||
|
||
from opencsp.app.sofast.lib.ImageCalibrationScaling import ImageCalibrationScaling | ||
from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe as Measurement | ||
from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe | ||
from opencsp.app.sofast.lib.MeasurementSofastFixed import MeasurementSofastFixed | ||
from opencsp.app.sofast.lib.ParamsSofastFringe import ParamsSofastFringe | ||
from opencsp.common.lib.camera.Camera import Camera | ||
import opencsp.app.sofast.lib.image_processing as ip | ||
|
@@ -36,6 +38,11 @@ def setUpClass(cls): | |
cls.data_file_measurement_ensemble = join(dir_sofast_fringe, 'data_measurement/measurement_ensemble.h5') | ||
cls.data_file_calibration = join(dir_sofast_fringe, 'data_measurement/image_calibration.h5') | ||
|
||
# Sofast fixed dot image | ||
cls.sofast_fixed_meas = MeasurementSofastFixed.load_from_hdf( | ||
join(opencsp_code_dir(), 'test/data/sofast_fixed/data_measurement/measurement_facet.h5') | ||
) | ||
|
||
def test_calc_mask_raw(self): | ||
"""Tests image_processing.calc_mask_raw()""" | ||
|
||
|
@@ -187,7 +194,7 @@ def test_unwrap_phase(self): | |
'DataSofastCalculation/image_processing/facet_000/mask_processed', | ||
] | ||
data = load_hdf5_datasets(datasets, self.data_file_facet) | ||
measurement = Measurement.load_from_hdf(self.data_file_measurement_facet) | ||
measurement = MeasurementSofastFringe.load_from_hdf(self.data_file_measurement_facet) | ||
calibration = ImageCalibrationScaling.load_from_hdf(self.data_file_calibration) | ||
|
||
measurement.calibrate_fringe_images(calibration) | ||
|
@@ -228,6 +235,50 @@ def test_calculate_active_pixel_pointing_vectors(self): | |
# Test | ||
np.testing.assert_allclose(data['u_pixel_pointing_facet'], u_pixel_pointing_optic) | ||
|
||
def test_detect_blobs(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unit tests! 🥳 |
||
params = cv.SimpleBlobDetector_Params() | ||
params.minDistBetweenBlobs = 2 | ||
params.filterByArea = True | ||
params.minArea = 3 | ||
params.maxArea = 30 | ||
params.filterByCircularity = True | ||
params.minCircularity = 0.8 | ||
params.filterByConvexity = False | ||
params.filterByInertia = False | ||
|
||
blobs = ip.detect_blobs(self.sofast_fixed_meas.image, params) | ||
|
||
self.assertEqual(len(blobs), 3761, 'Test number of blobs') | ||
np.testing.assert_allclose( | ||
blobs[0].data.squeeze(), | ||
np.array([672.20654297, 1138.20654297]), | ||
rtol=0, | ||
atol=1e-6, | ||
err_msg='First blob pixel location does not match expected', | ||
) | ||
|
||
def test_detect_blobs_inverse(self): | ||
params = cv.SimpleBlobDetector_Params() | ||
params.minDistBetweenBlobs = 2 | ||
params.filterByArea = True | ||
params.minArea = 3 | ||
params.maxArea = 30 | ||
params.filterByCircularity = True | ||
params.minCircularity = 0.8 | ||
params.filterByConvexity = False | ||
params.filterByInertia = False | ||
|
||
blobs = ip.detect_blobs_inverse(self.sofast_fixed_meas.image, params) | ||
|
||
self.assertEqual(len(blobs), 1, 'Test number of blobs') | ||
np.testing.assert_allclose( | ||
blobs[0].data.squeeze(), | ||
np.array([960.590515, 796.387695]), | ||
rtol=0, | ||
atol=1e-6, | ||
err_msg='blob pixel location does not match expected', | ||
) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't considered also including this style of visual separator. Makes this really easy to read!