diff --git a/client/dive-common/components/Attributes/AttributeEditor.vue b/client/dive-common/components/Attributes/AttributeEditor.vue index b438b1b79..fdf98c8a5 100644 --- a/client/dive-common/components/Attributes/AttributeEditor.vue +++ b/client/dive-common/components/Attributes/AttributeEditor.vue @@ -165,6 +165,7 @@ export default defineComponent({ if (renderingVals.value === undefined) { renderingVals.value = { typeFilter: ['all'], + hideEmpty: true, displayName: props.selectedAttribute.name, displayColor: 'auto', displayTextSize: -1, diff --git a/client/dive-common/components/Attributes/AttributeRendering.vue b/client/dive-common/components/Attributes/AttributeRendering.vue index 85e232bea..1f2becbb4 100644 --- a/client/dive-common/components/Attributes/AttributeRendering.vue +++ b/client/dive-common/components/Attributes/AttributeRendering.vue @@ -26,6 +26,7 @@ export default defineComponent({ const mainSettings = reactive({ selected: props.value.selected || false, + hideEmpty: props.value.hideEmpty ?? true, typeFilter: props.value.typeFilter || ['all'], order: props.value.order, }); @@ -103,6 +104,7 @@ export default defineComponent({ const updateSettings = () => { emit('input', { selected: mainSettings.selected, + hideEmpty: mainSettings.hideEmpty, typeFilter: mainSettings.typeFilter, order: mainSettings.order, displayName: displayNameSettings.displayName, @@ -227,6 +229,7 @@ export default defineComponent({ @@ -243,6 +246,13 @@ export default defineComponent({ persistent-hint class="mx-2" /> + = {}, +): AttributeRendering { + return { + typeFilter: ['all'], + hideEmpty: true, + displayName, + displayColor: 'auto', + displayTextSize: -1, + valueColor: 'auto', + valueTextSize: -1, + order: 0, + location: 'outside', + corner: 'SE', + layout: 'horizontal', + box: false, + boxColor: 'auto', + boxThickness: 1, + displayWidth: { + type: '%', + val: 10, + }, + displayHeight: { + type: 'auto', + val: 10, + }, + ...overrides, + }; +} + +export function createStereoLengthRendering(displayName = STEREO_LENGTH_ATTRIBUTE_NAME): AttributeRendering { + return createDefaultAttributeRendering(displayName); +} + +export function findStereoLengthAttribute( + attributes: Attribute[] | Record, +): Attribute | undefined { + const list = Array.isArray(attributes) ? attributes : Object.values(attributes); + return list.find((attribute) => ( + attribute.name === STEREO_LENGTH_ATTRIBUTE_NAME && attribute.belongs === 'detection' + )); +} + +/** + * Enable on-canvas rendering for the detection-level length attribute when it + * exists but has no render settings yet. + */ +export function ensureStereoLengthRendering( + attributes: Attribute[] | Record, +): boolean { + const lengthAttribute = findStereoLengthAttribute(attributes); + if (!lengthAttribute || lengthAttribute.render) { + return false; + } + lengthAttribute.render = createStereoLengthRendering(lengthAttribute.name); + return true; +} diff --git a/client/platform/desktop/frontend/components/ViewerLoader.vue b/client/platform/desktop/frontend/components/ViewerLoader.vue index 2f005b521..a56d6f900 100644 --- a/client/platform/desktop/frontend/components/ViewerLoader.vue +++ b/client/platform/desktop/frontend/components/ViewerLoader.vue @@ -17,6 +17,10 @@ import type { StereoSegmentationFinalizeParams, } from 'dive-common/use/useModeManager'; import { HeadPointKey, TailPointKey, HeadTailLineKey } from 'dive-common/recipes/headtail'; +import { + createStereoLengthRendering, + STEREO_LENGTH_ATTRIBUTE_NAME, +} from 'dive-common/utils/stereoLengthRendering'; import type { RectBounds } from 'vue-media-annotator/utils'; import { segmentationPredict, segmentationStereoSegment, segmentationInitialize, segmentationIsReady, @@ -694,13 +698,24 @@ export default defineComponent({ // eslint-disable-next-line @typescript-eslint/no-explicit-any const existing = (viewer.attributes || []) as any[]; STEREO_MEASUREMENT_ATTRS.forEach((name) => { - if (!existing.find((a) => a.name === name && a.belongs === 'detection')) { + const existingAttr = existing.find((a) => a.name === name && a.belongs === 'detection'); + if (!existingAttr) { viewer.handler.setAttribute({ data: { belongs: 'detection', datatype: 'number', name, key: `detection_${name}`, + ...(name === STEREO_LENGTH_ATTRIBUTE_NAME + ? { render: createStereoLengthRendering(name) } + : {}), + }, + }); + } else if (name === STEREO_LENGTH_ATTRIBUTE_NAME && !existingAttr.render) { + viewer.handler.setAttribute({ + data: { + ...existingAttr, + render: createStereoLengthRendering(existingAttr.name), }, }); } diff --git a/client/src/layers/AnnotationLayers/AttributeBoxLayer.ts b/client/src/layers/AnnotationLayers/AttributeBoxLayer.ts index 763486105..eb14b5d6d 100644 --- a/client/src/layers/AnnotationLayers/AttributeBoxLayer.ts +++ b/client/src/layers/AnnotationLayers/AttributeBoxLayer.ts @@ -5,7 +5,7 @@ import { Attribute } from 'vue-media-annotator/use/AttributeTypes'; import { boundToGeojson } from '../../utils'; import BaseLayer, { LayerStyle, BaseLayerParams } from '../BaseLayer'; import { FrameDataTrack } from '../LayerTypes'; -import { calculateAttributeArea } from './AttributeLayer'; +import { calculateAttributeArea, getAttributeValue, isEmptyAttributeValue } from './AttributeLayer'; interface RectGeoJSData{ trackId: number; @@ -62,12 +62,21 @@ export default class AttributeBoxLayer extends BaseLayer { } return false; }); - for (let i = 0; i < renderFiltered.length; i += 1) { - const currentRender = renderFiltered[i].render; + const visibleAttributes = renderFiltered.filter((item) => { + if (!item.render) { + return false; + } + if ((item.render.hideEmpty ?? true) && isEmptyAttributeValue(getAttributeValue(track, item, ''))) { + return false; + } + return true; + }); + for (let i = 0; i < visibleAttributes.length; i += 1) { + const currentRender = visibleAttributes[i].render; if (currentRender && currentRender.box) { - const { newBounds } = calculateAttributeArea(track.features.bounds, renderFiltered[i].render, i, renderFiltered.length); + const { newBounds } = calculateAttributeArea(track.features.bounds, visibleAttributes[i].render, i, visibleAttributes.length); const polygon = boundToGeojson(newBounds); - const lineColor = currentRender.boxColor === 'auto' ? renderFiltered[i].color || 'white' : currentRender.boxColor; + const lineColor = currentRender.boxColor === 'auto' ? visibleAttributes[i].color || 'white' : currentRender.boxColor; const lineThickness = currentRender.boxThickness || 1; const { boxBackground } = currentRender; const { boxOpacity } = currentRender; diff --git a/client/src/layers/AnnotationLayers/AttributeLayer.ts b/client/src/layers/AnnotationLayers/AttributeLayer.ts index fec71d4f4..99fd1b9c3 100644 --- a/client/src/layers/AnnotationLayers/AttributeLayer.ts +++ b/client/src/layers/AnnotationLayers/AttributeLayer.ts @@ -34,6 +34,38 @@ interface AttributeLayerParams { } const lineHeight = 15; + +export function getAttributeValue( + annotation: FrameDataTrack, + attr: Attribute, + user: string, +): string | number | boolean | undefined { + const { name } = attr; + if (attr.belongs === 'detection') { + if (annotation.features?.attributes) { + const { attributes } = annotation.features; + if (attr.user && user && attributes.userAttributes?.[user]) { + return (attributes.userAttributes[user] as StringKeyObject)[name] as string | boolean | number; + } + return attributes[name] as string | boolean | number; + } + } + if (attr.belongs === 'track') { + const { attributes } = annotation.track; + if (attributes) { + if (attr.user && user && attributes.userAttributes?.[user]) { + return (attributes.userAttributes[user] as StringKeyObject)[name] as string | boolean | number; + } + return attributes[name] as string | boolean | number; + } + } + return undefined; +} + +export function isEmptyAttributeValue(value: string | number | boolean | undefined): boolean { + return value === undefined || value === null || value === ''; +} + // function to calculate x,y as well as bounds based on render settings export function calculateAttributeArea(baseBounds: RectBounds, renderSettings: Attribute['render'], renderIndex: number, renderAttrLength: number) { // Calculate X Position @@ -125,40 +157,30 @@ function defaultFormatter( return false; }); - for (let i = 0; i < renderFiltered.length; i += 1) { - const currentRender = renderFiltered[i].render; - const { name } = renderFiltered[i]; + const visibleAttributes = renderFiltered.filter((item) => { + if (!item.render) { + return false; + } + if ((item.render.hideEmpty ?? true) && isEmptyAttributeValue(getAttributeValue(annotation, item, user))) { + return false; + } + return true; + }); + + for (let i = 0; i < visibleAttributes.length; i += 1) { + const currentRender = visibleAttributes[i].render; if (currentRender !== undefined) { const { displayName } = currentRender; - const type = renderFiltered[i].belongs; - // Calculate Value - let value: string | number | boolean = ''; - if (type === 'detection') { - if (annotation.features && annotation.features.attributes) { - const { attributes } = annotation.features; - if (renderFiltered[i].user && user && attributes.userAttributes && attributes.userAttributes[user]) { - value = (attributes.userAttributes[user] as StringKeyObject)[name] as string | boolean | number; - } else { - value = attributes[name] as string | boolean | number; - } - } - } - if (type === 'track') { - const { attributes } = annotation.track; - if (attributes) { - if (renderAttr[i].user && user && attributes.userAttributes && attributes.userAttributes[user]) { - value = (attributes.userAttributes[user] as StringKeyObject)[name] as string | boolean | number; - } else { - value = attributes[name] as string | boolean | number; - } - } + let value = getAttributeValue(annotation, visibleAttributes[i], user); + if (value === undefined) { + value = ''; } const { displayX, displayHeight, valueX, valueHeight, offsetY, - } = calculateAttributeArea(bounds, currentRender, i, renderFiltered.length); + } = calculateAttributeArea(bounds, currentRender, i, visibleAttributes.length); - const displayColor = currentRender.displayColor === 'auto' ? renderAttr[i].color : currentRender.displayColor; + const displayColor = currentRender.displayColor === 'auto' ? visibleAttributes[i].color : currentRender.displayColor; const { displayTextSize } = currentRender; arr.push({ selected: annotation.selected, @@ -172,11 +194,9 @@ function defaultFormatter( offsetY, offsetX: displayHeight === valueHeight ? 20 : 0, }); - const valueColor = autoColorIndex[i](value); + const attrIndex = renderAttr.indexOf(visibleAttributes[i]); + const valueColor = autoColorIndex[attrIndex](value); const { valueTextSize } = currentRender; - if (value === undefined) { - value = ''; - } arr.push({ selected: annotation.selected, editing: annotation.editing, diff --git a/client/src/use/AttributeTypes.ts b/client/src/use/AttributeTypes.ts index 45cf6b1a0..381b3c711 100644 --- a/client/src/use/AttributeTypes.ts +++ b/client/src/use/AttributeTypes.ts @@ -12,6 +12,7 @@ export interface StringAttributeEditorOptions { export interface AttributeRendering { typeFilter: string[]; selected?: boolean; + hideEmpty?: boolean; displayName: string; displayColor: 'auto' | string; displayTextSize: number; diff --git a/client/src/use/useAttributes.ts b/client/src/use/useAttributes.ts index bd78c45bb..b9a8b4775 100644 --- a/client/src/use/useAttributes.ts +++ b/client/src/use/useAttributes.ts @@ -2,6 +2,7 @@ import { ref, Ref, computed, set as VueSet, del as VueDel, } from 'vue'; import { StringKeyObject } from 'vue-media-annotator/BaseAnnotation'; +import { ensureStereoLengthRendering } from 'dive-common/utils/stereoLengthRendering'; import { StyleManager, Track } from '..'; import CameraStore from '../CameraStore'; import { isReservedAttributeName, RESERVED_ATTRIBUTES } from '../utils'; @@ -51,7 +52,13 @@ export default function UseAttributes( }); const timelineEnabled: Ref = ref(false); - function loadAttributes(metadataAttributes: Record) { + function loadAttributes( + metadataAttributes: Record, + options?: { enableStereoLengthRender?: boolean }, + ) { + if (options?.enableStereoLengthRender) { + ensureStereoLengthRendering(metadataAttributes); + } attributes.value = metadataAttributes; Object.values(attributes.value).forEach((attribute) => { if (attribute.color === undefined) { diff --git a/server/dive_utils/models.py b/server/dive_utils/models.py index 6f500207b..75b33383a 100644 --- a/server/dive_utils/models.py +++ b/server/dive_utils/models.py @@ -169,6 +169,7 @@ class RenderingDisplayDimension(BaseModel): class RenderingAttributes(BaseModel): typeFilter: List[str] selected: Optional[bool] + hideEmpty: Optional[bool] displayName: str displayColor: str displayTextSize: float