Skip to content

Commit a3a86dd

Browse files
authored
Merge pull request #294 from bbean23/293-code-feature-spotanalysis-examples-35-bcs-image-on-tower-east-side
293 code feature spotanalysis examples 35 bcs image on tower east side
2 parents 8977071 + ef37292 commit a3a86dd

File tree

14 files changed

+997
-125
lines changed

14 files changed

+997
-125
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
# Virtual environment
1111
env/
1212

13+
# Configuration files
14+
experiment_settings.ini
15+
1316
# Ben's Sublime files.
1417
sftp-config.json
1518

contrib/common/lib/cv/annotations/RectangleAnnotations.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import matplotlib.axes
2-
import matplotlib.collections
3-
import matplotlib.patches
41
import numpy as np
52
import scipy.spatial.transform
63

@@ -11,8 +8,8 @@
118
import opencsp.common.lib.render.Color as color
129
import opencsp.common.lib.render.figure_management as fm
1310
import opencsp.common.lib.render.view_spec as vs
14-
import opencsp.common.lib.render.View3d as v3d
1511
import opencsp.common.lib.render_control.RenderControlAxis as rca
12+
import opencsp.common.lib.render_control.RenderControlFigureRecord as rcfr
1613
import opencsp.common.lib.render_control.RenderControlPointSeq as rcps
1714
import opencsp.common.lib.tool.log_tools as lt
1815

contrib/common/lib/cv/annotations/SpotWidthAnnotation.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import matplotlib.axes
21
import matplotlib.patches
32
import numpy as np
43
import scipy.spatial.transform
5-
import scipy.special
64

75
from opencsp.common.lib.cv.annotations.AbstractAnnotations import AbstractAnnotations
86
import opencsp.common.lib.geometry.Pxy as p2
97
import opencsp.common.lib.geometry.RegionXY as reg
108
import opencsp.common.lib.geometry.Vxy as v2
119

12-
import opencsp.common.lib.render.Color as color
13-
import opencsp.common.lib.render.figure_management as fm
14-
import opencsp.common.lib.render.view_spec as vs
15-
import opencsp.common.lib.render.View3d as v3d
16-
import opencsp.common.lib.render_control.RenderControlAxis as rca
17-
import opencsp.common.lib.render_control.RenderControlFigure as rcfg
1810
import opencsp.common.lib.render_control.RenderControlFigureRecord as rcfr
1911
import opencsp.common.lib.render_control.RenderControlSpotSize as rcss
2012

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import unittest
2+
3+
4+
class TestImportAnnotations(unittest.TestCase):
5+
def test_import(self):
6+
"""Tests that we can import the annotations without encountering a cyclic import error."""
7+
import contrib.common.lib.cv.annotations.MomentsAnnotation
8+
import contrib.common.lib.cv.annotations.RectangleAnnotations
9+
import contrib.common.lib.cv.annotations.SpotWidthAnnotation
10+
11+
12+
if __name__ == "__main__":
13+
unittest.main()

contrib/common/lib/cv/spot_analysis/image_processor/EnclosedEnergyImageProcessor.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ def __init__(
7979
"Error in EnclosedEnergyImageProcessor(): "
8080
+ f"enclosed_shape must be one of {allowed_shapes}, but is '{enclosed_shape}'",
8181
)
82+
if percentages_of_interest is not None:
83+
if not np.all([poi >= 0 and poi <= 1.0 for poi in percentages_of_interest]):
84+
lt.error_and_raise(
85+
ValueError,
86+
"Error in EnclosedEnergyImageProcessor(): "
87+
+ f"percentages_of_interest must be between 0 and 1 but {percentages_of_interest=}!",
88+
)
8289

8390
# use default values
8491
if enclosed_energy_style is None:
@@ -182,6 +189,9 @@ def calculate_enclosed_energy(self, image: CacheableImage, center_location: tupl
182189

183190
# Sanity checks
184191
assert len(enclosed_energy_sums) == max_radius + 1
192+
assert np.all(
193+
[enclosed_energy_sums[i] >= enclosed_energy_sums[i - 1] for i in range(1, len(enclosed_energy_sums))]
194+
)
185195

186196
if enclosed_energy_sums[-1] != total_energy:
187197
lt.error_and_raise(
@@ -192,7 +202,7 @@ def calculate_enclosed_energy(self, image: CacheableImage, center_location: tupl
192202

193203
return enclosed_energy_sums, example_enclosed_image
194204

195-
def build_enclosed_energy_plot(self, enclosed_energy_sums: list[int]) -> np.ndarray:
205+
def build_enclosed_energy_plot(self, enclosed_energy_sums: list[int]) -> tuple[np.ndarray, dict[float, int]]:
196206
"""
197207
Builds a plot of the enclosed energy around the central_locator of the input image.
198208
@@ -209,29 +219,35 @@ def build_enclosed_energy_plot(self, enclosed_energy_sums: list[int]) -> np.ndar
209219
-------
210220
plot_image: np.ndarray
211221
A np.ndarray containing the enclosed energy plot.
222+
percentages_of_interest_radiuses: dict[float,int]
223+
The radiuses (in pixels) at which the enclosed energy of
224+
interest percentages are first reached.
212225
"""
213226
# Determine the axes ranges
214227
if self.plot_x_limit_pixels > 0:
215228
x_range = self.plot_x_limit_pixels
216229
else:
217230
x_range = len(enclosed_energy_sums)
218231

219-
# Build the data as a fraction of the total
232+
# Build the data as a fraction of the total.
220233
pq_vals: list[tuple[int, int]] = []
221-
enclosed_energy_fractions: list[float] = []
234+
enclosed_energy_fractions: list[tuple[int, int, float]] = []
222235
total_enclosed_energy = enclosed_energy_sums[-1]
223236
for radius in range(len(enclosed_energy_sums)):
224-
enclosed_energy_fractions.append(enclosed_energy_sums[radius] / total_enclosed_energy)
237+
enclosed_energy_fractions.append(
238+
tuple([radius, enclosed_energy_sums[radius], enclosed_energy_sums[radius] / total_enclosed_energy])
239+
)
225240
pq_vals.append(tuple([radius, enclosed_energy_fractions[radius - 1]]))
226241
assert len(enclosed_energy_fractions) == len(enclosed_energy_sums)
227242

228243
# Pad the plot for the given plot_x_limit_pixels, if any is given
229244
for radius in range(len(pq_vals) + 1, x_range + 1):
230-
pq_vals.append(tuple(radius, enclosed_energy_fractions[-1]))
245+
pq_vals.append(tuple(radius, enclosed_energy_fractions[-1][2]))
231246

232247
# Limit the plot to the x range
248+
print(f"{len(pq_vals)=}")
233249
pq_vals = pq_vals[: x_range + 1]
234-
assert len(pq_vals) == x_range + 1
250+
assert len(pq_vals) == x_range, f"{len(pq_vals)=} != {x_range=}"
235251

236252
# Create a new figure for the plot
237253
figure_control = rcfg.RenderControlFigure()
@@ -247,15 +263,19 @@ def build_enclosed_energy_plot(self, enclosed_energy_sums: list[int]) -> np.ndar
247263
)
248264

249265
# Draw the percentages of interest
266+
percentages_of_interest_radii: dict[float, int] = {}
250267
if self.percentages_of_interest is not None:
251268
for poi in sorted(self.percentages_of_interest):
252-
closest_radius, closest_dist = 0, np.abs(poi - enclosed_energy_fractions[0])
253-
for radius, fraction in enumerate(enclosed_energy_fractions):
269+
closest_radius, closest_dist = 0, np.abs(poi - enclosed_energy_fractions[0][2])
270+
for radius, enclosed_energy_sum, fraction in enclosed_energy_fractions:
254271
dist = np.abs(fraction - poi)
255272
if dist < closest_dist:
256273
closest_radius = radius
257274
closest_dist = dist
275+
258276
lt.info(f"Percentage of interest {poi} is at radius {closest_radius}")
277+
percentages_of_interest_radii[poi] = closest_radius
278+
259279
fig_record.view.draw_pq_list(
260280
[(0, poi), (closest_radius, poi)], style=self.percentages_of_interest_style.measured
261281
)
@@ -273,7 +293,7 @@ def build_enclosed_energy_plot(self, enclosed_energy_sums: list[int]) -> np.ndar
273293
# close the figure
274294
fig_record.close()
275295

276-
return plot_image
296+
return plot_image, percentages_of_interest_radii
277297

278298
def _execute(self, operable: SpotAnalysisOperable, is_last: bool) -> list[SpotAnalysisOperable]:
279299
# Calculate the enclosed energy around the central_locator of the image
@@ -283,11 +303,11 @@ def _execute(self, operable: SpotAnalysisOperable, is_last: bool) -> list[SpotAn
283303
)
284304

285305
# Generate a visual plot of the enclosed energy
286-
enclosed_energy_plot = self.build_enclosed_energy_plot(enclosed_energy_sums)
306+
enclosed_energy_plot, percentages_of_interest_radii = self.build_enclosed_energy_plot(enclosed_energy_sums)
287307

288308
# Build the new operable
289309
notes = copy.copy(operable.image_processor_notes)
290-
notes.append(tuple([self.name, [str(v) for v in enclosed_energy_sums]]))
310+
notes.append(tuple([self.name, tuple([percentages_of_interest_radii, [str(v) for v in enclosed_energy_sums]])]))
291311
algorithm_images = copy.copy(operable.algorithm_images)
292312
algorithm_images[self] = [CacheableImage.from_single_source(example_enclosed_energy_image)]
293313
vis_images = copy.copy(operable.visualization_images)

contrib/common/lib/cv/spot_analysis/image_processor/PowerpointImageProcessor.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def __init__(
137137
lt.error_and_raise(
138138
TypeError,
139139
"Error in PowerpointImageProcessor.__init__(): "
140-
+ f'"processors_per_slide" is a {type(processors_per_slide)}, but should be a list of lists',
140+
+ f'"processors_per_slide" is a {processors_per_slide.__name__}, but should be a list of lists',
141141
)
142142
else:
143143
for i, processor_set in enumerate(processors_per_slide):
@@ -147,23 +147,23 @@ def __init__(
147147
lt.error_and_raise(
148148
TypeError,
149149
"Error in PowerpointImageProcessor.__init__(): "
150-
+ f'"processors_per_slide[{i}]" is a {type(processor_set)}, but should be a list!',
150+
+ f'"processors_per_slide[{i}]" is a {processor_set.__name__}, but should be a list!',
151151
)
152152
for j, processor_sel in enumerate(processor_set):
153-
processor_sel_type = type(processor_sel)
154153
# normalize to use ProcOrImg
155154
if not isinstance(processor_sel, ProcessorSelector):
156155
if not isinstance(processor_sel, tuple):
157156
processor_sel = tuple([processor_sel])
158157
try:
159158
processors_per_slide[i][j] = ProcessorSelector.from_tuple(processor_sel)
160159
except TypeError:
160+
processor_sel_name = processor_sel.__class__.__name__
161161
lt.error_and_raise(
162162
TypeError,
163163
"Error in PowerpointImageProcessor.__init__(): "
164-
+ f'"processors_per_slide[{i}][{j}]" is a {processor_sel_type}, '
165-
+ f"but should be an {type(AbstractSpotAnalysisImageProcessor)}, "
166-
+ f"{type(CacheableImage)}, or image-like!",
164+
+ f'"processors_per_slide[{i}][{j}]" is a {processor_sel_name}, '
165+
+ f"but should be an {AbstractSpotAnalysisImageProcessor.__name__}, "
166+
+ f"{CacheableImage.__name__}, or image-like!",
167167
)
168168

169169
# register the rest of the input arguments

contrib/common/lib/cv/spot_analysis/image_processor/SpotWidthImageProcessor.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import copy
22
import dataclasses
33
import json
4+
from typing import TYPE_CHECKING
45

56
import cv2 as cv
67
import numpy as np
78

8-
from contrib.common.lib.cv.annotations.SpotWidthAnnotation import SpotWidthAnnotation
9-
from contrib.common.lib.cv.annotations.MomentsAnnotation import MomentsAnnotation
109
from opencsp.common.lib.cv.CacheableImage import CacheableImage
1110
import opencsp.common.lib.cv.image_reshapers as ir
1211
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisOperable import SpotAnalysisOperable
@@ -20,6 +19,11 @@
2019
import opencsp.common.lib.tool.image_tools as it
2120
import opencsp.common.lib.tool.log_tools as lt
2221

22+
if TYPE_CHECKING:
23+
# don't import at runtime to avoid cyclic imports
24+
from contrib.common.lib.cv.annotations.SpotWidthAnnotation import SpotWidthAnnotation
25+
from contrib.common.lib.cv.annotations.MomentsAnnotation import MomentsAnnotation
26+
2327

2428
class SpotWidthImageProcessor(AbstractSpotAnalysisImageProcessor):
2529
"""
@@ -68,6 +72,9 @@ def locate_half_max_pixels(self, image: np.ndarray) -> tuple[np.ndarray, np.ndar
6872
return np.where(image == half_max)
6973

7074
def find_centroid(self, coords: tuple[np.ndarray, np.ndarray]) -> p2.Pxy:
75+
# import here to avoid cyclic imports
76+
from contrib.common.lib.cv.annotations.MomentsAnnotation import MomentsAnnotation
77+
7178
# create an image with 1s at the specified coordinates
7279
y_max = np.max(coords[0])
7380
x_max = np.max(coords[1])
@@ -215,6 +222,9 @@ def fwhm(self, image_name: str, image: np.ndarray) -> tuple[p2.Pxy, float, float
215222
return centroid, long_axis_width, long_axis_rotation, long_axis_center, orthogonal_axis_width, algorithm_image
216223

217224
def _execute(self, operable: SpotAnalysisOperable, is_last: bool) -> list[SpotAnalysisOperable]:
225+
# import here to avoid cyclic imports
226+
from contrib.common.lib.cv.annotations.SpotWidthAnnotation import SpotWidthAnnotation
227+
218228
image = operable.primary_image.nparray
219229
annotations = copy.copy(operable.annotations)
220230
notes = copy.copy(operable.image_processor_notes)

0 commit comments

Comments
 (0)