Skip to content

Commit 3e3d5c8

Browse files
authored
chore(app): Enable using non-canvas bounding box and labels for images (#4804)
1 parent 94d8da2 commit 3e3d5c8

File tree

1 file changed

+117
-2
lines changed

1 file changed

+117
-2
lines changed

weave-js/src/components/Panel2/ImageWithOverlays.tsx

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import * as RadixTooltip from '@radix-ui/react-tooltip';
12
import {LineStyle, Style} from '@wandb/weave/common/components/MediaCard';
23
import {BoundingBox2D, LayoutType} from '@wandb/weave/common/types/media';
34
import {colorFromString} from '@wandb/weave/common/util/colors';
45
import {boxColor} from '@wandb/weave/common/util/media';
6+
import * as Tooltip from '@wandb/weave/components/RadixTooltip';
57
import {
68
constString,
79
File,
@@ -392,6 +394,7 @@ const SegmentationMaskFromCG: FC<SegmentationMaskFromCGProps> = props => {
392394

393395
export interface BoundingBoxCanvasProps {
394396
mediaSize: {width: number; height: number};
397+
cardSize?: {width: number; height: number};
395398
boxData: BoundingBox2D[];
396399
bboxControls: Controls.BoxControlState;
397400
sliders?: Controls.BoxSliderState;
@@ -440,6 +443,7 @@ const isBoundingBoxHidden = (
440443

441444
export const BoundingBoxesCanvas: FC<BoundingBoxCanvasProps> = ({
442445
mediaSize,
446+
cardSize,
443447
boxData,
444448
bboxControls,
445449
classStates,
@@ -449,7 +453,7 @@ export const BoundingBoxesCanvas: FC<BoundingBoxCanvasProps> = ({
449453
const {lineStyle, classOverlayStates} = bboxControls;
450454

451455
useEffect(() => {
452-
if (canvasRef.current == null) {
456+
if (canvasRef.current == null || cardSize != null) {
453457
return;
454458
}
455459

@@ -482,9 +486,120 @@ export const BoundingBoxesCanvas: FC<BoundingBoxCanvasProps> = ({
482486
bboxControls,
483487
mediaSize,
484488
sliderControls,
489+
cardSize,
485490
]);
486491

487-
return <OverlayCanvas {...mediaSize} ref={canvasRef} />;
492+
const collisionBoundary = useRef<HTMLDivElement>(null);
493+
494+
// Scale the image to fit the card size
495+
const scale = Math.min(
496+
(cardSize?.width ?? 0) / mediaSize.width,
497+
(cardSize?.height ?? 0) / mediaSize.height
498+
);
499+
const imageWidth = mediaSize.width * scale;
500+
const imageHeight = mediaSize.height * scale;
501+
502+
return (
503+
<div className="relative flex h-full w-full items-center">
504+
{cardSize && Number.isFinite(scale) && (
505+
<div
506+
className="relative overflow-hidden"
507+
ref={collisionBoundary}
508+
style={{
509+
width: imageWidth,
510+
height: imageHeight,
511+
minWidth: imageWidth,
512+
}}>
513+
{boxData.map(box => {
514+
const isHidden = isBoundingBoxHidden(
515+
box,
516+
bboxControls,
517+
sliderControls
518+
);
519+
if (isHidden) {
520+
return null;
521+
}
522+
523+
const {class_id: classId} = box;
524+
const name = classStates?.[classId]?.name ?? `ID: ${box.class_id}`;
525+
526+
let color = boxColor(classId);
527+
if (classStates?.[classId]?.color) {
528+
color = classStates?.[classId]?.color;
529+
}
530+
const position = box.position;
531+
const widthMultiplier = box.domain === 'pixel' ? scale : imageWidth;
532+
const heightMultiplier =
533+
box.domain === 'pixel' ? scale : imageHeight;
534+
535+
let left, top, width, height;
536+
if ('minX' in position) {
537+
left = position.minX * widthMultiplier;
538+
top = position.minY * heightMultiplier;
539+
width = (position.maxX - position.minX) * widthMultiplier;
540+
height = (position.maxY - position.minY) * heightMultiplier;
541+
} else {
542+
left =
543+
(position.middle[0] - position.width / 2) * widthMultiplier;
544+
top =
545+
(position.middle[1] - position.height / 2) * heightMultiplier;
546+
width = position.width * widthMultiplier;
547+
height = position.height * heightMultiplier;
548+
}
549+
// Scale the outline width based on the card size. If it's a small card, we probably want a thinner outline so
550+
// the image isn't obstructed too much. But if it's a large card, we want a thicker outline so it's more visible.
551+
const outlineWidth = Math.min(6, Math.max(2, cardSize.width / 100));
552+
553+
return (
554+
<div
555+
data-test="single-bounding-box"
556+
key={classId}
557+
className="absolute"
558+
style={{
559+
left,
560+
top: top,
561+
}}>
562+
<div className="relative">
563+
<Tooltip.Provider delayDuration={0}>
564+
<RadixTooltip.Root>
565+
<Tooltip.Trigger>
566+
<div
567+
style={{
568+
outline: outlineWidth,
569+
outlineColor: color,
570+
outlineStyle:
571+
lineStyle === 'line'
572+
? 'solid'
573+
: lineStyle ?? 'solid',
574+
width,
575+
height,
576+
}}
577+
/>
578+
</Tooltip.Trigger>
579+
<Tooltip.Content
580+
hideWhenDetached
581+
style={{backgroundColor: color, padding: 4}}
582+
avoidCollisions
583+
side="top"
584+
align="start"
585+
collisionBoundary={collisionBoundary.current}>
586+
<div
587+
style={{backgroundColor: color}}
588+
className="text-white">
589+
{name}
590+
</div>
591+
</Tooltip.Content>
592+
</RadixTooltip.Root>
593+
</Tooltip.Provider>
594+
</div>
595+
</div>
596+
);
597+
})}
598+
</div>
599+
)}
600+
<OverlayCanvas {...mediaSize} ref={canvasRef} />
601+
</div>
602+
);
488603
};
489604

490605
export interface BoundingBoxesProps {

0 commit comments

Comments
 (0)