Skip to content

Commit

Permalink
Annotation Viewer/Editor (#13)
Browse files Browse the repository at this point in the history
* initialize geoJS annotation viewer

* basic annotation rendering, beginning annotation editing
  • Loading branch information
BryonLewis authored Jan 10, 2024
1 parent 505001f commit 166b35e
Show file tree
Hide file tree
Showing 14 changed files with 985 additions and 25 deletions.
22 changes: 20 additions & 2 deletions bats_ai/core/models/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def generate_spectrogram(self):

# Plot and save the spectrogram
plt.figure(figsize=(10, 4))
librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='log')
librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='linear')
plt.colorbar(format='%+2.0f dB')
plt.title('Spectrogram')
plt.xlabel('Time')
Expand All @@ -49,4 +49,22 @@ def generate_spectrogram(self):

plt.close()

return base64_image
start_time = 0.0
end_time = librosa.get_duration(y=y, sr=sr) * 1000 # in milliseconds
low_frequency = 0 # Set your desired low frequency
high_frequency = sr / 2 # Set your desired high frequency
image_width = 10 * 100 # 10 inches at 100 dpi
image_height = 4 * 100 # 4 inches at 100 dpi

# Return dictionary with all required fields
return {
'base64_spectrogram': base64_image,
'spectroInfo': {
'width': image_width,
'height': image_height,
'start_time': start_time,
'end_time': end_time,
'low_freq': low_frequency,
'high_freq': high_frequency,
},
}
32 changes: 32 additions & 0 deletions bats_ai/core/views/annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from django.http import HttpRequest
from ninja import Schema
from ninja.errors import HttpError
from ninja.pagination import RouterPaginated
from oauth2_provider.models import AccessToken

logger = logging.getLogger(__name__)


router = RouterPaginated()


class AnnotationSchema(Schema):
recording: int # Foreign Key to index
owner_username: str
start_time: int
end_time: int
low_freq: int
high_freq: int
species: list[int]
comments: str


def get_owner_id(request: HttpRequest):
token = request.headers.get('Authorization').replace('Bearer ', '')
token_found = AccessToken.objects.get(token=token)
if not token_found:
raise HttpError(401, 'Authentication credentials were not provided.')

return token_found.user.pk
15 changes: 12 additions & 3 deletions bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class AnnotationSchema(Schema):
high_freq: int
species: list[SpeciesSchema]
comments: str
id: int


def get_user(request: HttpRequest):
Expand Down Expand Up @@ -98,14 +99,22 @@ def get_recordings(request: HttpRequest):

@router.get('/{id}/spectrogram')
def get_spectrogram(request: HttpRequest, id: int):
user_id = get_user(request)
try:
recording = Recording.objects.get(pk=id)
except Recording.DoesNotExist:
return {'error': 'Recording not found'}

base64_spectrogram = recording.generate_spectrogram()
spectro_data = recording.generate_spectrogram()

annotations_qs = Annotations.objects.filter(recording=recording, owner=user_id)

return {'base64_spectrogram': base64_spectrogram}
# Serialize the annotations using AnnotationSchema
annotations_data = [
AnnotationSchema.from_orm(annotation).dict() for annotation in annotations_qs
]
spectro_data['annotations'] = annotations_data
return spectro_data


@router.get('/{id}/annotations')
Expand All @@ -118,7 +127,7 @@ def get_annotations(request: HttpRequest, id: int):
return {'error': 'Recording not found'}

# Query annotations associated with the recording
annotations_qs = Annotations.objects.filter(recording=recording)
annotations_qs = Annotations.objects.filter(recording=recording, owner=user_id)

# Serialize the annotations using AnnotationSchema
annotations_data = [
Expand Down
1 change: 1 addition & 0 deletions bats_ai/core/views/species.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class SpeciesSchema(Schema):
species: str | None
common_name: str | None
species_code_6: str | None
pk: int = None


@router.get('/')
Expand Down
4 changes: 1 addition & 3 deletions client/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<script lang="ts">
import { defineComponent, inject, computed, ref, watch } from 'vue';
import { defineComponent, inject, computed, ref } from 'vue';
import OAuthClient from '@girder/oauth-client';
import { useRoute } from 'vue-router';
export default defineComponent({
setup() {
const route = useRoute();
const oauthClient = inject<OAuthClient>('oauthClient');
if (oauthClient === undefined) {
throw new Error('Must provide "oauthClient" into component.');
Expand Down
12 changes: 7 additions & 5 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import axios from 'axios';
import { GeoJsonObject } from 'geojson';
import { createS3ffClient } from '../plugins/S3FileField';
import { S3FileFieldProgressCallback } from 'django-s3-file-field';
import { SpectroInfo } from '../components/geoJS/geoJSUtils';

export interface PaginatedResponse<E> {
count: number,
Expand Down Expand Up @@ -47,8 +45,11 @@ export interface Species {
}

export interface SpectrogramAnnotation {
offset: number,
frequency: number,
start_time: number;
end_time: number;
low_freq: number;
high_freq: number;
id: number;
}


Expand All @@ -57,6 +58,7 @@ export interface Spectrogram {
url?: string;
filename?: string;
annotations?: SpectrogramAnnotation[];
spectroInfo?: SpectroInfo;

}

Expand Down
33 changes: 28 additions & 5 deletions client/src/components/SpectrogramViewer.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
<script lang="ts">
import { defineComponent, PropType, ref, Ref, watch } from "vue";
import geo, { GeoEvent } from "geojs";
import { useGeoJS } from './geoJS/geoJSUtils';
import { SpectroInfo, useGeoJS } from './geoJS/geoJSUtils';
import { SpectrogramAnnotation } from "../api/api";
import LayerManager from "./geoJS/LayerManager.vue";
export default defineComponent({
name: "SpectroViewer",
components: {
LayerManager
},
props: {
image: {
type: Object as PropType<HTMLImageElement>,
required: true,
},
spectroInfo: {
type: Object as PropType<SpectroInfo | undefined>,
default: () => undefined,
},
annotations: {
type: Array as PropType<SpectrogramAnnotation[]>,
default: () => [],
}
},
setup(props) {
const containerRef: Ref<HTMLElement | undefined> = ref();
const geoJS = useGeoJS();
const initialized = ref(false);
watch(containerRef, () => {
const { width, height } = props.image;
const { naturalWidth, naturalHeight } = props.image;
if (containerRef.value)
geoJS.initializeViewer(containerRef.value, width, height);
geoJS.drawImage(props.image, width, height);
geoJS.initializeViewer(containerRef.value, naturalWidth, naturalHeight);
geoJS.drawImage(props.image, naturalWidth, naturalHeight);
initialized.value = true;
});
return {
containerRef,
geoViewerRef: geoJS.getGeoViewer(),
initialized,
};
},
});
Expand All @@ -36,6 +52,12 @@ export default defineComponent({
ref="containerRef"
class="playback-container"
/>
<layer-manager
v-if="initialized"
:geo-viewer-ref="geoViewerRef"
:spectro-info="spectroInfo"
:annotations="annotations"
/>
</div>
</template>

Expand All @@ -49,6 +71,7 @@ export default defineComponent({
z-index: 0;
width:100vw;
height: 100vh;
background-color: black;
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/UploadRecording.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { defineComponent, ref, Ref } from 'vue';
import { S3FileFieldProgress, S3FileFieldProgressState } from 'django-s3-file-field';
import { RecordingMimeTypes } from '../constants';
import useRequest from '../use/useRequest';
import { uploadRecordingFile } from '../api/api';
Expand Down Expand Up @@ -51,6 +50,7 @@ export default defineComponent({
emit('done');

Check warning on line 50 in client/src/components/UploadRecording.vue

View workflow job for this annotation

GitHub Actions / Lint [eslint]

The "done" event has been triggered but not declared on `emits` option
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateTime = (time: any) => {
recordedDate.value = new Date(time as string).toISOString().split('T')[0];
};
Expand Down
46 changes: 46 additions & 0 deletions client/src/components/geoJS/LayerManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import { defineComponent, onMounted, PropType } from 'vue';
import { SpectrogramAnnotation } from '../../api/api';
import { SpectroInfo } from './geoJSUtils';
import RectangleLayer from './layers/rectangleLayer';
export default defineComponent({
name:'LayerManager',
props: {
geoViewerRef: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Object as PropType<any>,
required: true,
},
spectroInfo: {
type: Object as PropType<SpectroInfo | undefined>,
default: () => undefined
},
annotations: {
type: Array as PropType<SpectrogramAnnotation[]>,
default: () => [],
}
},
setup(props) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
const event = (type: string, data: any) => {
// Will handle clicking, selecting and editing here
};
onMounted(() => {
if (props.spectroInfo) {
const rectAnnotationLayer = new RectangleLayer(props.geoViewerRef, event, props.spectroInfo);
rectAnnotationLayer.formatData(props.annotations);
rectAnnotationLayer.redraw();
}
});
return {
};
}
});
</script>

<template>
<div />
</template>
Loading

0 comments on commit 166b35e

Please sign in to comment.