Skip to content

Commit

Permalink
add BcsFiducial, BcsLocatorImageProcessor, RenderControlBcs
Browse files Browse the repository at this point in the history
  • Loading branch information
bbean23 committed Apr 24, 2024
1 parent 895d436 commit 6ee50ac
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 9 deletions.
11 changes: 2 additions & 9 deletions contrib/app/SpotAnalysis/PeakFlux.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ def __init__(self, indir: str, outdir: str, experiment_name: str, settings_path_
ImageType.PRIMARY: lambda operable, operables: "off" not in operable.primary_image_source_path,
ImageType.NULL: lambda operable, operables: "off" in operable.primary_image_source_path,
}
# max_pixel_value_locator = AnnotationImageProcessor.AnnotationEngine(
# feature_locator=lambda operable: np.argmax(operable.primary_image.ndarray),
# color='k'
# )
# bcs_locator = AnnotationImageProcessor.AnnotationEngine(
# feature_locator=lambda operable: self.bcs_pixel,
# color='k'
# )

self.image_processors: list[AbstractSpotAnalysisImagesProcessor] = [
CroppingImageProcessor(*self.crop_box),
Expand All @@ -67,9 +59,10 @@ def __init__(self, indir: str, outdir: str, experiment_name: str, settings_path_
SupportingImagesCollectorImageProcessor(supporting_images_map),
NullImageSubtractionImageProcessor(),
ConvolutionImageProcessor(kernel="box", diameter=3),
BcsLocatorImageProcessor(),
PopulationStatisticsImageProcessor(initial_min=0, initial_max=255),
FalseColorImageProcessor(),
# AnnotationImageProcessor(max_pixel_value_locator, bcs_locator)
AnnotationImageProcessor(),
]
self.spot_analysis = sa.SpotAnalysis(
experiment_name, self.image_processors, save_dir=outdir, save_overwrite=True
Expand Down
77 changes: 77 additions & 0 deletions opencsp/common/lib/cv/fiducials/BcsFiducial.py
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,
)
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)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from opencsp.common.lib.cv.spot_analysis.image_processor.AverageByGroupImageProcessor import (
AverageByGroupImageProcessor,
)
from opencsp.common.lib.cv.spot_analysis.image_processor.BcsLocatorImageProcessor import BcsLocatorImageProcessor
from opencsp.common.lib.cv.spot_analysis.image_processor.ConvolutionImageProcessor import ConvolutionImageProcessor
from opencsp.common.lib.cv.spot_analysis.image_processor.CroppingImageProcessor import CroppingImageProcessor
from opencsp.common.lib.cv.spot_analysis.image_processor.EchoImageProcessor import EchoImageProcessor
Expand All @@ -32,6 +33,7 @@
'AbstractSpotAnalysisImagesProcessor',
'AnnotationImageProcessor',
'AverageByGroupImageProcessor',
'BcsLocatorImageProcessor',
'ConvolutionImageProcessor',
'CroppingImageProcessor',
'EchoImageProcessor',
Expand Down
63 changes: 63 additions & 0 deletions opencsp/common/lib/render_control/RenderControlBcs.py
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)

0 comments on commit 6ee50ac

Please sign in to comment.