Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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