-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add BcsFiducial, BcsLocatorImageProcessor, RenderControlBcs
- Loading branch information
Showing
5 changed files
with
251 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import matplotlib.axes | ||
import matplotlib.patches | ||
|
||
from opencsp.common.lib.cv.AbstractFiducials import AbstractFiducials | ||
import opencsp.common.lib.geometry.LoopXY as loop | ||
import opencsp.common.lib.geometry.RegionXY as reg | ||
import opencsp.common.lib.geometry.Pxy as p2 | ||
import opencsp.common.lib.geometry.Vxyz as v3 | ||
import opencsp.common.lib.render_control.RenderControlBcs as rcb | ||
|
||
|
||
class BcsFiducial(AbstractFiducials): | ||
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. | ||
Parameters | ||
---------- | ||
origin_px : 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 | ||
pixels_to_meters : float, optional | ||
A simple conversion method for how many meters a pixel represents, for use in scale(). by default 0.1 | ||
""" | ||
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: | ||
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: | ||
return self.origin_px | ||
|
||
@property | ||
def orientation(self) -> v3.Vxyz: | ||
return v3.Vxyz([0, 0, 0]) | ||
|
||
@property | ||
def size(self) -> list[float]: | ||
return [self.radius_px * 2] | ||
|
||
@property | ||
def scale(self) -> list[float]: | ||
return [self.size * self.pixels_to_meters] | ||
|
||
def _render(self, axes: matplotlib.axes.Axes): | ||
if self.style.linestyle is not None: | ||
circ = matplotlib.patches.Circle( | ||
self.origin.data.tolist(), | ||
self.radius_px, | ||
color=self.style.color, | ||
linestyle=self.style.linestyle, | ||
linewidth=self.style.linewidth, | ||
fill=False, | ||
) | ||
axes.add_patch(circ) | ||
|
||
if self.style.marker is not None: | ||
axes.scatter( | ||
self.origin.x, | ||
self.origin.y, | ||
linewidth=self.style.linewidth, | ||
marker=self.style.marker, | ||
s=self.style.markersize, | ||
c=self.style.markerfacecolor, | ||
edgecolor=self.style.markeredgecolor, | ||
) |
107 changes: 107 additions & 0 deletions
107
opencsp/common/lib/cv/spot_analysis/image_processor/BcsLocatorImageProcessor.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import copy | ||
import dataclasses | ||
|
||
import cv2 as cv | ||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
from opencsp.common.lib.cv.CacheableImage import CacheableImage | ||
from opencsp.common.lib.cv.fiducials.BcsFiducial import BcsFiducial | ||
from opencsp.common.lib.cv.spot_analysis.SpotAnalysisOperable import SpotAnalysisOperable | ||
from opencsp.common.lib.cv.spot_analysis.image_processor.AbstractSpotAnalysisImageProcessor import ( | ||
AbstractSpotAnalysisImagesProcessor, | ||
) | ||
from opencsp.common.lib.cv.spot_analysis.image_processor.AnnotationImageProcessor import AnnotationImageProcessor | ||
from opencsp.common.lib.cv.spot_analysis.image_processor.ConvolutionImageProcessor import ConvolutionImageProcessor | ||
import opencsp.common.lib.geometry.Pxy as p2 | ||
import opencsp.common.lib.opencsp_path.opencsp_root_path as orp | ||
import opencsp.common.lib.render_control.RenderControlBcs as rcb | ||
import opencsp.common.lib.render_control.RenderControlPointSeq as rcps | ||
import opencsp.common.lib.tool.file_tools as ft | ||
import opencsp.common.lib.tool.log_tools as lt | ||
|
||
|
||
class BcsLocatorImageProcessor(AbstractSpotAnalysisImagesProcessor): | ||
def __init__(self, min_radius_px=30, max_radius_px=150): | ||
""" | ||
Locates the BCS by identifying a circle in the image. | ||
It is recommended this this processor be used after ConvolutionImageProcessor(kernel='gaussian'). | ||
Parameters | ||
---------- | ||
min_radius_px : int, optional | ||
Minimum radius of the BSC circle, in pixels. By default 50 | ||
max_radius_px : int, optional | ||
Maximum radius of the BSC circle, in pixels. By default 300 | ||
""" | ||
super().__init__(self.__class__.__name__) | ||
|
||
self.min_radius_px = min_radius_px | ||
self.max_radius_px = max_radius_px | ||
|
||
def _execute(self, operable: SpotAnalysisOperable, is_last: bool) -> list[SpotAnalysisOperable]: | ||
image = operable.primary_image.nparray.squeeze() | ||
if image.ndim > 2: | ||
lt.error_and_raise( | ||
RuntimeError, | ||
"Error in BcsLocatorImageProcessor._execute(): image must be grayscale (2 dimensions), but " | ||
+ f"the shape of the image is {image.shape} for '{operable.primary_image_source_path}'", | ||
) | ||
|
||
# find all possible matches | ||
method = cv.HOUGH_GRADIENT | ||
accumulator_pixel_size: float = 1 | ||
circles: np.ndarray | None = cv.HoughCircles( | ||
image, | ||
method, | ||
accumulator_pixel_size, # resolution for accumulators | ||
minDist=self.min_radius_px, # distance between circles | ||
param1=70, # upper threshold to Canny edge detector | ||
param2=20, # minimum accumulations count for matching circles | ||
minRadius=self.min_radius_px, | ||
maxRadius=self.max_radius_px, | ||
) | ||
|
||
# opencv returns circles in order from best to worst matches, choose the first circle (best match) | ||
circle: BcsFiducial = None | ||
if circles is not None: | ||
circle_arr = circles[0][0] | ||
center = p2.Pxy([circle_arr[0], circle_arr[1]]) | ||
radius = circle_arr[2] | ||
circle = BcsFiducial(center, radius, style=rcb.thin(color='m')) | ||
|
||
# assign to the operable | ||
new_found_fiducials = copy.copy(operable.found_fiducials) | ||
if circle != None: | ||
new_found_fiducials.append(circle) | ||
ret = dataclasses.replace(operable, found_fiducials=new_found_fiducials) | ||
return [ret] | ||
|
||
|
||
if __name__ == "__main__": | ||
import os | ||
|
||
indir = ft.norm_path( | ||
os.path.join( | ||
orp.opencsp_scratch_dir(), | ||
"solar_noon/dev/2023-05-12_SpringEquinoxMidSummerSolstice/2_Data/BCS_data/Measure_01/raw_images", | ||
) | ||
) | ||
image_file = ft.norm_path(os.path.join(indir, "20230512_114854.74 5E09_000_880_2890 Raw.JPG")) | ||
|
||
style = rcps.RenderControlPointSeq(markersize=10) | ||
operable = SpotAnalysisOperable(CacheableImage(source_path=image_file)) | ||
|
||
processor0 = ConvolutionImageProcessor(kernel='gaussian', diameter=3) | ||
processor1 = BcsLocatorImageProcessor() | ||
processor2 = AnnotationImageProcessor() | ||
|
||
result0 = processor0.process_image(operable)[0] | ||
result1 = processor1.process_image(result0)[0] | ||
result2 = processor2.process_image(result1)[0] | ||
img = result2.primary_image.nparray | ||
|
||
plt.figure() | ||
plt.imshow(img) | ||
plt.show(block=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from opencsp.common.lib.render_control.RenderControlPointSeq import RenderControlPointSeq | ||
|
||
|
||
class RenderControlBcs(RenderControlPointSeq): | ||
def __init__( | ||
self, | ||
linestyle: str | None = '-', | ||
linewidth: float = 1, | ||
color: str = 'b', | ||
marker: str | None = '.', | ||
markersize: float = 8, | ||
markeredgecolor: str | None = None, | ||
markeredgewidth: float | None = None, | ||
markerfacecolor: str | None = None, | ||
): | ||
""" | ||
Render control for the Beam Characterization System target. | ||
Controls style of the point marker and circle marker of the BCS. | ||
Parameters | ||
---------- | ||
linestyle : str, optional | ||
How to draw the line for the circle around the BCS. One of '-', '--', '-.', ':', '' or None (see RenderControlPointSeq for a description). By default '-' | ||
linewidth : int, optional | ||
Width of the line for the circle around the BCS. By default 1 | ||
color : str, optional | ||
Color for the circle around the BCS. One of bgrcmykw (see RenderControlPointSeq for a description). By default 'b' | ||
marker : str, optional | ||
Shape of the center BCS marker. One of .,ov^<>12348sp*hH+xXDd|_ or None. By default '.' | ||
markersize : int, optional | ||
Size of the center BCS marker. By default 8 | ||
markeredgecolor : str, optional | ||
Defaults to color above if not set. By default None | ||
markeredgewidth : float, optional | ||
Defaults to linewidth if not set. By default None | ||
markerfacecolor : str, optional | ||
Defaults to color above if not set. By default None | ||
""" | ||
super().__init__( | ||
linestyle=linestyle, | ||
linewidth=linewidth, | ||
color=color, | ||
marker=marker, | ||
markersize=markersize, | ||
markeredgecolor=markeredgecolor, | ||
markeredgewidth=markeredgewidth, | ||
markerfacecolor=markerfacecolor, | ||
) | ||
|
||
|
||
# COMMON CASES | ||
|
||
|
||
def default(marker='.', color='b', linewidth=1, markersize=8) -> RenderControlBcs: | ||
""" | ||
What to draw if no particular preference is expressed. | ||
""" | ||
return RenderControlBcs(linewidth=linewidth, color=color, marker=marker, markersize=markersize) | ||
|
||
|
||
def thin(marker='.', color='b', linewidth=0.3, markersize=5) -> RenderControlBcs: | ||
return RenderControlBcs(color=color, marker=marker, linewidth=linewidth, markersize=markersize) |