Fix ±1px shift in render_instance_mask by using round() instead of int()#693
Fix ±1px shift in render_instance_mask by using round() instead of int()#693
Conversation
eta/core/image.py
Outdated
| tlx, tly, width, height = bounding_box.coords_in( | ||
| frame_size=frame_size, shape=shape, img=img | ||
| ) | ||
| w, h = to_frame_size(frame_size=frame_size, shape=shape, img=img) |
There was a problem hiding this comment.
Looks like we need to follow the rabbit hole another layer deeper and apply the fix directly in the coords_in() methods.
I'm sure it wasn't intentional to use the floor-based rounding we get via int(). int(round()) is a more accurate way of converting to pixel coordinates for all use cases.
6cee9b4 to
4509eca
Compare
|
@brimoor moved the fix to |
|
@brimoor checking in - this is ready for review whenever you get a chance. |
swheaton
left a comment
There was a problem hiding this comment.
Seems right to me. Not sure why the 1.0 was in there previously
brimoor
left a comment
There was a problem hiding this comment.
I did a quick search of the codebase and there are other cases of using int() to round when we possibly want int(round()).
Also, we should consider how the coords_in() and from_abs_coords() methods are implemented, as it strikes me that we have the possibility of off-by-one issues there too because we are converting relative points in [0, 1] to image coordinates in [0, w], but the image pixel range is only [0, w - 1].
Here are some changes that seem sensible to me (untested, which is why I haven't added a commit for these yet):
$ git diff
diff --git a/eta/core/geometry.py b/eta/core/geometry.py
index 7611b906..14fcb9aa 100644
--- a/eta/core/geometry.py
+++ b/eta/core/geometry.py
@@ -447,7 +447,7 @@ class RelativePoint(Serializable):
the absolute (x, y) coordinates of this point
"""
w, h = _to_frame_size(frame_size=frame_size, shape=shape, img=img)
- return int(w * 1.0 * self.x), int(h * 1.0 * self.y)
+ return int(round(w * self.x)), int(round(h * self.y))
@staticmethod
def clamp(x, y):
@@ -636,7 +636,7 @@ def _make_square(x, y, w, h):
# subimage is now always skinny
def pad(z, dz, zmax):
- dz1 = int(0.5 * dz)
+ dz1 = int(round(0.5 * dz))
dz2 = dz - dz1
ddz = max(0, dz1 - z.start) - max(0, z.stop + dz2 - zmax)
return slice(z.start - dz1 + ddz, z.stop + dz2 + ddz)
diff --git a/eta/core/keypoints.py b/eta/core/keypoints.py
index 53359c9d..ae374134 100644
--- a/eta/core/keypoints.py
+++ b/eta/core/keypoints.py
@@ -182,7 +182,10 @@ class Keypoints(etal.Labels):
a list of (x, y) keypoints in pixels
"""
w, h = _to_frame_size(frame_size=frame_size, shape=shape, img=img)
- return [(int(round(x * w)), int(round(y * h))) for x, y in self.points]
+ return [
+ (int(round(x * (w - 1))), int(round(y * (h - 1))))
+ for x, y in self.points
+ ]
def filter_by_schema(self, schema, allow_none_label=False):
"""Filters the keypoints by the given schema.
@@ -265,8 +268,8 @@ class Keypoints(etal.Labels):
rpoints = []
for x, y in points:
- xr = x / w
- yr = y / h
+ xr = x / (w - 1)
+ yr = y / (h - 1)
if clamp:
xr = max(0, min(xr, 1))
yr = max(0, min(yr, 1))
diff --git a/eta/core/polylines.py b/eta/core/polylines.py
index 356d7ae6..5d8e1e88 100644
--- a/eta/core/polylines.py
+++ b/eta/core/polylines.py
@@ -202,7 +202,10 @@ class Polyline(etal.Labels):
"""
w, h = _to_frame_size(frame_size=frame_size, shape=shape, img=img)
return [
- [(int(round(x * w)), int(round(y * h))) for x, y in shape]
+ [
+ (int(round(x * (w - 1))), int(round(y * (h - 1))))
+ for x, y in shape
+ ]
for shape in self.points
]
@@ -295,8 +298,8 @@ class Polyline(etal.Labels):
for shape in points:
rshape = []
for x, y in shape:
- xr = x / w
- yr = y / h
+ xr = x / (w - 1)
+ yr = y / (h - 1)
if clamp:
xr = max(0, min(xr, 1))
yr = max(0, min(yr, 1))
Rationale
render_instance_mask()computes the target region for an instance mask by callingbounding_box.coords_in(), which independently floors both the start and end coordinates usingint().floor(a + b) != floor(a) + floor(b), causing the computed target size to be±1pxoff from the actual mask dimensions.1-2px.objects_to_segmentations(), where segmentation labels appear shifted by 1-2px for detections with fractional pixel coordinates.Changes
render_instance_mask()to compute pixel coordinates usinground()instead of delegating tocoords_in()(which usesint()).Testing
Tested with 11 synthetic cases via FiftyOne's
objects_to_segmentations():±1pxmismatch withint()— all pass withround()Related