-
Notifications
You must be signed in to change notification settings - Fork 30
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
replace opencv-python
with scikit-image
for snowball detection
#138
base: main
Are you sure you want to change the base?
replace opencv-python
with scikit-image
for snowball detection
#138
Conversation
Contour-finding also needs to be ported from
def find_circles(dqplane, bitmask, min_area):
# Using an input DQ plane this routine will find the groups of pixels with at least the minimum
# area and return a list of the minimum enclosing circle parameters.
pixels = np.bitwise_and(dqplane, bitmask)
if ELLIPSE_PACKAGE == 'opencv-python':
contours, hierarchy = cv.findContours(pixels, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
bigcontours = [con for con in contours if cv.contourArea(con) >= min_area]
circles = [cv.minEnclosingCircle(con) for con in bigcontours]
else:
# raise ModuleNotFoundError(ELLIPSE_PACKAGE_WARNING)
raise ModuleNotFoundError('`opencv-python` required for this functionality') |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #138 +/- ##
==========================================
- Coverage 85.18% 83.99% -1.19%
==========================================
Files 35 37 +2
Lines 6797 7211 +414
==========================================
+ Hits 5790 6057 +267
- Misses 1007 1154 +147 ☔ View full report in Codecov by Sentry. |
As described in #126 (comment), there are some differences in the methods used to construct ellipses between the two packages which result in different image masks. Perhaps a warning should make this more clear.
|
349d600
to
ca9758c
Compare
b73fd2d
to
0c3d1ad
Compare
expanding the ellipse test case to a more oblong shape reveals a few edge cases in the |
e7e7235
to
8793d4a
Compare
tests/test_jump.py
Outdated
@@ -40,23 +45,25 @@ def test_find_simple_circle(): | |||
plane[2, 3] = DQFLAGS['JUMP_DET'] | |||
plane[2, 1] = DQFLAGS['JUMP_DET'] | |||
circle = find_circles(plane, DQFLAGS['JUMP_DET'], 1) | |||
assert circle[0][0] == pytest.approx((2, 2)) | |||
assert circle[0][1] == pytest.approx(1.0, 1e-3) |
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.
here is where the fit circle from the scikit-image
contour differs from the opencv-python
circle; the radius is 1.5 instead of 1
Currently, the |
we should also get input from INS on whether this alternative is acceptable |
f5b745e
to
6775cf3
Compare
6775cf3
to
bbc4608
Compare
cd1e6d2
to
7499170
Compare
I removed |
scikit-image
for ellipse functionsopencv-python
with scikit-image
for ellipse functions and snowball detection
b651c39
to
4d811c5
Compare
4d811c5
to
6f77bc0
Compare
2ceb67d
to
2e89556
Compare
opencv-python
with scikit-image
for ellipse functions and snowball detectionopencv-python
with scikit-image
for snowball detection
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.
the formatter made some irrelevant edits, so here are the relevant changes to this PR
src/stcal/jump/jump.py
Outdated
ellipse_mask = skimage.draw.ellipse( | ||
r=round(ceny), | ||
c=round(cenx), | ||
r_radius=round(axis1/2), | ||
c_radius=round(axis2/2), | ||
shape=image.shape, | ||
rotation=alpha, | ||
) | ||
image[*ellipse_mask, 2] = 22 |
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.
replaces cv.ellipse
with a mask derived from skimage.draw.ellipse
, then applies Blue 22 to the image array (why is the blue channel set to 22 if it's discarded right after? can this just be a simple mask?)
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.
No real reason. It's hack to create a mask of the ellipse. Sorry that's it is confusing.
src/stcal/jump/jump.py
Outdated
ellipse_mask = skimage.draw.ellipse( | ||
r=round(ceny), | ||
c=round(cenx), | ||
r_radius=round(ellipse[1][0] / 2), | ||
c_radius=round(ellipse[1][1] / 2), | ||
shape=persist_image.shape, | ||
rotation=alpha, | ||
) | ||
persist_image[*ellipse_mask, 2] = 22 |
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.
same as above; why is this blue?
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.
Same reason, a hack.
center = (round(ceny), round(cenx)) | ||
axes = (round(axis1 / 2), round(axis2 / 2)) | ||
rr, cc = skimage.draw.ellipse(*center, *axes, shape=image.shape, rotation=alpha) | ||
image[rr, cc, 2] = 4 |
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.
is there a reason why this is rounded to the nearest integer?
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.
If I remember right the routine is expecting integers. My guess was this is just an interface to C code.
Eventually we have to more to integers to mask the pixels anyway.
def minimum_bounding_rectangle(points: np.ndarray) -> np.ndarray: | ||
""" | ||
Find the smallest bounding rectangle for a set of points. | ||
Returns a set of points representing the corners of the bounding box. | ||
https://stackoverflow.com/questions/13542855/algorithm-to-find-the-minimum-area-rectangle-for-given-points-in-order-to-comput | ||
|
||
:param points: an nx2 matrix of coordinates | ||
:rval: an nx2 matrix of coordinates | ||
""" | ||
pi2 = np.pi / 2.0 | ||
|
||
# get the convex hull for the points | ||
hull_points = points[ConvexHull(points).vertices] | ||
|
||
# calculate edge angles | ||
edges = np.zeros((len(hull_points) - 1, 2)) | ||
edges = hull_points[1:] - hull_points[:-1] | ||
|
||
angles = np.zeros(len(edges)) | ||
angles = np.arctan2(edges[:, 1], edges[:, 0]) | ||
|
||
angles = np.abs(np.mod(angles, pi2)) | ||
angles = np.unique(angles) | ||
|
||
# find rotation matrices | ||
# XXX both work | ||
rotations = np.vstack( | ||
[np.cos(angles), np.cos(angles - pi2), np.cos(angles + pi2), np.cos(angles)] | ||
).T | ||
# rotations = np.vstack([ | ||
# np.cos(angles), | ||
# -np.sin(angles), | ||
# np.sin(angles), | ||
# np.cos(angles)]).T | ||
rotations = rotations.reshape((-1, 2, 2)) | ||
|
||
# apply rotations to the hull | ||
rot_points = np.dot(rotations, hull_points.T) | ||
|
||
# find the bounding points | ||
min_x = np.nanmin(rot_points[:, 0], axis=1) | ||
max_x = np.nanmax(rot_points[:, 0], axis=1) | ||
min_y = np.nanmin(rot_points[:, 1], axis=1) | ||
max_y = np.nanmax(rot_points[:, 1], axis=1) | ||
|
||
# find the box with the best area | ||
areas = (max_x - min_x) * (max_y - min_y) | ||
best_idx = np.argmin(areas) | ||
|
||
# return the best box | ||
x1 = max_x[best_idx] | ||
x2 = min_x[best_idx] | ||
y1 = max_y[best_idx] | ||
y2 = min_y[best_idx] | ||
r = rotations[best_idx] | ||
|
||
rval = np.zeros((4, 2)) |
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.
this function replaces shapely.geometry.Polygon.minimum_bounding_box
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.
Remember that the goal is to generate an ellipse. The ellipse can be specified by the minimum bounding box. I have to say that the format that CV uses for the bounding ellipse is very confusing. Hopefully, your version will be easier to understand.
def area_of_polygon(xy: np.ndarray) -> float: | ||
""" | ||
apply shoelace algorithm on collection of xy vertex pairs | ||
https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates | ||
""" | ||
return 0.5 * np.abs( | ||
np.dot(xy[:, 0], np.roll(xy[:, 1], 1)) - np.dot(xy[:, 1], np.roll(xy[:, 0], 1)) | ||
) |
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.
this function replaces shapely.geometry.Polygon.area
) | ||
|
||
|
||
def find_circles(dqplane: np.ndarray, bitmask: np.ndarray, min_area: float) -> list[Circle]: |
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.
added type hints here
ellipses = [ | ||
( | ||
tuple(np.mean(rectangle[[0, 2], :], axis=0)), | ||
tuple(np.hypot(*np.diff(rectangle[[0, 1, 2], :], axis=0))), | ||
np.degrees(np.arctan2(*np.flip(np.diff(rectangle[[3, 0], :], axis=0)[0]))), | ||
) | ||
for rectangle in rectangles | ||
] |
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.
this is where the axis differences comes from; the minor and major radii are larger than those from opencv
by 1.5 (?) units
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.
Well, that's odd. The major and minor axis are what they are. Have you tried an input ellipse and see if you get the same ellipse out?
a264bf9
to
f7786d9
Compare
to summarize, two tests still show differences between the two methods; |
Sorry about that! I'll update the comments to be more descriptive |
To use your first simple example. I find that if I give the radius for draw.disk as 4.0001 I get the same image as cv2. So it wouldn't surprise me that in one the test is an < test and the other <=. Or something like that. Adding a small epsilon may do it. |
Closes spacetelescope/jwst#7409
This PR
adds fallback functions forreplaces ellipse construction usingscikit-image
and, removingshapely
opencv-python
entirely. Thescikit-image
+methods produce slightly different contours, ellipses, and circles thanshapely
opencv-python
.Checklist
CHANGES.rst
(either inBug Fixes
orChanges to API
)