@@ -612,6 +612,9 @@ def sync_captures(self, batch_size=1000, regroup_events_per_batch=False, job: "J
612
612
job .progress .add_stage ("Update deployment cache" )
613
613
job .update_progress ()
614
614
615
+ # Regroup source images if needed
616
+ if deployment_event_needs_update (deployment ):
617
+ group_images_into_events (deployment )
615
618
self .save ()
616
619
self .update_calculated_fields (save = True )
617
620
@@ -1093,6 +1096,23 @@ def group_images_into_events(
1093
1096
return events
1094
1097
1095
1098
1099
+ def deployment_event_needs_update (deployment : Deployment ) -> bool :
1100
+ """
1101
+ Returns True if there are any SourceImages in the deployment
1102
+ that haven't been assigned to an `Event`.
1103
+
1104
+ Note: This does not detect if images were deleted from the deployment
1105
+ after being grouped. We currently have limited support for image deletion,
1106
+ so handling that is out of scope for this check.
1107
+ """
1108
+
1109
+ ungrouped_images_exist = SourceImage .objects .filter (deployment = deployment , event__isnull = True ).exists ()
1110
+
1111
+ logger .debug (f"Deployment { deployment .pk } : ungrouped images exist = { ungrouped_images_exist } " )
1112
+
1113
+ return ungrouped_images_exist
1114
+
1115
+
1096
1116
def delete_empty_events (deployment : Deployment , dry_run = False ):
1097
1117
"""
1098
1118
Delete events that have no images, occurrences or other related records.
@@ -3022,7 +3042,7 @@ def html(self) -> str:
3022
3042
3023
3043
3024
3044
_SOURCE_IMAGE_SAMPLING_METHODS = [
3025
- "common_combined" ,
3045
+ "common_combined" , # Deprecated
3026
3046
"random" ,
3027
3047
"stratified_random" ,
3028
3048
"interval" ,
@@ -3032,6 +3052,7 @@ def html(self) -> str:
3032
3052
"last_and_random_from_each_event" ,
3033
3053
"greatest_file_size_from_each_event" ,
3034
3054
"detections_only" ,
3055
+ "full" ,
3035
3056
]
3036
3057
3037
3058
@@ -3157,7 +3178,16 @@ def taxa_count(self) -> int | None:
3157
3178
# This should always be pre-populated using queryset annotations
3158
3179
return None
3159
3180
3160
- def get_queryset (self ):
3181
+ def get_queryset (
3182
+ self ,
3183
+ hour_start : int | None = None ,
3184
+ hour_end : int | None = None ,
3185
+ month_start : int | None = None ,
3186
+ month_end : int | None = None ,
3187
+ date_start : str | None = None ,
3188
+ date_end : str | None = None ,
3189
+ deployment_ids : list [int ] | None = None ,
3190
+ ):
3161
3191
return SourceImage .objects .filter (project = self .project )
3162
3192
3163
3193
@classmethod
@@ -3184,33 +3214,17 @@ def populate_sample(self, job: "Job | None" = None):
3184
3214
self .save ()
3185
3215
task_logger .info (f"Done sampling and saving captures to { self } " )
3186
3216
3187
- def sample_random (self , size : int = 100 ):
3188
- """Create a random sample of source images"""
3189
-
3190
- qs = self .get_queryset ()
3191
- return qs .order_by ("?" )[:size ]
3192
-
3193
- def sample_manual (self , image_ids : list [int ]):
3194
- """Create a sample of source images based on a list of source image IDs"""
3195
-
3196
- qs = self .get_queryset ()
3197
- return qs .filter (id__in = image_ids )
3198
-
3199
- def sample_common_combined (
3217
+ def _filter_sample (
3200
3218
self ,
3201
- minute_interval : int | None = None ,
3202
- max_num : int | None = None ,
3203
- shuffle : bool = True , # This is applicable if max_num is set and minute_interval is not set
3219
+ qs : models .QuerySet ,
3204
3220
hour_start : int | None = None ,
3205
3221
hour_end : int | None = None ,
3206
3222
month_start : int | None = None ,
3207
3223
month_end : int | None = None ,
3208
3224
date_start : str | None = None ,
3209
3225
date_end : str | None = None ,
3210
3226
deployment_ids : list [int ] | None = None ,
3211
- ) -> models .QuerySet | typing .Generator [SourceImage , None , None ]:
3212
- qs = self .get_queryset ()
3213
-
3227
+ ):
3214
3228
if deployment_ids is not None :
3215
3229
qs = qs .filter (deployment__in = deployment_ids )
3216
3230
if date_start is not None :
@@ -3235,6 +3249,66 @@ def sample_common_combined(
3235
3249
elif hour_end is not None :
3236
3250
qs = qs .filter (timestamp__hour__lte = hour_end )
3237
3251
3252
+ return qs
3253
+
3254
+ def sample_random (
3255
+ self ,
3256
+ size : int = 100 ,
3257
+ hour_start : int | None = None ,
3258
+ hour_end : int | None = None ,
3259
+ month_start : int | None = None ,
3260
+ month_end : int | None = None ,
3261
+ date_start : str | None = None ,
3262
+ date_end : str | None = None ,
3263
+ deployment_ids : list [int ] | None = None ,
3264
+ ):
3265
+ """Create a random sample of source images"""
3266
+
3267
+ qs = self .get_queryset ()
3268
+ qs = self ._filter_sample (
3269
+ qs = qs ,
3270
+ hour_start = hour_start ,
3271
+ hour_end = hour_end ,
3272
+ month_start = month_start ,
3273
+ month_end = month_end ,
3274
+ date_start = date_start ,
3275
+ date_end = date_end ,
3276
+ deployment_ids = deployment_ids ,
3277
+ )
3278
+ return qs .order_by ("?" )[:size ]
3279
+
3280
+ def sample_manual (self , image_ids : list [int ]):
3281
+ """Create a sample of source images based on a list of source image IDs"""
3282
+
3283
+ qs = self .get_queryset ()
3284
+ return qs .filter (id__in = image_ids )
3285
+
3286
+ # Deprecated
3287
+ def sample_common_combined (
3288
+ self ,
3289
+ minute_interval : int | None = None ,
3290
+ max_num : int | None = None ,
3291
+ shuffle : bool = True , # This is applicable if max_num is set and minute_interval is not set
3292
+ hour_start : int | None = None ,
3293
+ hour_end : int | None = None ,
3294
+ month_start : int | None = None ,
3295
+ month_end : int | None = None ,
3296
+ date_start : str | None = None ,
3297
+ date_end : str | None = None ,
3298
+ deployment_ids : list [int ] | None = None ,
3299
+ ) -> models .QuerySet | typing .Generator [SourceImage , None , None ]:
3300
+ qs = self .get_queryset ()
3301
+ qs = self ._filter_sample (
3302
+ qs = qs ,
3303
+ hour_start = hour_start ,
3304
+ hour_end = hour_end ,
3305
+ month_start = month_start ,
3306
+ month_end = month_end ,
3307
+ date_start = date_start ,
3308
+ date_end = date_end ,
3309
+ deployment_ids = deployment_ids ,
3310
+ )
3311
+
3238
3312
if minute_interval is not None :
3239
3313
# @TODO can this be done in the database and return a queryset?
3240
3314
# this currently returns a list of source images
@@ -3250,11 +3324,31 @@ def sample_common_combined(
3250
3324
return qs
3251
3325
3252
3326
def sample_interval (
3253
- self , minute_interval : int = 10 , exclude_events : list [int ] = [], deployment_id : int | None = None
3327
+ self ,
3328
+ minute_interval : int = 10 ,
3329
+ exclude_events : list [int ] = [],
3330
+ deployment_id : int | None = None , # Deprecated
3331
+ hour_start : int | None = None ,
3332
+ hour_end : int | None = None ,
3333
+ month_start : int | None = None ,
3334
+ month_end : int | None = None ,
3335
+ date_start : str | None = None ,
3336
+ date_end : str | None = None ,
3337
+ deployment_ids : list [int ] | None = None ,
3254
3338
):
3255
3339
"""Create a sample of source images based on a time interval"""
3256
3340
3257
3341
qs = self .get_queryset ()
3342
+ qs = self ._filter_sample (
3343
+ qs = qs ,
3344
+ hour_start = hour_start ,
3345
+ hour_end = hour_end ,
3346
+ month_start = month_start ,
3347
+ month_end = month_end ,
3348
+ date_start = date_start ,
3349
+ date_end = date_end ,
3350
+ deployment_ids = deployment_ids ,
3351
+ )
3258
3352
if deployment_id :
3259
3353
qs = qs .filter (deployment = deployment_id )
3260
3354
if exclude_events :
@@ -3314,6 +3408,31 @@ def sample_detections_only(self):
3314
3408
qs = self .get_queryset ()
3315
3409
return qs .filter (detections__isnull = False ).distinct ()
3316
3410
3411
+ def sample_full (
3412
+ self ,
3413
+ hour_start : int | None = None ,
3414
+ hour_end : int | None = None ,
3415
+ month_start : int | None = None ,
3416
+ month_end : int | None = None ,
3417
+ date_start : str | None = None ,
3418
+ date_end : str | None = None ,
3419
+ deployment_ids : list [int ] | None = None ,
3420
+ ):
3421
+ """Sample all source images"""
3422
+
3423
+ qs = self .get_queryset ()
3424
+ qs = self ._filter_sample (
3425
+ qs = qs ,
3426
+ hour_start = hour_start ,
3427
+ hour_end = hour_end ,
3428
+ month_start = month_start ,
3429
+ month_end = month_end ,
3430
+ date_start = date_start ,
3431
+ date_end = date_end ,
3432
+ deployment_ids = deployment_ids ,
3433
+ )
3434
+ return qs .all ().distinct ()
3435
+
3317
3436
@classmethod
3318
3437
def get_or_create_starred_collection (cls , project : Project ) -> "SourceImageCollection" :
3319
3438
"""
0 commit comments