Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
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
Copy Markdown
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