diff --git a/bats_ai/core/models/spectrogram.py b/bats_ai/core/models/spectrogram.py index 1bcaf70..9ee4ab6 100644 --- a/bats_ai/core/models/spectrogram.py +++ b/bats_ai/core/models/spectrogram.py @@ -239,6 +239,7 @@ def compressed(self): stops_ = [] domain = img.shape[1] widths = [] + total_width = 0 for start, stop in ranges: segment = img[:, start:stop] segments.append(segment) @@ -246,6 +247,7 @@ def compressed(self): starts_.append(int(round(self.duration * (start / domain)))) stops_.append(int(round(self.duration * (stop / domain)))) widths.append(stop - start) + total_width += stop - start # buffer = np.zeros((len(img), 20, 3), dtype=img.dtype) # segments.append(buffer) @@ -273,7 +275,7 @@ def compressed(self): buf.seek(0) img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8') - return img_base64, starts_, stops_, widths + return img_base64, starts_, stops_, widths, total_width @property def image_np(self): diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index 0acd72b..2752215 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -274,7 +274,7 @@ def get_spectrogram_compressed(request: HttpRequest, id: int): return {'error': 'Recording not found'} spectrogram = recording.spectrogram - compressed, starts, ends, widths = spectrogram.compressed + compressed, starts, ends, widths, total_width = spectrogram.compressed spectro_data = { 'base64_spectrogram': compressed, @@ -287,6 +287,7 @@ def get_spectrogram_compressed(request: HttpRequest, id: int): 'end_times': ends, 'low_freq': spectrogram.frequency_min, 'high_freq': spectrogram.frequency_max, + 'compressedWidth': total_width, 'widths': widths, }, } @@ -314,13 +315,22 @@ def get_spectrogram_compressed(request: HttpRequest, id: int): spectro_data['currentUser'] = request.user.email annotations_qs = Annotations.objects.filter(recording=recording, owner=request.user) + temporal_annotations_qs = TemporalAnnotations.objects.filter( + recording=recording, owner=request.user + ) # Serialize the annotations using AnnotationSchema annotations_data = [ AnnotationSchema.from_orm(annotation, owner_email=request.user.email).dict() for annotation in annotations_qs ] + temporal_annotations_data = [ + TemporalAnnotationSchema.from_orm(annotation, owner_email=request.user.email).dict() + for annotation in temporal_annotations_qs + ] + spectro_data['annotations'] = annotations_data + spectro_data['temporal'] = temporal_annotations_data return spectro_data diff --git a/client/src/components/SpectrogramViewer.vue b/client/src/components/SpectrogramViewer.vue index 5be5c98..4557d40 100644 --- a/client/src/components/SpectrogramViewer.vue +++ b/client/src/components/SpectrogramViewer.vue @@ -40,11 +40,13 @@ export default defineComponent({ "hoverData", ], setup(props, { emit }) { - const { annotations, temporalAnnotations, selectedId, selectedType, creationType, blackBackground } = useState(); + const { annotations, temporalAnnotations, selectedId, selectedType, creationType, blackBackground, scaledVals } = useState(); const containerRef: Ref = ref(); const geoJS = useGeoJS(); const initialized = ref(false); const cursor = ref(""); + const scaledWidth = ref(0); + const scaledHeight = ref(0); const imageCursorRef: Ref = ref(); const setCursor = (newCursor: string) => { cursor.value = newCursor; @@ -77,20 +79,23 @@ export default defineComponent({ if (!props.spectroInfo) { return; } + const adjustedWidth = scaledWidth.value > props.spectroInfo.width ? scaledWidth.value : props.spectroInfo.width; + const adjustedHeight = scaledHeight.value > props.spectroInfo.height ? scaledHeight.value : props.spectroInfo.height; + const freq = - props.spectroInfo.height - y >= 0 - ? ((props.spectroInfo.height - y) * - (props.spectroInfo.high_freq - props.spectroInfo.low_freq)) / - props.spectroInfo.height / - 1000 + - props.spectroInfo.low_freq / 1000 + adjustedHeight - y >= 0 + ? ((adjustedHeight - y) * + (props.spectroInfo.high_freq - props.spectroInfo.low_freq)) / + adjustedHeight / + 1000 + + props.spectroInfo.low_freq / 1000 : -1; if (!props.spectroInfo.end_times && !props.spectroInfo.start_times) { - if (x >= 0 && props.spectroInfo.height - y >= 0) { + if (x >= 0 && adjustedHeight - y >= 0) { const time = x * - ((props.spectroInfo.end_time - props.spectroInfo.start_time) / props.spectroInfo.width); + ((props.spectroInfo.end_time - props.spectroInfo.start_time) / adjustedWidth); emit("hoverData", { time, freq }); } else { emit("hoverData", { time: -1, freq: -1 }); @@ -101,9 +106,9 @@ export default defineComponent({ props.spectroInfo.end_times ) { // compressed view - if (x >= 0 && props.spectroInfo.height - y >= 0) { + if (x >= 0 && adjustedHeight - y >= 0) { const timeLength = props.spectroInfo.end_time - props.spectroInfo.start_time; - const timeToPixels = props.spectroInfo.width / timeLength; + const timeToPixels = adjustedWidth / timeLength; // find X in the range let offsetAdditive = 0; for (let i = 0; i < props.spectroInfo.start_times.length; i += 1) { @@ -127,6 +132,8 @@ export default defineComponent({ }; watch(containerRef, () => { const { naturalWidth, naturalHeight } = props.image; + scaledWidth.value = naturalWidth; + scaledHeight.value = naturalHeight; if (containerRef.value) geoJS.initializeViewer(containerRef.value, naturalWidth, naturalHeight); geoJS.drawImage(props.image, naturalWidth, naturalHeight); @@ -166,9 +173,9 @@ export default defineComponent({ if (skipNextSelected) { skipNextSelected = false; return; - + } - const found = selectedType.value === 'pulse' ? annotations.value.find((item) => item.id === selectedId.value): temporalAnnotations.value.find((item) => item.id === selectedId.value); + const found = selectedType.value === 'pulse' ? annotations.value.find((item) => item.id === selectedId.value) : temporalAnnotations.value.find((item) => item.id === selectedId.value); if (found && props.spectroInfo) { const center = spectroToCenter(found, props.spectroInfo, selectedType.value); @@ -182,6 +189,28 @@ export default defineComponent({ } ); + const wheelEvent = (event: WheelEvent) => { + const { naturalWidth, naturalHeight } = props.image; + + if (event.ctrlKey) { + scaledWidth.value = scaledWidth.value + event.deltaY * -4; + if (scaledWidth.value < naturalWidth) { + scaledWidth.value = naturalWidth; + } + geoJS.drawImage(props.image, scaledWidth.value, scaledHeight.value, false); + } else if (event.shiftKey) { + scaledHeight.value = scaledHeight.value + event.deltaY * -0.25; + if (scaledHeight.value < naturalHeight) { + scaledHeight.value = naturalHeight; + } + geoJS.drawImage(props.image, scaledWidth.value, scaledHeight.value, false); + } + const xScale = props.spectroInfo?.compressedWidth ? scaledWidth.value / props.spectroInfo.compressedWidth: scaledWidth.value / (props.spectroInfo?.width || 1) ; + const yScale = scaledHeight.value / (props.spectroInfo?.height || 1) ; + scaledVals.value = {x: xScale, y: yScale}; + }; + + return { containerRef, @@ -194,6 +223,9 @@ export default defineComponent({ cursorHandler, imageCursorRef, blackBackground, + wheelEvent, + scaledWidth, + scaledHeight, }; }, }); @@ -202,7 +234,8 @@ export default defineComponent({