Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
74d606b
Trigger deploy
mihow Apr 29, 2025
596d424
Support for saving Classification embeddings (#821)
mihow Apr 30, 2025
5518f0d
Save and display OOD scores (#814)
mihow Apr 30, 2025
aa05142
Fields for Taxon reference images (#822)
mihow May 1, 2025
a3f2034
Prepare species details view for print mode (#813)
annavik May 1, 2025
2b1a470
fix: make sure taxon label is not overflowing its container
annavik May 5, 2025
edac77b
Support for clustering detections (#818)
mohamedelabbas1996 May 7, 2025
2466c50
Merge branch 'main' into deployments/ood.antenna.insectai.org
mihow May 7, 2025
977d773
Merge branch 'main' into deployments/ood.antenna.insectai.org
mihow May 7, 2025
92a24d1
Import cover images & external references (#838)
mihow May 8, 2025
ee5f218
feat: allow sorting taxa by cover image
mihow May 8, 2025
9065893
Merge branch 'deployments/ood.antenna.insectai.org' of github.com:Rol…
mihow May 8, 2025
a1237cd
Backend support for OOD score threshold filter (#840)
mihow May 11, 2025
e39541a
Improve bulk identification workflow (#841)
mihow May 12, 2025
3d5efa9
Update Occurrence model to determine & save best examples (#837)
mihow May 13, 2025
5c9070c
UI tweaks for OOD project (#842)
annavik May 13, 2025
ebe5341
Make it possible to suggest ID in bulk (#847)
annavik May 14, 2025
5544a1d
fix: tweak cover image labels and captions
annavik May 15, 2025
65810c1
fix: handle undefined OOD score (#848)
annavik May 15, 2025
2d11e9a
fix: tweak logic for what images to present in detail view to avoid d…
annavik May 15, 2025
61ce748
feat: add thumbnails to taxa search (#851)
annavik May 16, 2025
f6f69bc
Make it possible to edit unknown species from UI (#843)
annavik May 16, 2025
5302486
Add support for Taxa Tags (#830)
mohamedelabbas1996 May 16, 2025
247e738
Add unknown species filter control (#850)
annavik May 16, 2025
a6bead3
layout: update element order after merge
annavik May 16, 2025
a4dd173
Improve representation of generated clusters (#849)
mihow May 16, 2025
87cd01f
Make it possible to register new clusters from UI (#855)
annavik May 22, 2025
8ecd85c
Clean up global vs. project taxa & API responses (#853)
mihow May 23, 2025
4d6a58d
Add best score filtering and column settings support (#856)
annavik May 23, 2025
d928ba3
fix: change default filter key typo
annavik May 23, 2025
684aa9f
fix: make project id required for taxa search
annavik May 23, 2025
ef0b975
chore: update default column settings
annavik May 23, 2025
c44633e
fix: bring back missing tag filter
annavik May 23, 2025
a426255
fix: pass project ID when assigning tags
annavik May 23, 2025
decadbf
fix: pass project ID with taxa request
annavik May 24, 2025
8968113
fix: update reject option ID:s for the OOD environment
annavik May 24, 2025
1819ffb
fix: update cluster images in bulk after creation (#859)
mihow May 25, 2025
78b1c48
fix: increase number of connections allowed in staging DB (#860)
mihow May 25, 2025
3755596
fix: don't add parents of imported taxa to list
mihow May 25, 2025
8942b72
feat: update taxa detail view with links to child taxa
annavik May 25, 2025
4fa88a0
Merge branch 'main' into deployments/ood.antenna.insectai.org
annavik May 26, 2025
ce334db
fix: ensure all occurrences in list view have a determination (#861)
mihow May 26, 2025
cd56199
Merge branch 'deployments/ood.antenna.insectai.org' of https://github…
annavik May 26, 2025
3528f27
Merge branch 'main' of github.com:RolnickLab/antenna into deployments…
mihow Aug 21, 2025
e1c8d5f
fix: try to fix build error in Netlify
mihow Aug 21, 2025
19fa180
Revert "fix: try to fix build error in Netlify"
mihow Aug 21, 2025
eb132f5
fix: remove package-lock.json and update yarn.lock to fix build problems
annavik Aug 21, 2025
8c158f6
fix: remove duplicated column
annavik Oct 21, 2025
97bd433
chore: align button styles
annavik Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ env:

on:
pull_request:
branches: ["master", "main"]
branches: ["main", "deployments/*", "releases/*"]
paths-ignore: ["docs/**", "ui/**"]

push:
branches: ["master", "main"]
branches: ["main", "deployments/*", "releases/*"]
paths-ignore: ["docs/**", "ui/**"]

concurrency:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ env:

on:
pull_request:
branches: ["master", "main"]
branches: ["main", "deployments/*", "releases/*"]
paths:
- "!./**"
- "ui/**"

push:
branches: ["master", "main"]
branches: ["main", "deployments/*", "releases/*"]
paths:
- "!./**"
- "ui/**"
Expand Down
2 changes: 1 addition & 1 deletion ami/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ProjectMixin:
request: rest_framework.request.Request
kwargs: dict

def get_active_project(self) -> Project:
def get_active_project(self) -> Project | None:
from ami.base.serializers import SingleParamSerializer

param = "project_id"
Expand Down
29 changes: 29 additions & 0 deletions ami/jobs/migrations/0017_alter_job_job_type_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.10 on 2025-04-24 16:25

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("jobs", "0016_job_data_export_job_params_alter_job_job_type_key"),
]

operations = [
migrations.AlterField(
model_name="job",
name="job_type_key",
field=models.CharField(
choices=[
("ml", "ML pipeline"),
("populate_captures_collection", "Populate captures collection"),
("data_storage_sync", "Data storage sync"),
("unknown", "Unknown"),
("data_export", "Data Export"),
("occurrence_clustering", "Occurrence Feature Clustering"),
],
default="unknown",
max_length=255,
verbose_name="Job Type",
),
),
]
29 changes: 29 additions & 0 deletions ami/jobs/migrations/0018_alter_job_job_type_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.10 on 2025-04-28 11:06

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("jobs", "0017_alter_job_job_type_key"),
]

operations = [
migrations.AlterField(
model_name="job",
name="job_type_key",
field=models.CharField(
choices=[
("ml", "ML pipeline"),
("populate_captures_collection", "Populate captures collection"),
("data_storage_sync", "Data storage sync"),
("unknown", "Unknown"),
("data_export", "Data Export"),
("detection_clustering", "Detection Feature Clustering"),
],
default="unknown",
max_length=255,
verbose_name="Job Type",
),
),
]
44 changes: 42 additions & 2 deletions ami/jobs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,8 @@ def run(cls, job: "Job"):
total_classifications = 0

config = job.pipeline.get_config(project_id=job.project.pk)
chunk_size = config.get("request_source_image_batch_size", 1)
chunk_size = config.get("request_source_image_batch_size", 2)
# @TODO Ensure only images of the same dimensions are processed in a batch
chunks = [images[i : i + chunk_size] for i in range(0, image_count, chunk_size)] # noqa
request_failed_images = []

Expand Down Expand Up @@ -640,6 +641,38 @@ def run(cls, job: "Job"):
job.update_status(JobState.SUCCESS, save=True)


class DetectionClusteringJob(JobType):
name = "Detection Feature Clustering"
key = "detection_clustering"

@classmethod
def run(cls, job: "Job"):
job.update_status(JobState.STARTED)
job.started_at = datetime.datetime.now()
job.finished_at = None
job.progress.add_stage(name="Collecting Features", key="feature_collection")
job.progress.add_stage("Clustering", key="clustering")
job.progress.add_stage("Creating Unknown Taxa", key="create_unknown_taxa")
job.save()

if not job.source_image_collection:
raise ValueError("No source image collection provided")

job.logger.info(f"Clustering detections for collection {job.source_image_collection}")
job.update_status(JobState.STARTED)
job.started_at = datetime.datetime.now()
job.finished_at = None
job.save()

# Call the clustering method
job.source_image_collection.cluster_detections(job=job)
job.logger.info(f"Finished clustering detections for collection {job.source_image_collection}")

job.finished_at = datetime.datetime.now()
job.update_status(JobState.SUCCESS, save=False)
job.save()


class UnknownJobType(JobType):
name = "Unknown"
key = "unknown"
Expand All @@ -649,7 +682,14 @@ def run(cls, job: "Job"):
raise ValueError(f"Unknown job type '{job.job_type()}'")


VALID_JOB_TYPES = [MLJob, SourceImageCollectionPopulateJob, DataStorageSyncJob, UnknownJobType, DataExportJob]
VALID_JOB_TYPES = [
MLJob,
SourceImageCollectionPopulateJob,
DataStorageSyncJob,
UnknownJobType,
DataExportJob,
DetectionClusteringJob,
]


def get_job_type_by_key(key: str) -> type[JobType] | None:
Expand Down
63 changes: 60 additions & 3 deletions ami/main/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.http.request import HttpRequest
from django.template.defaultfilters import filesizeformat
from django.utils.formats import number_format
from django.utils.html import format_html
from guardian.admin import GuardedModelAdmin

import ami.utils
Expand Down Expand Up @@ -245,7 +246,6 @@ def update_calculated_fields(self, request: HttpRequest, queryset: QuerySet[Even
self.message_user(request, f"Updated {queryset.count()} events.")

list_filter = ("deployment", "project", "start")
actions = [update_calculated_fields]


@admin.register(SourceImage)
Expand Down Expand Up @@ -287,20 +287,27 @@ class ClassificationInline(admin.TabularInline):
model = Classification
extra = 0
fields = (
"view_classification",
"taxon",
"algorithm",
"timestamp",
"terminal",
"created_at",
)
readonly_fields = (
"view_classification",
"taxon",
"algorithm",
"timestamp",
"terminal",
"created_at",
)

@admin.display(description="Classification")
def view_classification(self, obj):
url = f"/admin/main/classification/{obj.pk}/change/"
return format_html('<a href="{}">{}</a>', url, obj.pk)

def get_queryset(self, request: HttpRequest) -> QuerySet[Any]:
qs = super().get_queryset(request)
return qs.select_related("taxon", "algorithm", "detection")
Expand All @@ -310,20 +317,27 @@ class DetectionInline(admin.TabularInline):
model = Detection
extra = 0
fields = (
"view_detection",
"detection_algorithm",
"source_image",
"timestamp",
"created_at",
"occurrence",
)
readonly_fields = (
"view_detection",
"detection_algorithm",
"source_image",
"timestamp",
"created_at",
"occurrence",
)

@admin.display(description="Detection")
def view_detection(self, obj):
url = f"/admin/main/detection/{obj.pk}/change/"
return format_html('<a href="{}">{}</a>', url, obj.pk)


@admin.register(Detection)
class DetectionAdmin(admin.ModelAdmin[Detection]):
Expand All @@ -340,6 +354,7 @@ class DetectionAdmin(admin.ModelAdmin[Detection]):
)

autocomplete_fields = ("source_image", "occurrence")
search_fields = ("id",)

def get_queryset(self, request: HttpRequest) -> QuerySet[Any]:
qs = super().get_queryset(request)
Expand Down Expand Up @@ -430,6 +445,7 @@ class ClassificationAdmin(admin.ModelAdmin[Classification]):
"detection__source_image__project",
"taxon__rank",
)
autocomplete_fields = ("taxon", "detection")

def get_queryset(self, request: HttpRequest) -> QuerySet[Any]:
qs = super().get_queryset(request)
Expand Down Expand Up @@ -485,7 +501,7 @@ class TaxonAdmin(admin.ModelAdmin[Taxon]):
"created_at",
"updated_at",
)
list_filter = ("lists", "rank", TaxonParentFilter)
list_filter = ("unknown_species", "lists", "rank", TaxonParentFilter)
search_fields = ("name",)
autocomplete_fields = (
"parent",
Expand Down Expand Up @@ -618,7 +634,48 @@ def populate_collection_async(self, request: HttpRequest, queryset: QuerySet[Sou
f"Populating {len(queued_tasks)} collection(s) background tasks: {queued_tasks}.",
)

actions = [populate_collection, populate_collection_async]
@admin.action(description="Create clustering job (but don't run it)")
@admin.action()
def create_clustering_job(self, request: HttpRequest, queryset: QuerySet[SourceImageCollection]) -> None:
from ami.jobs.models import DetectionClusteringJob, Job

for collection in queryset:
job = Job.objects.create(
name=f"Clustering detections for collection {collection.pk}",
project=collection.project,
source_image_collection=collection,
job_type_key=DetectionClusteringJob.key,
params={
"ood_threshold": 0.3,
"algorithm": "agglomerative",
"algorithm_kwargs": {"distance_threshold": 80},
"pca": {"n_components": 384},
},
)
self.message_user(request, f"Created clustering job #{job.pk} for collection #{collection.pk}")

@admin.action()
def cluster_detections(self, request: HttpRequest, queryset: QuerySet[SourceImageCollection]) -> None:
for collection in queryset:
from ami.jobs.models import DetectionClusteringJob, Job

job = Job.objects.create(
name=f"Clustering detections for collection {collection.pk}",
project=collection.project,
source_image_collection=collection,
job_type_key=DetectionClusteringJob.key,
params={
"ood_threshold": 0.3,
"algorithm": "agglomerative",
"algorithm_kwargs": {"distance_threshold": 80},
"pca": {"n_components": 384},
},
)
job.enqueue()

self.message_user(request, f"Clustered {queryset.count()} collection(s).")

actions = [populate_collection, populate_collection_async, cluster_detections, create_clustering_job]

# Hide images many-to-many field from form. This would list all source images in the database.
exclude = ("images",)
Expand Down
Loading