Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotation Viewer/Editor #13

Merged
merged 2 commits into from
Jan 10, 2024
Merged
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
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 SpectrogramAnnotation {
offset: number,
frequency: number,
start_time: number;
end_time: number;
low_freq: number;
high_freq: number;
id: number;
}


Expand All @@ -57,10 +58,11 @@
url?: string;
filename?: string;
annotations?: SpectrogramAnnotation[];
spectroInfo?: SpectroInfo;

}

interface PaginatedNinjaResponse<T> {

Check warning on line 65 in client/src/api/api.ts

View workflow job for this annotation

GitHub Actions / Lint [eslint]

'PaginatedNinjaResponse' is defined but never used
count: number,
items: T[],
}
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 @@ -48,9 +47,10 @@
throw new Error('Unreachable');
}
await uploadRecordingFile(file, name.value, recordedDate.value, equipment.value, comments.value);
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 Expand Up @@ -180,7 +180,7 @@
<v-card-actions>
<v-spacer />
<v-btn
@click="$emit('cancel', true)"

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

View workflow job for this annotation

GitHub Actions / Lint [eslint]

The "cancel" event has been triggered but not declared on `emits` option
>
Cancel
</v-btn>
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