Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Added an option to count detection upon the center point of the bounding box crossing the line counter #735

Conversation

revtheundead
Copy link
Contributor

@revtheundead revtheundead commented Jan 16, 2024

Description

Added an option to count detection upon the center point of the bounding box crossing the line counter.

Added an option to LineZone class specifying the condition which determines whether a detection has crossed the line counter or not.

Additionally made it so that the line counters check whether if the corners (or optionally the center point) of a detection's bounding box are in the line counter's coordinate ranges. This way, line counters count only the detections that have precisely crossed the bounds that are drawn without failing and counting targets that have crossed invisible extensions of the lines. To this end, a new class function is_in_line_range(self, point: Point) that checks whether if a given point is within the coordinate ranges of a line counter is added to class LineZone.

This change was motivated by a use case in a personal project in which a box made from line counters (LineZones) is used to count not only the current count of the targets in the box like in PolygonZone but also to keep tabs on the total the count of targets.

Being able to customize the counting condition of line counters was also seen to have been requested in issue #87 by user @falkaabi.

Extra

An additional function cross_product() was added to the Vector class. This takes the exact same arguments as is_in() function (the function signature is cross_product(self, point: Point) -> float) but returns the actual cross product of the vector with the given point instead of boolean.

This change was made as a small quality of life improvement and a comment was added to better interpret the cross product results.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How has this change been tested, please provide a testcase or example of how you tested the change?

A small rectangle made from 4 separate line counters (LineZones) was formed in the middle of the webcam view. "Bicycle" target class (YOLOv8) was selected to filter detections. A photo of a bicycle was shown to the webcam and the existing and new modes of operation in the line counters were tested.

With the changes made, line counters no longer updated "in count" or "out count" when any of the corners (or center point) of the detection's bounding box fell out of the X and Y ranges of the line counters. Essentially, an update to "in count" or "out count" was made only when every corner (or the center point) of a detection's bounding box was precisely within the coordinate ranges (startx - endx and starty - endy) of the line counter.

Any specific deployment considerations

A new argument was added to LineZone class called count_condition. This argument can take the values whole_crossed or center_point_crossed which determines what condition should be satisfied in order for a detection to be considered as having "crossed" the line counter.

count_condition is set to whole_crossed by default. This is the previous behaviour of the class prior to the changes with the exception of also checking the coordinate of every bounding box anchor against the line counter's ranges.

When count_condition is set to center_point_crossed, the center point of the bounding box is calculated and the detection is said to have crossed the line counter only if the center point has a negative product with its previous state and is within the line counter's coordinate ranges.

Docs

  • Docs updated? What were the changes:
  • New argument count_condition added to class LineZone.
  • New class method is_in_line_range() added to class LineZone.
  • New function cross_product() added to class Vector.

…ing box crossing the line counter.

Added an option to LineCounter class specifying the condition which determines whether a detection has crossed the line counter or not.

Additionally made it so that the line counters check whether if the corners (or optionally the center point) of a detection's bounding box are in the line counter's coordinate ranges. This way, line counters count only the detections that have precisely crossed the bounds that are drawn without failing and counting targets that have crossed invisible extensions of the lines.
@CLAassistant
Copy link

CLAassistant commented Jan 16, 2024

CLA assistant check
All committers have signed the CLA.

@SkalskiP
Copy link
Collaborator

Hi, @revtheundead 👋🏻 ! Thanks a lot for your interest in supervision. I was thinking about adding other triggering strategies for quite some time. I'll try to review this code as fast as possible.

@SkalskiP
Copy link
Collaborator

@revtheundead, would you be willing to implement changes some changes? Looks like I will have a few comments. Nothing huge. ;)

@revtheundead
Copy link
Contributor Author

@revtheundead, would you be willing to implement changes some changes? Looks like I will have a few comments. Nothing huge. ;)

Of course :) I'll be on them as soon as I'm available.

@@ -51,6 +51,16 @@ def is_in(self, point: Point) -> bool:
) * (v2.end.x - v2.start.x)
return cross_product < 0

def cross_product(self, point: Point) -> float:
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are a few things I would like to address.

  1. We generally require function/methods arguments and results to be documented in docstring. This docstring is later used to generate our documentation website automatically.
  2. Secondly, we already have is_in method, responsible for very similar calculations. I don't like the is_in name. cross_product is a lot better. I'm happy that you made this change:
# old
triggers = [self.vector.is_in(point=anchor) for anchor in anchors]

# new
triggers = [
    self.vector.cross_product(point=anchor) < 0 
    for anchor 
    in anchors
]

Please remove the old is_in method, and adjust test_vector_is_in to test your new method.

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 made the requested adjustments. Please check them if any further modifications is needed.

if tracker_id is None:
continue
if self.count_condition == "whole_crossed":
for i, (xyxy, _, confidence, class_id, tracker_id) in enumerate(detections):
Copy link
Collaborator

Choose a reason for hiding this comment

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

That line was changed recently. Please reflect that change in your new implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed this line with the new commit.

"Argument count_condition must be 'whole_crossed' or 'center_point_crossed'"
)

def is_point_in_line_range(self, point: Point) -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you share a bit more about this change? Does it work with diagonal lines? Adding a behavior like the one shown in the picture has been on my mind for some time.

The idea is to check whether the bounding box points of interest ( in this case, the bottom center) are between the dashed lines.

line zone-1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The given function works for the above case of a diagonal line. In the case of purely vertical and horizontal lines; Xstart, Xend and Ystart, Yend respectively are the same. So in those cases only the coordinate range that is meaningful to check is being asserted. In all other cases (such as the diagonal line in the image) both coordinate ranges should hold for the point of interest to be considered "in range".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

However, it should be noted that in the current way the function works, the dashed lines are parallel to the axes instead of being perpendicular to the line as shown above. So in reality the area considered as "in range" of the line counter's coordinates is much smaller. This could potentially lead to some bugs when there are multiple triggering anchors that are far from each other, in which case the line counter won't count the object as having crossed in or out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking of a fix for that in the mean time :)

return self.in_count, self.out_count

elif self.count_condition == "center_point_crossed":
for i, (xyxy, _, confidence, class_id, tracker_id) in enumerate(detections):
Copy link
Collaborator

Choose a reason for hiding this comment

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

That line was changed recently. Please reflect that change in your new implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That check is no longer necessary so it was removed with the new commit.

self.out_count += 1
crossed_out[i] = True
# Update the tracker state and check for crossing
if previous_state * current_state < 0 and self.is_point_in_line_range(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please do not include comments in code :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, you are right. I'll try not to leave any more comments :)

x1, y1, x2, y2 = xyxy

# Calculate the center point of the box
center_point = Point(x=(x1 + x2) / 2, y=(y1 + y2) / 2)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's not limit ourselves to just the center of the box. Let's make it more general. Also, you can use get_anchors_coordinates to calculate coordinates of specific bounding box points. (already tested with multiple unit tests)

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 have used the function you mentioned in the new commit.

@@ -26,16 +26,61 @@ class LineZone:
to outside.
"""

def __init__(self, start: Point, end: Point):
def __init__(self, start: Point, end: Point, count_condition="whole_crossed"):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here's an idea. Let's replace count_condition with triggering_anchors: List[sv.Position]. In this way, we can provide our users with full flexibility.

Let me know if you need more explanation. :)

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 believe I implemented just what you needed. Please check so we can discuss further if need be.

@revtheundead
Copy link
Contributor Author

Hey @SkalskiP, I made some small mistakes and reverted them. Sorry for the confusion but it should all be good to go now. Let me know if you have any questions.

@SkalskiP
Copy link
Collaborator

@revtheundead, please don't work on this branch now. I'm testing a few things around is_point_in_line_range 🙏🏻

@SkalskiP
Copy link
Collaborator

@revtheundead, it looks like I managed to successfully implement the logic to filter detections located in the region of interest. I just need to plug it into your code, and we should be good.

download - 2024-01-18T150033 271
download - 2024-01-18T150030 389

@revtheundead
Copy link
Contributor Author

@SkalskiP, thats great! Hope I was of some help. I hadn't noticed that something was wrong with the logic. Would you mind sharing what was missing and how you fixed it? Asking out of curiosity :)

@SkalskiP
Copy link
Collaborator

@revtheundead I just described it on Twitter: https://x.com/skalskip92/status/1747998962429186111?s=20! Take a look!

Hope I was of some help.

Of course, you were! I'll keep everything else. Just replace your is_point_in_line_range with new is_point_in_limits.

@revtheundead
Copy link
Contributor Author

@revtheundead I just described it on Twitter: https://x.com/skalskip92/status/1747998962429186111?s=20! Take a look!

Hope I was of some help.

Of course, you were! I'll keep everything else. Just replace your is_point_in_line_range with new is_point_in_limits.

I suppose you made the necessary changes and it looks good to me. Let me know if you need further modifications :)

@SkalskiP
Copy link
Collaborator

@revtheundead, thanks a lot for your help! I believe we made LineZone a lot better today. This PR brings in a new, strong mechanism and solves a bug identified months ago.

  • Only detections ACTUALLY crossing the line are taken into consideration. If the object passes the line from the side, it is discarded.
vehicle-counting-result-left-lane-4-anchors-optimized.mp4
vehicle-counting-result-right-lane-4-anchors-optimized.mp4
  • You can select the list of bounding box anchors that need to cross the line for the object to be counted as crossing the line.
vehicle-counting-result-left-lane-1-anchor-bottom-center-optimized.mp4

@revtheundead
Copy link
Contributor Author

@SkalskiP, its been my pleasure! Seeing the line counters work this efficiently makes me think that the effort really paid off. It would be nice to contribute more when I can.

@revtheundead
Copy link
Contributor Author

@SkalskiP I hope you don't mind me mentioning this improvement and our collaboration on LinkedIn. With credits of course :)

@SkalskiP
Copy link
Collaborator

@revtheundead, not at all! I always write a post related to supervision releases and tag all contributors. supervision-0.18.0 is probably coming next week, so I'll tag you 100%.

@revtheundead
Copy link
Contributor Author

@revtheundead, not at all! I always write a post related to supervision releases and tag all contributors. supervision-0.18.0 is probably coming next week, so I'll tag you 100%.

@SkalskiP Awesome! I'd be honored.

@SkalskiP SkalskiP merged commit c04edbc into roboflow:develop Jan 18, 2024
8 checks passed
@revtheundead revtheundead deleted the add-center-point-crossed-condition-to-line-counter branch January 18, 2024 19:05
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants