TripoSplat-WebGPU / src /models /sharp /SharpWebGPUModel.ts
Yosun's picture
Deploy TripoSplat WebGPU demo
31c7d49 verified
Raw
History Blame Contribute Delete
3.59 kB
import { convertFocalMmToPx } from '../../lib/focal'
import { imageBitmapToSharpTensor } from '../../lib/image'
import {
DEFAULT_FOCAL_MM,
DEFAULT_MAX_GAUSSIANS,
DEFAULT_OPACITY_THRESHOLD,
DEFAULT_WEB_MODEL_URL,
SHARP_INTERNAL_RESOLUTION,
} from '../../lib/sharpConstants'
import { SharpWorkerClient } from '../../lib/sharpWorkerClient'
import type { WorkerStatusMessage } from '../../workers/messages'
import type {
GaussianScene,
GenerationOptions,
GenerationProgress,
ImageToGaussianModel,
} from '../ImageToGaussianModel'
import { throwIfAborted } from '../ImageToGaussianModel'
export interface SharpWebGPUModelOptions {
modelUrl?: string
onStatus?: (status: WorkerStatusMessage) => void
}
/**
* The known-good ml-sharp-web pipeline behind the model-independent lifecycle.
*
* SHARP's graph, NDC unprojection, opacity pruning and camera-rich PLY writer
* intentionally remain in its existing worker. TripoSplat does not share those
* assumptions; only the public model contract is shared.
*/
export class SharpWebGPUModel implements ImageToGaussianModel {
readonly modelUrl: string
private readonly worker: SharpWorkerClient
private loaded = false
private disposed = false
constructor(options: SharpWebGPUModelOptions = {}) {
this.modelUrl = options.modelUrl ?? DEFAULT_WEB_MODEL_URL
this.worker = new SharpWorkerClient(options.onStatus)
}
async load(): Promise<void> {
this.assertUsable()
if (this.loaded) return
await this.worker.loadModel({ modelUrl: this.modelUrl })
this.loaded = true
}
async generate(image: ImageBitmap, options: GenerationOptions = {}): Promise<GaussianScene> {
this.assertUsable()
throwIfAborted(options.signal)
await this.load()
const progress = this.createProgressReporter(options)
progress({ stage: 'preprocessing', message: 'Preparing SHARP image tensor…' })
const imageTensor = imageBitmapToSharpTensor(image, SHARP_INTERNAL_RESOLUTION)
throwIfAborted(options.signal)
const focalPx = options.focalPx ?? convertFocalMmToPx(image.width, image.height, DEFAULT_FOCAL_MM)
if (!Number.isFinite(focalPx) || focalPx <= 0) {
throw new Error('SHARP focalPx must be a positive finite number.')
}
progress({ stage: 'inference', message: 'Running SHARP with ONNX Runtime WebGPU…' })
const result = await this.worker.runInference({
modelUrl: this.modelUrl,
imageTensor: imageTensor.buffer,
imageWidth: image.width,
imageHeight: image.height,
focalPx,
disparityFactor: focalPx / image.width,
opacityThreshold: options.opacityThreshold ?? DEFAULT_OPACITY_THRESHOLD,
maxGaussians: options.maxGaussians ?? DEFAULT_MAX_GAUSSIANS,
})
throwIfAborted(options.signal)
return {
model: 'sharp',
count: result.selectedGaussians,
totalCount: result.totalGaussians,
ply: new Uint8Array(result.plyBuffer as ArrayBuffer),
coordinateSystem: 'opencv-camera',
colorSpace: 'linear-rgb',
metadata: {
focalPx,
imageWidth: image.width,
imageHeight: image.height,
},
}
}
async dispose(): Promise<void> {
if (this.disposed) return
this.disposed = true
this.loaded = false
this.worker.dispose()
}
private assertUsable(): void {
if (this.disposed) {
throw new Error('SharpWebGPUModel has been disposed.')
}
}
private createProgressReporter(options: GenerationOptions): (progress: GenerationProgress) => void {
return (progress) => options.onProgress?.(progress)
}
}