Skip to content
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

Merged
merged 26 commits into from
May 9, 2022
Merged

Distance IoU #5786

merged 26 commits into from
May 9, 2022

Conversation

yassineAlouini
Copy link
Contributor

@yassineAlouini yassineAlouini commented Apr 7, 2022

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.

@yassineAlouini yassineAlouini changed the title [FEAT] Add distance IoU and distance IoU loss + some tests (WIP for t… Distance IoU Apr 7, 2022
@datumbox datumbox mentioned this pull request Apr 7, 2022
24 tasks
@yassineAlouini
Copy link
Contributor Author

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.

@datumbox
Copy link
Contributor

datumbox commented Apr 7, 2022

@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.

@yassineAlouini
Copy link
Contributor Author

@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. 👌

@yassineAlouini yassineAlouini marked this pull request as ready for review April 13, 2022 14:58
@yassineAlouini
Copy link
Contributor Author

@oke-aditya @abhi-glitchhg I have added more tests highly inspired from cIoU and fixed the ones I have added already.
I think it is a good time to give this PR reviews if you can and I will then take into account your suggestions. Thanks in advance. 👍

test/test_ops.py Outdated Show resolved Hide resolved
test/test_ops.py Outdated Show resolved Hide resolved
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)
Copy link
Contributor

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,

Suggested change
expected_output = torch.tensor(expected_output, dtype=dtype, device=device)
expected_output = torch.tensor(expected_output, device=device)

Notice that

https://github.com/pytorch/vision/pull/5792/files#diff-d183f2afc51d6a59bc70094e8f476d2468c45e415500f6eb60abad955e065156R1587

this works!

Copy link
Contributor Author

@yassineAlouini yassineAlouini Apr 14, 2022

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.

Copy link
Collaborator

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]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general question to torchvision maintainers
Do we type hint in tests?
It's not bad to type hint. But it isn't something which we follow either 😅

cc @pmeier @datumbox

Copy link
Collaborator

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.

Copy link
Contributor Author

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. 👍

@yassineAlouini
Copy link
Contributor Author

yassineAlouini commented Apr 14, 2022

@oke-aditya @abhi-glitchhg @pmeier I have a question regarding float16 input to the distance_box_iou_loss function:
is it required to also have the output as a float16 or is float32 enough?

If float16 is required, what is the best way to achieve this? Is disabling the autocast a good thing to do or is there a better way. Thanks for any help.

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]
Copy link
Contributor

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.

Copy link
Contributor

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. :)

Copy link
Contributor

@abhi-glitchhg abhi-glitchhg Apr 26, 2022

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.

Copy link
Contributor

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. :)

Copy link
Contributor Author

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. 👌

Copy link
Contributor

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.

Copy link
Contributor Author

@yassineAlouini yassineAlouini Apr 26, 2022

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? 🤔

Copy link
Contributor

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.

Comment on lines 345 to 349
# 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
Copy link
Contributor

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.

Suggested change
# 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.

Copy link
Contributor

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
...

Copy link
Contributor Author

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. 👍

@yassineAlouini
Copy link
Contributor Author

@datumbox I see that the cIoU metric has been merged. I will take inspiration to fix remaining issues today hopefully. 👌

@yassineAlouini
Copy link
Contributor Author

yassineAlouini commented May 2, 2022

@datumbox @pmeier I made the code iso with the cIoU one. However, I have noticed two things:

  • some imports are missing in the __all__ for cIoU and have fixed them.
  • the torch.half nightmarish test 😁 is still not working. I thought that the cIoU code found a clever fix but it seems that it is missing the dtype setup (I have put a TODO so that we don't forget it) in the appropriate test and so once I add it, it fails as well. I think it is best to fix this in the next PR (where I will refactor some of the tests).

Copy link
Contributor

@datumbox datumbox left a 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...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it still valid?

Copy link
Contributor

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?

Copy link
Contributor Author

@yassineAlouini yassineAlouini May 10, 2022

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


Copy link
Contributor

@oke-aditya oke-aditya May 9, 2022

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant this comment

@oke-aditya
Copy link
Contributor

I can clean up. But @yassineAlouini feel free if you want to have a go :)

Copy link
Collaborator

@pmeier pmeier left a 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!

@datumbox
Copy link
Contributor

datumbox commented May 9, 2022

@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).

@pmeier
Copy link
Collaborator

pmeier commented May 9, 2022

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.

@datumbox datumbox merged commit 1ae3829 into pytorch:main May 9, 2022
@yassineAlouini
Copy link
Contributor Author

I can clean up. But @yassineAlouini feel free if you want to have a go :)

Thanks @oke-aditya for the review. You can work on the #5976 ticket. I can help with any additional/related tasks if needed. 👌

@yassineAlouini yassineAlouini deleted the dIoU branch May 10, 2022 09:14
@oke-aditya
Copy link
Contributor

@yassineAlouini Feel free to review #5979

facebook-github-bot pushed a commit that referenced this pull request May 11, 2022
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>
@yassineAlouini
Copy link
Contributor Author

@yassineAlouini Feel free to review #5979

I will, thanks for the suggestion @oke-aditya. 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants