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
4 changes: 4 additions & 0 deletions bats_ai/core/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
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
from .sequence_annotations import SequenceAnnotationsAdmin
from .species import SpeciesAdmin
from .spectrogram import SpectrogramAdmin
from .spectrogram_image import SpectrogramImageAdmin
from .spectrogram_svg import SpectrogramSvgAdmin

__all__ = [
'AnnotationsAdmin',
Expand All @@ -34,9 +36,11 @@
'ConfigurationAdmin',
'ExportedAnnotationFileAdmin',
'SpectrogramImageAdmin',
'SpectrogramSvgAdmin',
# NABat Models
'NABatRecordingAnnotationAdmin',
'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
22 changes: 22 additions & 0 deletions bats_ai/core/admin/spectrogram_svg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib import admin

from bats_ai.core.models import SpectrogramSvg


@admin.register(SpectrogramSvg)
class SpectrogramSvgAdmin(admin.ModelAdmin):
list_display = [
'pk',
'content_type',
'object_id',
'index',
'image_file',
]
list_select_related = True
readonly_fields = [
'pk',
'content_type',
'object_id',
'index',
'image_file',
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 4.2.23 on 2025-12-08 22:19

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


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0023_recordingtag_recording_tags_and_more'),
]

operations = [
migrations.CreateModel(
name='SpectrogramSvg',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('type', models.CharField(choices=[('spectrogram', 'Spectrogram'), ('compressed', 'Compressed')], default='spectrogram', max_length=20)),
('index', models.PositiveIntegerField()),
('image_file', models.FileField(upload_to=bats_ai.core.models.spectrogram_vector.spectrogram_svg_upload_to)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'ordering': ['index'],
},
),
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')),
],
),
]
4 changes: 4 additions & 0 deletions bats_ai/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from .grts_cells import GRTSCells
from .image import Image
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
from .sequence_annotations import SequenceAnnotations
from .species import Species
from .spectrogram import Spectrogram
from .spectrogram_image import SpectrogramImage
from .spectrogram_vector import SpectrogramSvg

__all__ = [
'Annotations',
Expand All @@ -30,4 +32,6 @@
'ProcessingTaskType',
'ExportedAnnotationFile',
'SpectrogramImage',
'SpectrogramSvg',
'ComputedPulseAnnotation',
]
7 changes: 7 additions & 0 deletions bats_ai/core/models/compressed_spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .recording import Recording
from .spectrogram import Spectrogram
from .spectrogram_image import SpectrogramImage
from .spectrogram_vector import SpectrogramSvg


# TimeStampedModel also provides "created" and "modified" fields
Expand All @@ -17,6 +18,7 @@ class CompressedSpectrogram(TimeStampedModel, models.Model):
spectrogram = models.ForeignKey(Spectrogram, on_delete=models.CASCADE)
length = models.IntegerField()
images = GenericRelation(SpectrogramImage)
vector_images = GenericRelation(SpectrogramSvg)
starts = ArrayField(ArrayField(models.IntegerField()))
stops = ArrayField(ArrayField(models.IntegerField()))
widths = ArrayField(ArrayField(models.IntegerField()))
Expand All @@ -28,6 +30,11 @@ def image_url_list(self):
images = self.images.filter(type='compressed').order_by('index')
return [default_storage.url(img.image_file.name) for img in images]

@property
def vector_url_list(self):
images = self.vector_images.filter(type='compressed').order_by('index')
return [default_storage.url(img.image_file.name) for img in images]

@property
def image_pil_list(self):
"""List of PIL images in order."""
Expand Down
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()
7 changes: 7 additions & 0 deletions bats_ai/core/models/spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

from .recording import Recording
from .spectrogram_image import SpectrogramImage
from .spectrogram_vector import SpectrogramSvg


class Spectrogram(TimeStampedModel, models.Model):
recording = models.ForeignKey(Recording, on_delete=models.CASCADE)
images = GenericRelation(SpectrogramImage)
vector_images = GenericRelation(SpectrogramSvg)
width = models.IntegerField() # pixels
height = models.IntegerField() # pixels
duration = models.IntegerField() # milliseconds
Expand All @@ -24,6 +26,11 @@ def image_url_list(self):
images = self.images.filter(type='spectrogram').order_by('index')
return [default_storage.url(img.image_file.name) for img in images]

@property
def vector_url_list(self):
images = self.vector_images.filter(type='spectrogram').order_by('index')
return [default_storage.url(img.image_file.name) for img in images]

@property
def image_pil_list(self):
"""List of PIL images in order."""
Expand Down
43 changes: 43 additions & 0 deletions bats_ai/core/models/spectrogram_vector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.dispatch import receiver


def spectrogram_svg_upload_to(instance, filename):
related = instance.content_object

recording = getattr(related, 'recording', None) or getattr(related, 'nabat_recording', None)
recording_id = getattr(recording, 'id', None)

if not recording_id:
raise ValueError('Related content must have a recording or nabat_recording.')

return f'recording_{recording_id}/{instance.type}/svg_{instance.index}_{filename}'


class SpectrogramSvg(models.Model):
SPECTROGRAM_TYPE_CHOICES = [
('spectrogram', 'Spectrogram'),
('compressed', 'Compressed'),
]
content_object = GenericForeignKey('content_type', 'object_id')

content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
type = models.CharField(
max_length=20,
choices=SPECTROGRAM_TYPE_CHOICES,
default='spectrogram',
)
index = models.PositiveIntegerField()
image_file = models.FileField(upload_to=spectrogram_svg_upload_to)

class Meta:
ordering = ['index']


@receiver(models.signals.pre_delete, sender=SpectrogramSvg)
def delete_content(sender, instance, **kwargs):
if instance.image_file:
instance.image_file.delete(save=False)
41 changes: 40 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 @@ -127,6 +128,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 @@ -373,6 +390,7 @@ def get_spectrogram(request: HttpRequest, id: int):

spectro_data = {
'urls': spectrogram.image_url_list,
'vectors': spectrogram.vector_url_list,
'spectroInfo': {
'spectroId': spectrogram.pk,
'width': spectrogram.width,
Expand Down Expand Up @@ -443,6 +461,7 @@ def get_spectrogram_compressed(request: HttpRequest, id: int):

spectro_data = {
'urls': compressed_spectrogram.image_url_list,
'vectors': compressed_spectrogram.vector_url_list,
'spectroInfo': {
'spectroId': compressed_spectrogram.pk,
'width': compressed_spectrogram.spectrogram.width,
Expand Down Expand Up @@ -526,6 +545,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
Loading