Skip to content

Commit

Permalink
Merge pull request #66 from braden6521/sofast_fixed_point_light
Browse files Browse the repository at this point in the history
Added SofastFixed processing for origin point detection using a point light.
  • Loading branch information
braden6521 authored Apr 4, 2024
2 parents f7e798c + 7f95e0a commit c3128f0
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 11 deletions.
88 changes: 88 additions & 0 deletions example/sofast_fixed/example_measurement_from_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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
# =============
# Load image from MeasurementSofastFixed file
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

# Load image from existing image file
# image = cv2.imread(file_jpg, cv2.IMREAD_GRAYSCALE)
# image = imageio.imread(file_png)

# 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()
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)
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()
5 changes: 4 additions & 1 deletion opencsp/app/sofast/lib/BlobIndex.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ def _nearest_unassigned_idx_from_xy_point_direction(
dists_axis = v_search.dot(points_rel) # Distance of points along search axis
dists_perp = np.abs(v_perp.dot(points_rel)) # Distance of points from line
# Make mask of valid points
mask = np.logical_and(dists_axis > 0, dists_perp / dists_axis <= self.search_perp_axis_ratio)
mask_dist_positive = dists_axis > 0
dists_axis[dists_axis == 0] = np.nan
mask_ratio = (dists_perp / dists_axis) <= self.search_perp_axis_ratio
mask = np.logical_and(mask_dist_positive, mask_ratio)
# Check there are points to find
if mask.sum() == 0:
return False, (None, None)
Expand Down
2 changes: 0 additions & 2 deletions opencsp/app/sofast/lib/CalibrateDisplayShape.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
"""

from dataclasses import dataclass
import os

import cv2 as cv
import h5py
import matplotlib.pyplot as plt
import numpy as np
from numpy import ndarray
Expand Down
64 changes: 58 additions & 6 deletions opencsp/app/sofast/lib/image_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]])
v_perp = v_axial.rotate(rot_mat_90)

# Create points
points = []
Expand All @@ -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
----------
Expand All @@ -462,13 +465,40 @@ 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
2D input image, single color channel, NxM or NxMx1, uint8
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
----------
image : np.ndarray
Input image, uint8
2D input image, single color channel, NxM or NxMx1, uint8
params : cv.SimpleBlobDetector_Params
Blob parameters
Expand All @@ -481,13 +511,35 @@ 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
2D input image, single color channel, NxM or NxMx1, 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
Parameters
----------
image : np.ndarray
Input image, uint8
2D input image, single color channel, NxM or NxMx1, uint8
params : cv.SimpleBlobDetector_Params
Blob parameters
Expand Down
55 changes: 53 additions & 2 deletions opencsp/app/sofast/test/test_image_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()"""

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
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()
Binary file not shown.
Binary file not shown.

0 comments on commit c3128f0

Please sign in to comment.