diff --git a/application/ui/src/components/virtualizer-grid-layout/virtualizer-grid-layout.module.scss b/application/ui/src/components/virtualizer-grid-layout/virtualizer-grid-layout.module.scss index 2fc839f8a7..5d946bcfb7 100644 --- a/application/ui/src/components/virtualizer-grid-layout/virtualizer-grid-layout.module.scss +++ b/application/ui/src/components/virtualizer-grid-layout/virtualizer-grid-layout.module.scss @@ -33,7 +33,7 @@ img { box-sizing: border-box; - object-fit: contain; + object-fit: cover; width: 100%; height: 100%; } diff --git a/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.component.tsx b/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.component.tsx index 1e94b33c39..4b295b4ff7 100644 --- a/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.component.tsx +++ b/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.component.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { SchemaPredictionResponse } from '@geti-inspect/api/spec'; -import { dimensionValue, Flex, View } from '@geti/ui'; +import { DimensionValue, Responsive, View } from '@geti/ui'; import { clsx } from 'clsx'; import { AnimatePresence, motion } from 'motion/react'; import { isNonEmptyString } from 'src/features/inspect/utils'; @@ -9,6 +9,7 @@ import { isNonEmptyString } from 'src/features/inspect/utils'; import { MediaItem } from '../../types'; import { useInference } from '../providers/inference-opacity-provider.component'; import { LabelScore } from './label-score.component'; +import { getImageDimensions } from './util'; import classes from './inference-result.module.scss'; @@ -17,49 +18,65 @@ interface InferenceResultProps { inferenceResult: SchemaPredictionResponse | undefined; } +const labelHeight: Responsive = 'size-350'; + export const InferenceResult = ({ selectedMediaItem, inferenceResult }: InferenceResultProps) => { + const imageRef = useRef(null); const { inferenceOpacity } = useInference(); - const [isVerticalImage, setIsVerticalImage] = useState(false); + const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0, left: 0, top: 0 }); - const handleImageOrientation = (imageElement: HTMLImageElement) => { - setIsVerticalImage(imageElement.clientHeight > imageElement.clientWidth); + const handleImageLoaded = (imageElement: HTMLImageElement) => { + setImageDimensions(getImageDimensions(imageElement)); }; - return ( - - - - {inferenceResult && } - + useEffect(() => { + if (!imageRef.current) return; + + const observer = new ResizeObserver(([entry]) => { + handleImageLoaded(entry.target as HTMLImageElement); + }); + + observer.observe(imageRef.current); + return () => observer.disconnect(); + }, []); + return ( + + {inferenceResult && ( - {selectedMediaItem.filename} handleImageOrientation(target as HTMLImageElement)} - /> - - - {isNonEmptyString(inferenceResult?.anomaly_map) && ( - <> - - - )} - + - - + )} + + + {selectedMediaItem.filename} handleImageLoaded(target as HTMLImageElement)} + /> + + + {isNonEmptyString(inferenceResult?.anomaly_map) && ( + + )} + + + ); }; diff --git a/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.module.scss b/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.module.scss index eb050c045b..b0e02a767d 100644 --- a/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.module.scss +++ b/application/ui/src/features/inspect/dataset/media-preview/inference-result/inference-result.module.scss @@ -1,7 +1,6 @@ -.img, -.inferenceImage { - height: 100%; +.img { width: 100%; + height: 100%; object-fit: contain; } @@ -11,12 +10,12 @@ } .inferenceImage { - left: 0px; - right: 0px; - width: 100%; - height: 100%; position: absolute; - object-fit: cover; + + img { + width: 100%; + height: 100%; + } } .label { diff --git a/application/ui/src/features/inspect/dataset/media-preview/inference-result/util.ts b/application/ui/src/features/inspect/dataset/media-preview/inference-result/util.ts new file mode 100644 index 0000000000..14dddb5c5e --- /dev/null +++ b/application/ui/src/features/inspect/dataset/media-preview/inference-result/util.ts @@ -0,0 +1,43 @@ +/** + * Calculates the actual rendered dimensions and position of an image element when using object-fit: contain. + * + * This function determines how an image is displayed within its container when the image maintains + * its aspect ratio and is scaled to fit entirely within the container bounds. It calculates the + * actual rendered size and the offset position where the image appears within the container. + */ + +export const getImageDimensions = (img: HTMLImageElement) => { + const containerWidth = img.clientWidth; + const containerHeight = img.clientHeight; + + const naturalWidth = img.naturalWidth; + const naturalHeight = img.naturalHeight; + + if (naturalHeight === 0 || containerHeight === 0) { + return { top: 0, left: 0, width: 0, height: 0 }; + } + + const imageRatio = naturalWidth / naturalHeight; + const containerRatio = containerWidth / containerHeight; + + let renderedWidth, renderedHeight; + + if (imageRatio > containerRatio) { + renderedWidth = containerWidth; + renderedHeight = containerWidth / imageRatio; + } else { + renderedHeight = containerHeight; + renderedWidth = containerHeight * imageRatio; + } + + // Calculate offset (image is centered by default with object-fit) + const offsetTop = (containerHeight - renderedHeight) / 2; + const offsetLeft = (containerWidth - renderedWidth) / 2; + + return { + top: offsetTop, + left: offsetLeft, + width: renderedWidth, + height: renderedHeight, + }; +}; diff --git a/application/ui/src/features/inspect/dataset/media-preview/media-preview.component.tsx b/application/ui/src/features/inspect/dataset/media-preview/media-preview.component.tsx index 4bf2d8ffd0..23f49a7a59 100644 --- a/application/ui/src/features/inspect/dataset/media-preview/media-preview.component.tsx +++ b/application/ui/src/features/inspect/dataset/media-preview/media-preview.component.tsx @@ -70,7 +70,7 @@ export const MediaPreview = ({ UNSAFE_style={{ padding: dimensionValue('size-125') }} areas={['canvas sidebar', 'canvas sidebar']} > - + diff --git a/application/ui/src/features/inspect/dataset/media-preview/sidebar-items/sidebar-items.component.tsx b/application/ui/src/features/inspect/dataset/media-preview/sidebar-items/sidebar-items.component.tsx index feeb871fe5..ec1fcd1a71 100644 --- a/application/ui/src/features/inspect/dataset/media-preview/sidebar-items/sidebar-items.component.tsx +++ b/application/ui/src/features/inspect/dataset/media-preview/sidebar-items/sidebar-items.component.tsx @@ -1,6 +1,6 @@ import { Selection, View } from '@geti/ui'; import { GridLayoutOptions } from 'react-aria-components'; -import { getThumbnailUrl } from 'src/features/inspect/utils'; +import { getThumbnailUrl, isNonEmptyString } from 'src/features/inspect/utils'; import { GridMediaItem } from '../../../../..//components/virtualizer-grid-layout/grid-media-item/grid-media-item.component'; import { MediaThumbnail } from '../../../../../components/media-thumbnail/media-thumbnail.component'; @@ -35,7 +35,7 @@ export const SidebarItems = ({ const firstKey = updatedSelectedKeys.values().next().value; const mediaItem = mediaItems.find((item) => item.id === firstKey); - onSelectedMediaItem(mediaItem?.id ?? null); + isNonEmptyString(mediaItem?.id) && onSelectedMediaItem(mediaItem.id); }; const handleDeletedItem = (deletedIds: string[]) => { diff --git a/application/ui/src/features/inspect/stream/fps/fps.component.tsx b/application/ui/src/features/inspect/stream/fps/fps.component.tsx index d59002e089..1616178771 100644 --- a/application/ui/src/features/inspect/stream/fps/fps.component.tsx +++ b/application/ui/src/features/inspect/stream/fps/fps.component.tsx @@ -32,9 +32,10 @@ export const Fps = ({ projectId }: FpsProp) => { return (