Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions bats_ai/core/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
NABatSpectrogramAdmin,
)
from .processing_task import ProcessingTaskAdmin
from .pulse_annotation import ComputedPulseAnnotationAdmin
from .recording import RecordingAdmin
from .recording_annotations import RecordingAnnotationAdmin
from .recording_tag import RecordingTagAdmin
Expand Down Expand Up @@ -39,4 +40,5 @@
'NABatCompressedSpectrogramAdmin',
'NABatSpectrogramAdmin',
'NABatRecordingAdmin',
'ComputedPulseAnnotationAdmin',
]
13 changes: 13 additions & 0 deletions bats_ai/core/admin/pulse_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib import admin

from bats_ai.core.models import ComputedPulseAnnotation


@admin.register(ComputedPulseAnnotation)
class ComputedPulseAnnotationAdmin(admin.ModelAdmin):
list_display = [
'id',
'recording',
'bounding_box',
]
list_select_related = True
35 changes: 35 additions & 0 deletions bats_ai/core/migrations/0026_computedpulseannotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.23 on 2026-01-21 22:46

import django.contrib.gis.db.models.fields
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('core', '0025_configuration_mark_annotations_completed_enabled_and_more'),
]

operations = [
migrations.CreateModel(
name='ComputedPulseAnnotation',
fields=[
(
'id',
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
('index', models.IntegerField()),
('bounding_box', django.contrib.gis.db.models.fields.PolygonField(srid=4326)),
('contours', models.JSONField()),
(
'recording',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='core.recording'
),
),
],
),
]
2 changes: 2 additions & 0 deletions bats_ai/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .exported_file import ExportedAnnotationFile
from .grts_cells import GRTSCells
from .processing_task import ProcessingTask, ProcessingTaskType
from .pulse_annotation import ComputedPulseAnnotation
from .recording import Recording, RecordingTag
from .recording_annotation import RecordingAnnotation
from .recording_annotation_status import RecordingAnnotationStatus
Expand All @@ -29,5 +30,6 @@
'ProcessingTaskType',
'ExportedAnnotationFile',
'SpectrogramImage',
'ComputedPulseAnnotation',
'VettingDetails',
]
10 changes: 10 additions & 0 deletions bats_ai/core/models/pulse_annotation.py
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Rename file to computed_pulse_annotation.py

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib.gis.db import models

from .recording import Recording


class ComputedPulseAnnotation(models.Model):
recording = models.ForeignKey(Recording, on_delete=models.CASCADE)
index = models.IntegerField(null=False, blank=False)
bounding_box = models.PolygonField(null=False, blank=False)
contours = models.JSONField()
41 changes: 41 additions & 0 deletions bats_ai/core/tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import tempfile

from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.geos import Polygon
from django.core.files import File

from bats_ai.celery import app
from bats_ai.core.models import (
CompressedSpectrogram,
ComputedPulseAnnotation,
Configuration,
Recording,
RecordingAnnotation,
Expand Down Expand Up @@ -78,6 +80,45 @@ def recording_compute_spectrogram(recording_id: int):
},
)

# Generate computed annotations for contours
logger.info(
'Adding contour and bounding boxes for ' f'{len(results.get("contours", []))} pulses'
)
for idx, contour in enumerate(results.get('contours', [])):
# Transform contour (x, y) pairs into (time, freq) pairs
widths, starts, stops = compressed['widths'], compressed['starts'], compressed['stops']
start_time = starts[idx]
end_time = stops[idx]
width = widths[idx]
time_per_pixel = (end_time - start_time) / width
mhz_per_pixel = (results['freq_max'] - results['freq_min']) / compressed['height']
transformed_lines = []
for contour_line in contour:
new_curve = [
[
point[0] * time_per_pixel + start_time,
results['freq_max'] - (point[1] * mhz_per_pixel),
]
for point in contour_line['curve']
]
transformed_lines.append(
{'curve': new_curve, 'level': contour_line['level'], 'index': idx}
)
ComputedPulseAnnotation.objects.get_or_create(
index=idx,
recording=recording,
contours=transformed_lines,
bounding_box=Polygon(
(
(start_time, results['freq_max']),
(end_time, results['freq_max']),
(end_time, results['freq_min']),
(start_time, results['freq_min']),
(start_time, results['freq_max']),
)
),
)

config = Configuration.objects.first()
if config and config.run_inference_on_upload:
predict_results = predict_from_compressed(compressed_obj)
Expand Down
39 changes: 38 additions & 1 deletion bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
import json
import logging
from typing import List, Optional
from typing import Any, List, Optional

from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
Expand All @@ -16,6 +16,7 @@
from bats_ai.core.models import (
Annotations,
CompressedSpectrogram,
ComputedPulseAnnotation,
Recording,
RecordingAnnotation,
RecordingTag,
Expand Down Expand Up @@ -129,6 +130,22 @@ class UpdateAnnotationsSchema(Schema):
id: int | None


class ComputedPulseAnnotationSchema(Schema):
id: int | None
index: int
bounding_box: Any
contours: list

@classmethod
def from_orm(cls, obj: ComputedPulseAnnotation):
return cls(
id=obj.id,
index=obj.index,
contours=obj.contours,
bounding_box=json.loads(obj.bounding_box.geojson),
)


@router.post('/')
def create_recording(
request: HttpRequest,
Expand Down Expand Up @@ -542,6 +559,26 @@ def get_annotations(request: HttpRequest, id: int):
return {'error': 'Recording not found'}


@router.get('/{id}/pulse_data')
def get_pulse_data(request: HttpRequest, id: int):
try:
recording = Recording.objects.get(pk=id)
if recording.owner == request.user or recording.public:
computed_pulse_annotation_qs = ComputedPulseAnnotation.objects.filter(
recording=recording
).order_by('index')
return [
ComputedPulseAnnotationSchema.from_orm(pulse)
for pulse in computed_pulse_annotation_qs.all()
]
else:
return {
'error': 'Permission denied. You do not own this recording, and it is not public.'
}
except Recording.DoesNotExist:
return {'error': 'Recording not found'}


@router.get('/{id}/annotations/other_users')
def get_other_user_annotations(request: HttpRequest, id: int):
try:
Expand Down
Loading