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

labels rendering performance improvement: create ImageBitmaps in worker #5169

Merged
merged 31 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6e4775a
change typedef: export ImageBitmap from worker
sashankaryal Nov 20, 2024
53efcf3
implement getSizeBytes in heatmap and segmentation, too
sashankaryal Nov 21, 2024
7e00596
remove canvas creation in detection constructor
sashankaryal Nov 21, 2024
3f30521
remove buffers arg from mask deserializer
sashankaryal Nov 21, 2024
e29f140
make worker send bitmaps back to main thread
sashankaryal Nov 21, 2024
6cfce47
move path decoder to another file
sashankaryal Nov 21, 2024
6cb954e
add unit tests for on disk decoder
sashankaryal Nov 21, 2024
fcc480e
add LabelMask type
sashankaryal Nov 21, 2024
e428200
use modified import for getSampleSrc
sashankaryal Nov 21, 2024
997828d
remove detections specific logic in bitmap collection function
sashankaryal Nov 21, 2024
4ea1df5
return targets buffers to main thread but not image buffers
sashankaryal Nov 21, 2024
4a0cf06
fix frames
sashankaryal Nov 21, 2024
e28dce4
push buffers in deserializer
sashankaryal Nov 21, 2024
c55387a
draw bitmap in segmentation, too
sashankaryal Nov 21, 2024
35cceee
clarify comments
sashankaryal Nov 21, 2024
00d7381
take fetch opts
sashankaryal Nov 22, 2024
bc9ba41
pool fetch
sashankaryal Nov 22, 2024
0fcedd1
pass missing params in recursive calls
sashankaryal Nov 22, 2024
b732e20
add types
sashankaryal Nov 22, 2024
1a0331a
close bitmap when looker destroys
sashankaryal Nov 22, 2024
a56869c
add error handling
sashankaryal Nov 22, 2024
80039dd
remove log
sashankaryal Nov 25, 2024
781a4f1
address edge case of empty mask
sashankaryal Nov 25, 2024
0e347ff
Fix app test
sashankaryal Nov 25, 2024
882f5aa
fix video
sashankaryal Nov 25, 2024
532fe7b
fix edge case where bitmap is already set but we need to reinit image…
sashankaryal Nov 25, 2024
f65aa4d
fix null check and remove logs
sashankaryal Nov 25, 2024
3b0ccf6
fix frames
sashankaryal Nov 26, 2024
7d5d7c6
fix frames memory leak
sashankaryal Nov 26, 2024
abc6a13
add unit tests for pooled fetch
sashankaryal Nov 26, 2024
28e2ab5
fix fetch unit tests
sashankaryal Nov 26, 2024
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
5 changes: 5 additions & 0 deletions app/packages/looker/src/lookers/abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ export abstract class AbstractLooker<
return;
}

if (this.state.destroyed && this.sampleOverlays) {
// close all current overlays
this.pluckedOverlays.forEach((overlay) => overlay.cleanup?.());
}

if (
!this.state.windowBBox ||
this.state.destroyed ||
Expand Down
10 changes: 9 additions & 1 deletion app/packages/looker/src/lookers/frame-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ interface AcquireReaderOptions {
export const { acquireReader, clearReader } = (() => {
const createCache = (removeFrame: RemoveFrame) => {
return new LRUCache<number, Frame>({
dispose: (_, key) => removeFrame(key),
dispose: (frame, key) => {
const overlays = frame.overlays;

for (let i = 0; i < overlays.length; i++) {
overlays[i].cleanup?.();
}

removeFrame(key);
},
max: MAX_FRAME_STREAM_SIZE,
maxSize: MAX_FRAME_STREAM_SIZE_BYTES,
noDisposeOnSet: true,
Expand Down
7 changes: 7 additions & 0 deletions app/packages/looker/src/overlays/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import { getCls, sizeBytesEstimate } from "@fiftyone/utilities";
import { OverlayMask } from "../numpy";
import type { BaseState, Coordinates, NONFINITE } from "../state";
import { getLabelColor, shouldShowLabelTag } from "./util";

Expand Down Expand Up @@ -39,6 +40,11 @@ export interface SelectData {
frameNumber?: number;
}

export type LabelMask = {
bitmap?: ImageBitmap;
data?: OverlayMask;
};

export interface RegularLabel extends BaseLabel {
_id?: string;
label?: string;
Expand Down Expand Up @@ -67,6 +73,7 @@ export interface Overlay<State extends Partial<BaseState>> {
getPoints(state: Readonly<State>): Coordinates[];
getSelectData(state: Readonly<State>): SelectData;
getSizeBytes(): number;
cleanup?(): void;
}

export abstract class CoordinateOverlay<
Expand Down
54 changes: 18 additions & 36 deletions app/packages/looker/src/overlays/detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
import { NONFINITES } from "@fiftyone/utilities";

import { INFO_COLOR } from "../constants";
import { OverlayMask } from "../numpy";
import { BaseState, BoundingBox, Coordinates, NONFINITE } from "../state";
import { distanceFromLineSegment } from "../util";
import { CONTAINS, CoordinateOverlay, PointInfo, RegularLabel } from "./base";
import {
CONTAINS,
CoordinateOverlay,
LabelMask,
PointInfo,
RegularLabel,
} from "./base";
import { t } from "./util";

export interface DetectionLabel extends RegularLabel {
mask?: {
data: OverlayMask;
image: ArrayBuffer;
};
mask?: LabelMask;
bounding_box: BoundingBox;

// valid for 3D bounding boxes
Expand All @@ -27,10 +29,8 @@ export interface DetectionLabel extends RegularLabel {
export default class DetectionOverlay<
State extends BaseState
> extends CoordinateOverlay<State, DetectionLabel> {
private imageData: ImageData;
private is3D: boolean;
private labelBoundingBox: BoundingBox;
private canvas: HTMLCanvasElement;

constructor(field, label) {
super(field, label);
Expand All @@ -40,32 +40,6 @@ export default class DetectionOverlay<
} else {
this.is3D = false;
}

if (this.label.mask) {
const [height, width] = this.label.mask.data.shape;

if (!height || !width) {
return;
}

this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;
this.imageData = new ImageData(
new Uint8ClampedArray(this.label.mask.image),
width,
height
);
const maskCtx = this.canvas.getContext("2d");
maskCtx.imageSmoothingEnabled = false;
maskCtx.clearRect(
0,
0,
this.label.mask.data.shape[1],
this.label.mask.data.shape[0]
);
maskCtx.putImageData(this.imageData, 0, 0);
}
}

containsPoint(state: Readonly<State>): CONTAINS {
Expand Down Expand Up @@ -169,16 +143,17 @@ export default class DetectionOverlay<
}

private drawMask(ctx: CanvasRenderingContext2D, state: Readonly<State>) {
if (!this.canvas) {
if (!this.label.mask?.bitmap) {
return;
}

const [tlx, tly, w, h] = this.label.bounding_box;
const [x, y] = t(state, tlx, tly);
const tmp = ctx.globalAlpha;
ctx.globalAlpha = state.options.alpha;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(
this.canvas,
this.label.mask.bitmap,
sashankaryal marked this conversation as resolved.
Show resolved Hide resolved
x,
y,
w * state.canvasBBox[2],
Expand Down Expand Up @@ -285,6 +260,13 @@ export default class DetectionOverlay<
const oh = state.strokeWidth / state.canvasBBox[3];
return [(bx - ow) * w, (by - oh) * h, (bw + ow * 2) * w, (bh + oh * 2) * h];
}

public cleanup(): void {
if (this.label.mask?.bitmap) {
this.label.mask?.bitmap.close();
this.label.mask.bitmap = null;
}
}
}

export const getDetectionPoints = (labels: DetectionLabel[]): Coordinates[] => {
Expand Down
56 changes: 16 additions & 40 deletions app/packages/looker/src/overlays/heatmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,25 @@ import {
getColor,
getRGBA,
getRGBAColor,
sizeBytesEstimate,
} from "@fiftyone/utilities";
import { ARRAY_TYPES, OverlayMask, TypedArray } from "../numpy";
import { ARRAY_TYPES, TypedArray } from "../numpy";
import { BaseState, Coordinates } from "../state";
import { isFloatArray } from "../util";
import { clampedIndex } from "../worker/painter";
import {
BaseLabel,
CONTAINS,
LabelMask,
Overlay,
PointInfo,
SelectData,
isShown,
} from "./base";
import { strokeCanvasRect, t } from "./util";

interface HeatMap {
data: OverlayMask;
image: ArrayBuffer;
}

interface HeatmapLabel extends BaseLabel {
map?: HeatMap;
map?: LabelMask;
range?: [number, number];
}

Expand All @@ -45,8 +42,6 @@ export default class HeatmapOverlay<State extends BaseState>
private label: HeatmapLabel;
private targets?: TypedArray;
private readonly range: [number, number];
private canvas: HTMLCanvasElement;
private imageData: ImageData;

constructor(field: string, label: HeatmapLabel) {
this.field = field;
Expand All @@ -68,25 +63,6 @@ export default class HeatmapOverlay<State extends BaseState>
if (!width || !height) {
return;
}

this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;

this.imageData = new ImageData(
new Uint8ClampedArray(this.label.map.image),
width,
height
);
const maskCtx = this.canvas.getContext("2d");
maskCtx.imageSmoothingEnabled = false;
maskCtx.clearRect(
0,
0,
this.label.map.data.shape[1],
this.label.map.data.shape[0]
);
maskCtx.putImageData(this.imageData, 0, 0);
}

containsPoint(state: Readonly<State>): CONTAINS {
Expand All @@ -101,22 +77,12 @@ export default class HeatmapOverlay<State extends BaseState>
}

draw(ctx: CanvasRenderingContext2D, state: Readonly<State>): void {
if (this.imageData) {
const maskCtx = this.canvas.getContext("2d");
maskCtx.imageSmoothingEnabled = false;
maskCtx.clearRect(
0,
0,
this.label.map.data.shape[1],
this.label.map.data.shape[0]
);
maskCtx.putImageData(this.imageData, 0, 0);

if (this.label.map?.bitmap) {
const [tlx, tly] = t(state, 0, 0);
const [brx, bry] = t(state, 1, 1);
const tmp = ctx.globalAlpha;
ctx.globalAlpha = state.options.alpha;
ctx.drawImage(this.canvas, tlx, tly, brx - tlx, bry - tly);
ctx.drawImage(this.label.map.bitmap, tlx, tly, brx - tlx, bry - tly);
ctx.globalAlpha = tmp;
}

Expand Down Expand Up @@ -235,6 +201,16 @@ export default class HeatmapOverlay<State extends BaseState>

return this.targets[index];
}

getSizeBytes(): number {
return sizeBytesEstimate(this.label);
}

public cleanup(): void {
if (this.label.map?.bitmap) {
this.label.map?.bitmap.close();
}
}
}

export const getHeatmapPoints = (labels: HeatmapLabel[]): Coordinates[] => {
Expand Down
46 changes: 17 additions & 29 deletions app/packages/looker/src/overlays/segmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
* Copyright 2017-2024, Voxel51, Inc.
*/

import { getColor } from "@fiftyone/utilities";
import { ARRAY_TYPES, OverlayMask, TypedArray } from "../numpy";
import { getColor, sizeBytesEstimate } from "@fiftyone/utilities";
import { ARRAY_TYPES, TypedArray } from "../numpy";
import { BaseState, Coordinates, MaskTargets } from "../state";
import {
BaseLabel,
CONTAINS,
LabelMask,
Overlay,
PointInfo,
SelectData,
Expand All @@ -16,10 +17,7 @@ import {
import { isRgbMaskTargets, strokeCanvasRect, t } from "./util";

interface SegmentationLabel extends BaseLabel {
mask?: {
data: OverlayMask;
image: ArrayBuffer;
};
mask?: LabelMask;
}

interface SegmentationInfo extends BaseLabel {
Expand All @@ -34,8 +32,6 @@ export default class SegmentationOverlay<State extends BaseState>
readonly field: string;
private label: SegmentationLabel;
private targets?: TypedArray;
private canvas: HTMLCanvasElement;
private imageData: ImageData;

private isRgbMaskTargets = false;

Expand All @@ -53,6 +49,7 @@ export default class SegmentationOverlay<State extends BaseState>
if (!this.label.mask) {
return;
}

const [height, width] = this.label.mask.data.shape;

if (!height || !width) {
Expand All @@ -62,25 +59,6 @@ export default class SegmentationOverlay<State extends BaseState>
this.targets = new ARRAY_TYPES[this.label.mask.data.arrayType](
this.label.mask.data.buffer
);

this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;

this.imageData = new ImageData(
new Uint8ClampedArray(this.label.mask.image),
width,
height
);
const maskCtx = this.canvas.getContext("2d");
maskCtx.imageSmoothingEnabled = false;
maskCtx.clearRect(
0,
0,
this.label.mask.data.shape[1],
this.label.mask.data.shape[0]
);
maskCtx.putImageData(this.imageData, 0, 0);
}

containsPoint(state: Readonly<State>): CONTAINS {
Expand All @@ -99,12 +77,12 @@ export default class SegmentationOverlay<State extends BaseState>
return;
}

if (this.imageData) {
if (this.label.mask?.bitmap) {
const [tlx, tly] = t(state, 0, 0);
const [brx, bry] = t(state, 1, 1);
const tmp = ctx.globalAlpha;
ctx.globalAlpha = state.options.alpha;
ctx.drawImage(this.canvas, tlx, tly, brx - tlx, bry - tly);
ctx.drawImage(this.label.mask.bitmap, tlx, tly, brx - tlx, bry - tly);
ctx.globalAlpha = tmp;
}

Expand Down Expand Up @@ -278,6 +256,16 @@ export default class SegmentationOverlay<State extends BaseState>
}
return this.targets[index];
}

getSizeBytes(): number {
return sizeBytesEstimate(this.label);
}
sashankaryal marked this conversation as resolved.
Show resolved Hide resolved

public cleanup(): void {
if (this.label.mask?.bitmap) {
this.label.mask?.bitmap.close();
}
}
}

export const getSegmentationPoints = (
Expand Down
Loading
Loading