-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Added an option to count detection upon the center point of the bounding box crossing the line counter #735
Conversation
…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.
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. |
@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: |
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 are a few things I would like to address.
- We generally require function/methods arguments and results to be documented in docstring. This docstring is later used to generate our documentation website automatically.
- Secondly, we already have
is_in
method, responsible for very similar calculations. I don't like theis_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.
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 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): |
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.
That line was changed recently. Please reflect that change in your new implementation.
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.
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: |
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.
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".
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.
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.
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.
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): |
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.
That line was changed recently. Please reflect that change in your new implementation.
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.
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( |
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.
Please do not include comments in code :)
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.
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) |
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.
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)
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 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"): |
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'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. :)
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 believe I implemented just what you needed. Please check so we can discuss further if need be.
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. |
@revtheundead, please don't work on this branch now. I'm testing a few things around |
@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. |
@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 :) |
@revtheundead I just described it on Twitter: https://x.com/skalskip92/status/1747998962429186111?s=20! Take a look!
Of course, you were! I'll keep everything else. Just replace your |
I suppose you made the necessary changes and it looks good to me. Let me know if you need further modifications :) |
@revtheundead, thanks a lot for your help! I believe we made
vehicle-counting-result-left-lane-4-anchors-optimized.mp4vehicle-counting-result-right-lane-4-anchors-optimized.mp4
vehicle-counting-result-left-lane-1-anchor-bottom-center-optimized.mp4 |
@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. |
@SkalskiP I hope you don't mind me mentioning this improvement and our collaboration on LinkedIn. With credits of course :) |
@revtheundead, not at all! I always write a post related to supervision releases and tag all contributors. |
@SkalskiP Awesome! I'd be honored. |
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 classLineZone
.This change was motivated by a use case in a personal project in which a box made from line counters (
LineZone
s) 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 theVector
class. This takes the exact same arguments asis_in()
function (the function signature iscross_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.
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 (
LineZone
s) 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
andY
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
andstarty - endy
) of the line counter.Any specific deployment considerations
A new argument was added to
LineZone
class calledcount_condition
. This argument can take the valueswhole_crossed
orcenter_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 towhole_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 tocenter_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
count_condition
added to classLineZone
.is_in_line_range()
added to classLineZone
.cross_product()
added to classVector
.