-
Notifications
You must be signed in to change notification settings - Fork 11
Fix incorrect session grouping #904
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
ece53c1
76a4cb4
9c9a216
5249e77
1b2d2a6
660e883
0bbcfdd
4fb045a
7a4949c
a9213ee
7cb4c6e
706c560
9bd037f
05ce90c
e0fcadc
aeee536
5d8df37
67ff8b1
ebcf866
bb14620
5465b4d
20ed64c
21e39eb
a800802
931a686
541f26b
9ecf60c
f1ab1b4
3583da5
21afc3c
1b46b35
9808407
8ec6671
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Generated by Django 4.2.10 on 2025-07-23 05:15 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("main", "0060_alter_sourceimagecollection_method"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RemoveConstraint( | ||
| model_name="event", | ||
| name="unique_event", | ||
| ), | ||
| migrations.RemoveIndex( | ||
| model_name="event", | ||
| name="main_event_group_b_6ce666_idx", | ||
| ), | ||
| migrations.RemoveField( | ||
| model_name="event", | ||
| name="group_by", | ||
| ), | ||
| migrations.AddConstraint( | ||
| model_name="event", | ||
| constraint=models.UniqueConstraint(fields=("deployment", "start", "end"), name="unique_event"), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -785,18 +785,6 @@ def save(self, update_calculated_fields=True, *args, **kwargs): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Event(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """A monitoring session""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| group_by = models.CharField( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_length=255, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db_index=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| help_text=( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "A unique identifier for this event, used to group images into events. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "This allows images to be prepended or appended to an existing event. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "The default value is the day the event started, in the format YYYY-MM-DD. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "However images could also be grouped by camera settings, image dimensions, hour of day, " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "or a random sample." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start = models.DateTimeField(db_index=True, help_text="The timestamp of the first image in the event.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end = models.DateTimeField(null=True, blank=True, help_text="The timestamp of the last image in the event.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -815,11 +803,10 @@ class Event(BaseModel): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Meta: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ordering = ["start"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexes = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| models.Index(fields=["group_by"]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| models.Index(fields=["start"]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constraints = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| models.UniqueConstraint(fields=["deployment", "group_by"], name="unique_event"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| models.UniqueConstraint(fields=["deployment", "start", "end"], name="unique_event"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __str__(self) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -918,10 +905,6 @@ def update_calculated_fields(self, save=False, updated_timestamp: datetime.datet | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Important: if you update a new field, add it to the bulk_update call in update_calculated_fields_for_events | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event = self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not event.group_by and event.start: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If no group_by is set, use the start "day" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event.group_by = str(event.start.date()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not event.project and event.deployment: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event.project = event.deployment.project | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -982,7 +965,6 @@ def update_calculated_fields_for_events( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updated_count = Event.objects.bulk_update( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to_update, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "group_by", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "start", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "end", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "project", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -998,9 +980,14 @@ def update_calculated_fields_for_events( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def group_images_into_events( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deployment: Deployment, max_time_gap=datetime.timedelta(minutes=120), delete_empty=True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deployment: Deployment, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_time_gap=datetime.timedelta(minutes=120), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delete_empty=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use_existing=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> list[Event]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Log a warning if multiple SourceImages have the same timestamp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"Grouping images into events for deployment '{deployment}' (use_existing={use_existing})") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Log duplicate timestamps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dupes = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SourceImage.objects.filter(deployment=deployment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .values("timestamp") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1016,60 +1003,82 @@ def group_images_into_events( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Found {len(values)} images with the same timestamp in deployment '{deployment}'. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Only one image will be used for each timestamp for each event." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Get all images | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image_qs = SourceImage.objects.filter(deployment=deployment).exclude(timestamp=None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if use_existing: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Get only newly added images (images without an event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image_qs = image_qs.filter(event__isnull=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| images = list(image_qs.order_by("timestamp")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not images: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("No relevant images found; skipping") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image_timestamps = list( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SourceImage.objects.filter(deployment=deployment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .exclude(timestamp=None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .values_list("timestamp", flat=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .order_by("timestamp") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .distinct() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp_groups = ami.utils.dates.group_datetimes_by_gap(image_timestamps, max_time_gap) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # @TODO this event grouping needs testing. Still getting events over 24 hours | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # timestamp_groups = ami.utils.dates.group_datetimes_by_shifted_day(image_timestamps) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Group timestamps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamps = list(image_qs.order_by("timestamp").values_list("timestamp", flat=True).distinct()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp_groups = ami.utils.dates.group_datetimes_by_gap(timestamps, max_time_gap) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existing_events = list(Event.objects.filter(deployment=deployment)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # For each group of images check if we can merge with an existing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # event based on overlapping or proximity if use_existing is True. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Otherwise if use is_existing is False, we look for an existing event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # with the exact same start and end times and reuse it, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # if not found create a new event. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # if not found create a new event. | |
| # if not found, create a new event. |
Outdated
Copilot
AI
Jul 24, 2025
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.
This list comprehension creates a new list for each group and checks membership in group_set for every image. Consider creating a mapping of timestamps to image IDs beforehand to improve performance when processing multiple groups.
| for group in timestamp_groups: | |
| if not len(group): | |
| continue | |
| start_date = group[0] | |
| end_date = group[-1] | |
| # Print debugging info about groups | |
| delta = end_date - start_date | |
| hours = round(delta.seconds / 60 / 60, 1) | |
| logger.debug( | |
| f"Found session starting at {start_date} with {len(group)} images that ran for {hours} hours.\n" | |
| f"From {start_date.strftime('%c')} to {end_date.strftime('%c')}." | |
| ) | |
| group_start, group_end = group[0], group[-1] | |
| group_set = set(group) | |
| group_image_ids = [img.pk for img in images if img.timestamp in group_set] | |
| # Precompute a mapping of timestamps to image IDs | |
| timestamp_to_image_ids = collections.defaultdict(list) | |
| for img in images: | |
| timestamp_to_image_ids[img.timestamp].append(img.pk) | |
| for group in timestamp_groups: | |
| group_start, group_end = group[0], group[-1] | |
| group_image_ids = [] | |
| for timestamp in group: | |
| group_image_ids.extend(timestamp_to_image_ids.get(timestamp, [])) |
Outdated
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.
If you are looping over a queryset, you can do for existing_event in events_qs, which supposedly avoids loading the whole queryset result into memory. Sometimes you need to convert to a list so you can index the list like events[3], but often you never need to convert to a list.
Copilot
AI
Jul 24, 2025
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.
Database refresh is called for every existing event in every group iteration. Consider fetching fresh event data once before the loop or only refresh when actually needed after a merge.
Outdated
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 this check necessary? I think you just checked if and existing event has the exact start & end time. Perhaps you meant to do an OR query? If an existing event has either the same start or end time as the group.
If there is an existing event with exactly the same start AND end time (for same deployment), then I don't think we should check for use_existing. Just re-use those without question.
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.
You likely don't have to evaluate the queryset yet with
list(image_qs). You can check if images are found withimages_qs.exists(), which is efficient for large datasets.