-
Notifications
You must be signed in to change notification settings - Fork 7k
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
Distance IoU #5786
Distance IoU #5786
Conversation
I have to improve the tests a bit. I have also some refactoring ideas in mind but it is better to leave those for a new PR. |
@yassineAlouini Thanks for the contribution. I know your PR is still draft but it's worth looking at the comments at #5776 since many of them are relevant to you too. |
Indeed. I am already using some of the first feedbacks and I will look for new ones. 👌 |
@oke-aditya @abhi-glitchhg I have added more tests highly inspired from cIoU and fixed the ones I have added already. |
test/test_ops.py
Outdated
@staticmethod | ||
def assert_distance_iou_loss(box1, box2, expected_output, dtype, device, reduction="none"): | ||
output = ops.distance_box_iou_loss(box1, box2, reduction=reduction) | ||
expected_output = torch.tensor(expected_output, dtype=dtype, device=device) |
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.
I think this will automatically take the dtype, by passing dtype as function arg. It will also get parameterized,
expected_output = torch.tensor(expected_output, dtype=dtype, device=device) | |
expected_output = torch.tensor(expected_output, device=device) |
Notice that
this works!
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.
Not exactly the same. I made the nested function into a staticmethod
so it doesn't have access to the same scope.
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.
Yeah, but why is it a static method in the first place? IIUC, we are only using inside test_distance_iou_loss
, correct? If yes, we can simply inline it, which also removes the need to pass the device and dtype.
test/test_ops.py
Outdated
@@ -1258,6 +1258,85 @@ def test_giou_jit(self) -> None: | |||
self._run_jit_test([[0, 0, 100, 100], [0, 0, 50, 50], [200, 200, 300, 300]]) | |||
|
|||
|
|||
class TestDistanceBoxIoU(BoxTestBase): | |||
def _target_fn(self) -> Tuple[bool, Callable]: |
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.
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, we don't. See #5563 (comment). The reason is that without also checking the tests with mypy
they might go out of date and that is usually more harmful than not having them at all.
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.
I am removing the type hints then, thanks for the link @pmeier. 👍
@oke-aditya @abhi-glitchhg @pmeier I have a question regarding If |
torchvision/ops/diou_loss.py
Outdated
batch_boxes2 = boxes2.unsqueeze(0) | ||
diou = distance_box_iou(batch_boxes1, batch_boxes2, eps)[0, 0] | ||
else: | ||
diou = distance_box_iou(boxes1, boxes2, eps)[0] |
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.
@yassineAlouini I'm not sure this approach is equivalent to what we had earlier. Please correct me if I'm wrong but I understand that distance_box_iou()
does NxM pairwise comparisons while here we just want the Nx1 comparisons. This can be given by using the diagonal but this should be extremely slow as we would be throwing away the majority of operations.
What I had in mind is try to refactor the code at ops.boxes
so that we can share some of the estimations. Thoughts?
cc @abhi-glitchhg because you follow a similar approach on the other PR.
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.
BTW if you both prefer to revert to your earlier versions of the code (which didn't reuse ops.boxes) and tackle this on separate future PRs, I'm happy to go down that route. The PRs for cIoU and dIoU has been dragging for a while and I appreciate that this can become frustrating at one point. Let me know your preference so that we make this a more enjoyable experience for both of you. Thanks a bunch for your work so far. :)
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.
@datumbox , I agree with your concerns. This is not computationally efficient.
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.
BTW if you both prefer to revert to your earlier versions of the code (which didn't reuse ops.boxes) and tackle this on separate future PRs,
Yeah, sounds good. :)
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.
@yassineAlouini I'm not sure this approach is equivalent to what we had earlier. Please correct me if I'm wrong but I understand that
distance_box_iou()
does NxM pairwise comparisons while here we just want the Nx1 comparisons. This can be given by using the diagonal but this should be extremely slow as we would be throwing away the majority of operations.What I had in mind is try to refactor the code at
ops.boxes
so that we can share some of the estimations. Thoughts?cc @abhi-glitchhg because you follow a similar approach on the other PR.
That's a very good point and I might have introduced a bug by going quickly on my refactor, so thanks for pointing this out.
I can revert to previous code or keep working on this here (in this PR), both work for me. 👌
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.
Thanks for the flexibility! Let's revert and use the previously vetted code on the loss. We can investigate refactoring all losses to share code with ops.boxes
on separate PRs as you originally suggested.
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.
Sounds great! I have also found a fix for the torch.half
casting issue but it requires removing the _upcast
of the boxes. I have left a TODO where I have removed the casting.
There are many options:
- either I remove the torch.half tests for now.
- remove the
_upcast
but could get an overflow error as you have mentioned above. - keep investigating to find a fix for all the problems at once.
What are your thoughts @datumbox? 🤔
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.
@yassineAlouini From what I see you've reverted the code that reused estimations from ops.boxes
and now use the a modified version of the loss at Detectron2. BTW we should add a reference in the source code similar to this, to indicate the original source.
Concerning the casting question, note that in ops.boxes
the _upcast method is primarily there to handle overflows of low precision values that might overflow. The main concern is integers (because those methods need to support them). In the case of losses, the _upcast method works a bit different as it converts everything to floats (we don't handle integers in losses). This is crucial because some of the area estimation operations will overflow in torch.float16
.
>>> torch.finfo(torch.float16)
finfo(resolution=0.001, min=-65504, max=65504, eps=0.000976562, tiny=6.10352e-05, dtype=float16)
So I think the safe thing to do here is to follow the same approach as in gIoU and upcast. I think we should move the method _upcast
to _utils
so it can be shared. This is going to be useful also for the cIoU PR that @abhi-glitchhg is working on.
cc @fmassa for visibility in case I stated something incorrect here.
…ed a bug and can be slow.
torchvision/ops/boxes.py
Outdated
# centers of boxes | ||
x_p = boxes1[:, None, :2].sum() / 2 | ||
y_p = boxes1[:, None, 2:].sum() / 2 | ||
x_g = boxes2[:, :2].sum() / 2 | ||
y_g = boxes2[:, 2:].sum() / 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.
Hey @yassineAlouini , I think there is a problem with this implementation. The calculation of centre of boxes is not correct acc to me. We should be adding up only x1
x2
and y1
y2
, ref .
But in current implementation, we are adding x1
,y1
and x2
, y2
. (BBox shape is in form [x1,y1,x2,y2]) .
This can also be checked by calculating distance_box_iou_loss
and distance_box_iou
on a sample tensors.
import torch
from torchvision.ops import distance_box_iou, distance_box_iou_loss
box1 = torch.tensor([[-1, -1, 1, 1]], )
box2 = torch.tensor([[0, 0, 1, 1]],)
1-distance_box_iou(box1, box2)[0] == distance_box_iou_loss(box1, box2)
Last statement returns False
. Ideally it should return True
.
I suggest you to do following changes.
# centers of boxes | |
x_p = boxes1[:, None, :2].sum() / 2 | |
y_p = boxes1[:, None, 2:].sum() / 2 | |
x_g = boxes2[:, :2].sum() / 2 | |
y_g = boxes2[:, 2:].sum() / 2 | |
# centers of boxes | |
x_p = (boxes1[:, 0] + boxes1[:, 2]) / 2 | |
y_p = (boxes1[:, 1] + boxes1[:, 3]) / 2 | |
x_g = (boxes2[:, 0] + boxes2[:, 2]) / 2 | |
y_g = (boxes2[:, 1] + boxes2[:, 3]) / 2 |
@datumbox,
Please correct me if I'm wrong.
Thanks.
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.
Good catch. I haven't yet reviewed the correctness of the implementation as we still discuss the structure/API.
I think that's probably a typo and @yassineAlouini intended to write something like:
x_p = boxes1[:, 0::2].sum() / 2
y_p = boxes1[:, 1::2].sum() / 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.
Yes indeed, I think I went too quickly over this and thought that the bounding box was in the x1x2y1y2
format. Thanks for pointing this out and your suggestions. 👍
@datumbox I see that the cIoU metric has been merged. I will take inspiration to fix remaining issues today hopefully. 👌 |
@datumbox @pmeier I made the code iso with the
|
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.
@yassineAlouini Apologies for the delay. I was OOO last week.
I've reviewed the code of the implementation and it looks good to me! There was a code attribution missing but I pushed directly into your branch to avoid the back and forth.
In addition to the review, I verified that the two implementations for boxes and losses return the same result by running:
import torch
from torchvision.ops.boxes import distance_box_iou
from torchvision.ops.diou_loss import distance_box_iou_loss
def random_box(canvas_size):
x1y1 = torch.rand((1, 2)) * canvas_size
wh = torch.rand((1, 2)) * canvas_size
x2y2 = (x1y1 + wh).clamp(0, canvas_size)
return torch.cat((x1y1, x2y2), axis=1)
canvas_size = 1000
for _ in range(10000):
box1 = random_box(canvas_size)
box2 = random_box(canvas_size)
v1 = 1 - distance_box_iou(box1, box2)
v2 = distance_box_iou_loss(box1, box2)
torch.testing.assert_close(v1[0], v2, rtol=0, atol=1e-6)
@pmeier Any blocking changes required on the testing side? If not I recommend merging and do a deep cleaning between the g/c/d-iou.
@yassineAlouini @abhi-glitchhg @oke-aditya Anyone interested in doing this cleanup?
@@ -1676,6 +1767,7 @@ def test_ciou_loss(self, dtype, device): | |||
def assert_ciou_loss(box1, box2, expected_output, reduction="none"): | |||
|
|||
output = ops.complete_box_iou_loss(box1, box2, reduction=reduction) | |||
# TODO: When passing the dtype, the torch.half test doesn't pass... |
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 it still valid?
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.
Can we provide a bit more info on what doesn't pass here and what's exactly the issue?
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.
@oke-aditya I think so.
@datumbox I read the cIoU code since it was passing the torch.half tests and I found out that the dtype wasn't passed so the test wasn't correct for torch.half. For now, I have removed the dtype to have the same code as cIoU but I think we should investigate this further (or maybe we can't do anything since we use the _upcast function? 🤔). Let me know if this clear enough. I can provide more details.
from ..utils import _log_api_usage_once | ||
from .boxes import _upcast | ||
|
||
|
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.
Commenting above the function might be better? As comments in the function call will be a small execution of commented code everytime we call code? (Is there a subtle performance difference? Not sure but always had this doubt)
https://arxiv.org/abs/1911.08287 | ||
""" | ||
|
||
# Original Implementation : https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/losses.py |
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.
I meant this comment
I can clean up. But @yassineAlouini feel free if you want to have a go :) |
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.
Testing LGTM, thanks @yassineAlouini!
@oke-aditya Here is the ticket: #5976. Feel free to assign it to yourself or comment you want it so that I can assign it to you (Github won't let me do it now for some reason). |
Unless you they have commented on the issue, you can only assign issues to people that have a least collaborator status. |
Thanks @oke-aditya for the review. You can work on the #5976 ticket. I can help with any additional/related tasks if needed. 👌 |
@yassineAlouini Feel free to review #5979 |
Summary: * [FEAT] Add distance IoU and distance IoU loss + some tests (WIP for tests). * [FIX] Remove URL from docstring + remove assert since it causes a big performance drop. * [FIX] eps isn't None. * [TEST] Update existing box dIoU test + add dIoU loss tests (inspired from cIoU ones). * [ENH] Some pre-commit fixes + remove print + mypy. * [ENH] Pass the device in the assertion for the dIoU loss test. * [FIX] Remove type hints from the dIoU box test. * [ENH] Refactor box and loss for dIoU functions + fix half tests. * [FIX] Precommits fix. * [ENH] Some improvement for the distance IoU tests thanks to code review. * [ENH] Upcast in distance boxes computation to avoid overflow. * [ENH] Revert the refactor of distance IoU loss back since it introduced a bug and can be slow. * Precommit fix. * [FIX] Few changes introduced by merge conflict. * Add code reference * Fix test Reviewed By: YosuaMichael Differential Revision: D36281596 fbshipit-source-id: 70e5102ec6fae9c9795d1895911f94f0a74e42f8 Co-authored-by: Vasilis Vryniotis <datumbox@users.noreply.github.com>
I will, thanks for the suggestion @oke-aditya. 👌 |
In this PR, I implement the distance IoU and associated loss as described from this paper: https://arxiv.org/abs/1911.08287.
The implementation is inspired and adapted from here https://github.com/facebookresearch/detectron2/blob/dfe8d368c8b7cc2be42c5c3faf9bdcc3c08257b1/detectron2/layers/losses.py#L5 and from the implemented giou.