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
f1c3782
initial testing and integration
BryonLewis Jan 16, 2026
db3834a
batbot metadata parser
BryonLewis Jan 16, 2026
4253365
Merge branch 'main' into batbot-integration
BryonLewis Jan 19, 2026
5c3d9a8
batbot spectrogram generation
BryonLewis Jan 22, 2026
1c24339
remove old spectrogram generation code
BryonLewis Jan 22, 2026
8878620
swap back to using the github installation for batbot
BryonLewis Jan 26, 2026
e96da72
Merge branch 'batbot-integration' of https://github.com/Kitware/batai…
BryonLewis Jan 26, 2026
9623991
use temp branch for start/stop fixes
BryonLewis Jan 27, 2026
a1d5eed
increase accuracy for spectrograms and annotations
BryonLewis Jan 27, 2026
a438016
thumbnail centering fixes
BryonLewis Jan 27, 2026
b1ae312
add noise filter
BryonLewis Jan 28, 2026
4aa3016
contour testing
BryonLewis Jan 28, 2026
6cc0530
contours backend
BryonLewis Jan 29, 2026
8f1ff79
contour support
BryonLewis Jan 29, 2026
02d6d27
update batbot
BryonLewis Jan 29, 2026
9fcea27
Merge branch 'batbot-integration' into batbot-contours
BryonLewis Jan 29, 2026
028843b
fix contour width calculations
BryonLewis Jan 29, 2026
968a433
contour opacity settings
BryonLewis Jan 29, 2026
a5ebef3
contour testing
BryonLewis Jan 30, 2026
439dae3
Merge branch 'main' into batbot-integration
BryonLewis Jan 30, 2026
d95e58b
fix NABat spectrogram generation
BryonLewis Jan 30, 2026
0bed12a
client linting
BryonLewis Jan 30, 2026
dacd997
removing integration notes
BryonLewis Jan 30, 2026
58f29d1
Merge branch 'batbot-integration' into batbot-contours
BryonLewis Jan 30, 2026
52f9ba6
use masks for contours
BryonLewis Jan 30, 2026
6a6fa8f
save mask images along compressed spectrograms
BryonLewis Jan 30, 2026
6b43dfe
contour and mask UI
BryonLewis Jan 30, 2026
cc1c694
Update client/src/components/TransparencyFilterControl.vue
BryonLewis Feb 3, 2026
9747b84
reverting float to ints for pixel fields
BryonLewis Feb 3, 2026
78c1093
remove uneeded dependencies
BryonLewis Feb 3, 2026
bf1fa9c
Merge branch 'main' into batbot-integration
BryonLewis Feb 3, 2026
856081b
import GRTS updated for sciencebase.gov downtime
BryonLewis Feb 3, 2026
29b0d5d
add batbot issue for ml model integration
BryonLewis Feb 3, 2026
ba11371
main merge migration update
BryonLewis Feb 3, 2026
b18710b
swap to port 8080 for client based on main's client redirect port
BryonLewis Feb 3, 2026
b50e74d
Merge branch 'batbot-integration' into batbot-contours
BryonLewis Feb 3, 2026
25d4475
remaking migrations
BryonLewis Feb 3, 2026
f3bbc7d
Merge branch 'main' into batbot-contours
BryonLewis Feb 4, 2026
b7abb14
remove svgwrite depedency
BryonLewis Feb 4, 2026
21ee18f
rename extract_contours script
BryonLewis Feb 4, 2026
3bc9f65
client side fixes to contour toggling
BryonLewis Feb 4, 2026
1531d4c
update pulse metadata if the compute spectrogram is run again
BryonLewis Feb 4, 2026
c7b4c10
remove vetting details print
BryonLewis Feb 4, 2026
10a85b8
make contours optional for pulseMetadata
BryonLewis Feb 4, 2026
49f5cad
update migrations
BryonLewis Feb 4, 2026
b7acb5c
swap to batbot main branch
BryonLewis Feb 5, 2026
fb305b0
Merge branch 'main' into batbot-contours
BryonLewis Feb 5, 2026
4f4f313
Update client/src/use/useState.ts
BryonLewis Feb 5, 2026
e49c63b
Update client/src/views/Spectrogram.vue
BryonLewis Feb 5, 2026
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_metadata import PulseMetadataAdmin
from .recording import RecordingAdmin
from .recording_annotations import RecordingAnnotationAdmin
from .recording_tag import RecordingTagAdmin
Expand All @@ -34,6 +35,7 @@
'ExportedAnnotationFileAdmin',
'SpectrogramImageAdmin',
'VettingDetailsAdmin',
'PulseMetadataAdmin',
# NABat Models
'NABatRecordingAnnotationAdmin',
'NABatCompressedSpectrogramAdmin',
Expand Down
12 changes: 12 additions & 0 deletions bats_ai/core/admin/compressed_spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class CompressedSpectrogramAdmin(admin.ModelAdmin):
'starts',
'stops',
'image_url_list_display',
'mask_url_list_display',
]
list_display_links = ['pk', 'recording', 'spectrogram']
list_select_related = True
Expand All @@ -28,6 +29,7 @@ class CompressedSpectrogramAdmin(admin.ModelAdmin):
'starts',
'stops',
'image_url_list_display',
'mask_url_list_display',
]

@admin.display(description='Image URLs')
Expand All @@ -39,3 +41,13 @@ def image_url_list_display(self, obj):
return format_html_join(
'\n', '<div><a href="{}" target="_blank">{}</a></div>', ((url, url) for url in urls)
)

@admin.display(description='Mask URLs')
def mask_url_list_display(self, obj):
"""Render each mask URL as a clickable link in admin detail view."""
urls = obj.mask_url_list
if not urls:
return '(No masks)'
return format_html_join(
'\n', '<div><a href="{}" target="_blank">{}</a></div>', ((url, url) for url in urls)
)
9 changes: 9 additions & 0 deletions bats_ai/core/admin/pulse_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib import admin

from bats_ai.core.models import PulseMetadata


@admin.register(PulseMetadata)
class PulseMetadataAdmin(admin.ModelAdmin):
list_display = ('recording', 'index', 'bounding_box')
list_select_related = True
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.23 on 2026-02-04 17:02

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


class Migration(migrations.Migration):
dependencies = [
('core', '0027_alter_annotations_end_time_and_more'),
]

operations = [
migrations.AlterField(
model_name='spectrogramimage',
name='type',
field=models.CharField(
choices=[
('spectrogram', 'Spectrogram'),
('compressed', 'Compressed'),
('masks', 'Masks'),
],
default='spectrogram',
max_length=20,
),
),
migrations.CreateModel(
name='PulseMetadata',
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(blank=True, null=True)),
(
'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_metadata import PulseMetadata
from .recording import Recording, RecordingTag
from .recording_annotation import RecordingAnnotation
from .recording_annotation_status import RecordingAnnotationStatus
Expand All @@ -23,6 +24,7 @@
'SequenceAnnotations',
'GRTSCells',
'CompressedSpectrogram',
'PulseMetadata',
'RecordingAnnotation',
'Configuration',
'ProcessingTask',
Expand Down
12 changes: 12 additions & 0 deletions bats_ai/core/models/compressed_spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,24 @@ 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 mask_url_list(self):
"""Ordered list of mask image URLs for this spectrogram."""
images = self.images.filter(type='masks').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."""
images = self.images.filter(type='compressed').order_by('index')
return [Image.open(img.image_file) for img in images]

@property
def mask_pil_list(self):
"""List of PIL mask images in order."""
images = self.images.filter(type='masks').order_by('index')
return [Image.open(img.image_file) for img in images]

@property
def image_np(self):
"""Combined image as a single numpy array by horizontal stacking."""
Expand Down
12 changes: 12 additions & 0 deletions bats_ai/core/models/nabat/nabat_compressed_spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,24 @@ 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 mask_url_list(self):
"""Ordered list of mask image URLs for this spectrogram."""
images = self.images.filter(type='masks').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."""
images = self.images.filter(type='compressed').order_by('index')
return [Image.open(img.image_file) for img in images]

@property
def mask_pil_list(self):
"""List of PIL mask images in order."""
images = self.images.filter(type='masks').order_by('index')
return [Image.open(img.image_file) for img in images]

@property
def image_np(self):
"""Combined image as a single numpy array by horizontal stacking."""
Expand Down
11 changes: 11 additions & 0 deletions bats_ai/core/models/pulse_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.contrib.gis.db import models

from .recording import Recording


class PulseMetadata(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(null=True, blank=True)
# TODO: Add in metadata from batbot
1 change: 1 addition & 0 deletions bats_ai/core/models/spectrogram_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class SpectrogramImage(models.Model):
SPECTROGRAM_TYPE_CHOICES = [
('spectrogram', 'Spectrogram'),
('compressed', 'Compressed'),
('masks', 'Masks'),
]
content_object = GenericForeignKey('content_type', 'object_id')

Expand Down
34 changes: 34 additions & 0 deletions bats_ai/core/tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
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,
Configuration,
PulseMetadata,
Recording,
RecordingAnnotation,
Species,
Expand Down Expand Up @@ -88,6 +90,38 @@ def recording_compute_spectrogram(recording_id: int):
},
)

# Save mask images (from batbot metadata mask_path)
for idx, mask_path in enumerate(compressed.get('masks', [])):
with open(mask_path, 'rb') as f:
SpectrogramImage.objects.get_or_create(
content_type=ContentType.objects.get_for_model(compressed_obj),
object_id=compressed_obj.id,
index=idx,
type='masks',
defaults={
'image_file': File(f, name=os.path.basename(mask_path)),
},
)

# Create SpectrogramContour objects for each segment
for segment in results['segments']['segments']:
PulseMetadata.objects.update_or_create(
recording=compressed_obj.recording,
index=segment['segment_index'],
defaults={
'contours': segment.get('contours', []),
'bounding_box': Polygon(
(
(segment['start_ms'], segment['freq_max']),
(segment['stop_ms'], segment['freq_max']),
(segment['stop_ms'], segment['freq_min']),
(segment['start_ms'], segment['freq_min']),
(segment['start_ms'], segment['freq_max']),
)
),
},
)

config = Configuration.objects.first()
# TODO: Disabled until prediction is in batbot
# https://github.com/Kitware/batbot/issues/29
Expand Down
33 changes: 33 additions & 0 deletions bats_ai/core/utils/batbot_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import batbot
from pydantic import BaseModel, ConfigDict, Field, field_validator

from .contour_utils import process_spectrogram_assets_for_contours


class SpectrogramMetadata(BaseModel):
"""Metadata about the spectrogram."""

uncompressed_path: list[str] = Field(alias='uncompressed.path')
compressed_path: list[str] = Field(alias='compressed.path')
mask_path: list[str] = Field(alias='mask.path')


class UncompressedSize(BaseModel):
Expand Down Expand Up @@ -227,19 +230,43 @@ class SpectrogramAssetResult(TypedDict):

class SpectrogramCompressedAssetResult(TypedDict):
paths: list[str]
masks: list[str]
width: int
height: int
widths: list[float]
starts: list[float]
stops: list[float]


class SpectrogramContour(TypedDict):
level: float
curve: list[list[float]]
index: int


class SpectrogramContourSegment(TypedDict):
segment_index: int
contour_count: int
freq_min: float
freq_max: float
contours: list[SpectrogramContour]
width_px: float
start_ms: float
stop_ms: float


class SpectrogramContours(TypedDict):
segments: list[SpectrogramContourSegment]
total_segments: int


class SpectrogramAssets(TypedDict):
duration: float
freq_min: int
freq_max: int
normal: SpectrogramAssetResult
compressed: SpectrogramCompressedAssetResult
segments: SpectrogramContours | None


@contextmanager
Expand All @@ -261,6 +288,7 @@ def generate_spectrogram_assets(recording_path: str, output_folder: str):
# from the metadata we should have the images that are used
uncompressed_paths = metadata.spectrogram.uncompressed_path
compressed_paths = metadata.spectrogram.compressed_path
mask_paths = metadata.spectrogram.mask_path

metadata.frequencies.min_hz
metadata.frequencies.max_hz
Expand All @@ -277,11 +305,16 @@ def generate_spectrogram_assets(recording_path: str, output_folder: str):
},
'compressed': {
'paths': compressed_paths,
'masks': mask_paths,
'width': metadata.size.compressed.width_px,
'height': metadata.size.compressed.height_px,
'widths': compressed_metadata.widths,
'starts': compressed_metadata.starts,
'stops': compressed_metadata.stops,
},
}

segments_data = process_spectrogram_assets_for_contours(result)
result['segments'] = segments_data

return result
Loading