-
-
Notifications
You must be signed in to change notification settings - Fork 30
docs: Binary image segmentation example #1795
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
Open
aleksanderkatan
wants to merge
57
commits into
main
Choose a base branch
from
docs/binary-image-segmentation-example
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 38 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
fa18202
Fork chroma-keying, rewrite it to tgsl
661ac6b
Use fixed resources
cad7e0b
Use const
707946b
Update Chroma Keying
58e721b
Remove unnecessary controls
3995789
Split into files
5c582be
Add onnx
37d78bf
Merge remote-tracking branch 'origin/main' into docs/binary-image-seg…
c8af87a
Semi working state
4b9cae9
Add downscaling pipeline
d23f00b
Somewhat working one frame
0376eb3
Extract fullscreen triangle and model creation to dedicated files
8d5912a
Remove ios handling temporarily (surely)
c077735
Move downscale to another file
53f0677
Change downscale from render to compute
75111fb
More refactor
5180176
More refactor
2788f54
Rewrite generateMasko to compute
67b5b9b
More refactor
c9d4113
More refactor
8a9c68a
More refactor
2d8584e
Add a new model to assets
2152bca
Switch models
9dea9f7
Fix the offset
f69b10b
Better model
5555457
Calculate mask independently
0f78115
Add blur
d7956d5
Remove options uniform
20224b4
Fix the blur to cover the entire screen
8b24433
Merge remote-tracking branch 'origin/main' into docs/binary-image-seg…
89fb49f
Use fullScreenTriangle
f2ce2cf
Merge remote-tracking branch 'origin/main' into docs/binary-image-seg…
e29e193
Fix camera tresholding, use fullscreen triangle
71d6fe2
Update tests
e74fbb7
Add blur strength controls
fdb447d
Update tags and add thumbnail
a9b6165
Add proper cleanup
ef4a9d0
Docs & refactor
bd7b82a
Add attribution
cd21293
Review fixes
136ba07
Remove the x coordinate flip
9cee84d
Add back iOS support
0ed89b9
Make attribution transparent until the model loads
744bd47
Use mipmaps for blur
9d75a5f
Add controls to swap between blur types
152bb5b
Add subgroups check
bb13302
Create textures for the blurred images immediately
131a6c6
Precompute bind groups for blur
1e9b36a
Precompute mask bind group
5d14aa6
Merge remote-tracking branch 'origin/main' into docs/binary-image-seg…
89b88e7
Lint
a40be8a
Throw an appropriate error on Safari
2bfb616
Merge remote-tracking branch 'origin/main' into docs/binary-image-seg…
b3a16ea
Remove iOS handling (again...)
652673b
Add back lazy textures (didn't work with mobile resolutions)
f715c32
Merge branch 'main' into docs/binary-image-segmentation-example
aleksanderkatan 7d4ce5b
Update tests
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
15 changes: 15 additions & 0 deletions
15
apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <canvas></canvas> | ||
| <video autoplay playsinline></video> | ||
|
|
||
| <style> | ||
| /* Keep the video "painted" so Safari continues decoding frames. */ | ||
| video { | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: 1px; | ||
| height: 1px; | ||
| opacity: 0; | ||
| pointer-events: none; | ||
| } | ||
| </style> |
289 changes: 289 additions & 0 deletions
289
apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| import tgpu, { | ||
| type RenderFlag, | ||
| type SampledFlag, | ||
| type StorageFlag, | ||
| type TgpuBindGroup, | ||
| type TgpuTexture, | ||
| } from 'typegpu'; | ||
| import { fullScreenTriangle } from 'typegpu/common'; | ||
| import * as d from 'typegpu/data'; | ||
| import { MODEL_HEIGHT, MODEL_WIDTH, prepareSession } from './model.ts'; | ||
| import { | ||
| blockDim, | ||
| blurLayout, | ||
| drawWithMaskLayout, | ||
| generateMaskLayout, | ||
| prepareModelInputLayout, | ||
| } from './schemas.ts'; | ||
| import { | ||
| computeFn, | ||
| drawWithMaskFragment, | ||
| generateMaskFromOutput, | ||
| prepareModelInput, | ||
| } from './shaders.ts'; | ||
|
|
||
| // setup | ||
|
|
||
| const canvas = document.querySelector('canvas') as HTMLCanvasElement; | ||
| const video = document.querySelector('video') as HTMLVideoElement; | ||
|
|
||
| if (navigator.mediaDevices.getUserMedia) { | ||
| video.srcObject = await navigator.mediaDevices.getUserMedia({ | ||
| video: { | ||
| facingMode: 'user', | ||
| width: { ideal: 1280 }, | ||
| height: { ideal: 720 }, | ||
| frameRate: { ideal: 60 }, | ||
| }, | ||
| }); | ||
| } else { | ||
| throw new Error('getUserMedia not supported'); | ||
| } | ||
|
|
||
| const adapter = await navigator.gpu?.requestAdapter(); | ||
| const device = await adapter?.requestDevice({ | ||
| label: `my device ${performance.now()}`, | ||
| }) as GPUDevice; | ||
| if (!device || !adapter) { | ||
| throw new Error('Failed to initialize device.'); | ||
| } | ||
|
|
||
| // monkey patching ONNX (setting device in the environment does nothing) | ||
| const oldRequestAdapter = navigator.gpu.requestAdapter; | ||
| const oldRequestDevice = adapter.requestDevice; | ||
| navigator.gpu.requestAdapter = async () => adapter; | ||
| adapter.requestDevice = async () => device; | ||
| const root = await tgpu.initFromDevice({ device }); | ||
| const context = canvas.getContext('webgpu') as GPUCanvasContext; | ||
| const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); | ||
|
|
||
| context.configure({ | ||
| device, | ||
| format: presentationFormat, | ||
| alphaMode: 'premultiplied', | ||
| }); | ||
|
|
||
| // resources | ||
|
|
||
| let iterations = 10; | ||
|
|
||
| const sampler = root['~unstable'].createSampler({ | ||
| magFilter: 'linear', | ||
| minFilter: 'linear', | ||
| }); | ||
|
|
||
| const maskTexture = root['~unstable'].createTexture({ | ||
| size: [MODEL_WIDTH, MODEL_HEIGHT], | ||
| format: 'rgba8unorm', | ||
| dimension: '2d', | ||
| }).$usage('sampled', 'render', 'storage'); | ||
|
|
||
| const modelInputBuffer = root | ||
| .createBuffer(d.arrayOf(d.f32, 3 * MODEL_WIDTH * MODEL_HEIGHT)) | ||
| .$usage('storage'); | ||
|
|
||
| const modelOutputBuffer = root | ||
| .createBuffer(d.arrayOf(d.f32, 1 * MODEL_WIDTH * MODEL_HEIGHT)) | ||
| .$usage('storage'); | ||
|
|
||
| let blurredTextures: ( | ||
| & TgpuTexture<{ | ||
| size: [number, number]; | ||
| format: 'rgba8unorm'; | ||
| }> | ||
| & SampledFlag | ||
| & RenderFlag | ||
| & StorageFlag | ||
| )[]; | ||
|
|
||
| let blurBindGroups: TgpuBindGroup<(typeof blurLayout)['entries']>[]; | ||
|
|
||
| const zeroBuffer = root.createBuffer(d.u32, 0).$usage('uniform'); | ||
| const oneBuffer = root.createBuffer(d.u32, 1).$usage('uniform'); | ||
|
|
||
| // pipelines | ||
|
|
||
| const prepareModelInputPipeline = root['~unstable'].prepareDispatch( | ||
| prepareModelInput, | ||
| ); | ||
|
|
||
| const session = await prepareSession( | ||
| root.unwrap(modelInputBuffer), | ||
| root.unwrap(modelOutputBuffer), | ||
| ); | ||
|
|
||
| const generateMaskFromOutputPipeline = root['~unstable'].prepareDispatch( | ||
| generateMaskFromOutput, | ||
| ); | ||
|
|
||
| const blurPipeline = root['~unstable'] | ||
| .withCompute(computeFn) | ||
| .createPipeline(); | ||
|
|
||
| const drawWithMaskPipeline = root['~unstable'] | ||
| .withVertex(fullScreenTriangle, {}) | ||
| .withFragment(drawWithMaskFragment, { format: presentationFormat }) | ||
| .createPipeline(); | ||
|
|
||
| // recalculating mask | ||
|
|
||
| let calculateMaskCallbackId: number | undefined; | ||
|
|
||
| async function processCalculateMask() { | ||
| if (video.readyState < 2) { | ||
| calculateMaskCallbackId = video.requestVideoFrameCallback( | ||
| processCalculateMask, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| prepareModelInputPipeline | ||
| .with(root.createBindGroup(prepareModelInputLayout, { | ||
| inputTexture: device.importExternalTexture({ source: video }), | ||
| outputBuffer: modelInputBuffer, | ||
| sampler, | ||
| })) | ||
| .dispatchThreads(MODEL_WIDTH, MODEL_HEIGHT); | ||
|
|
||
| root['~unstable'].flush(); | ||
|
|
||
| await session.run(); | ||
|
|
||
| generateMaskFromOutputPipeline | ||
| .with(root.createBindGroup(generateMaskLayout, { | ||
| maskTexture: maskTexture, | ||
| outputBuffer: modelOutputBuffer, | ||
| })) | ||
| .dispatchThreads(MODEL_WIDTH, MODEL_HEIGHT); | ||
|
|
||
| calculateMaskCallbackId = video.requestVideoFrameCallback( | ||
| processCalculateMask, | ||
| ); | ||
| } | ||
| calculateMaskCallbackId = video.requestVideoFrameCallback(processCalculateMask); | ||
|
|
||
| // frame | ||
|
|
||
| function onVideoChange(size: { width: number; height: number }) { | ||
| const aspectRatio = size.width / size.height; | ||
| video.style.height = `${video.clientWidth / aspectRatio}px`; | ||
| if (canvas.parentElement) { | ||
| canvas.parentElement.style.aspectRatio = `${aspectRatio}`; | ||
| canvas.parentElement.style.height = | ||
| `min(100cqh, calc(100cqw/(${aspectRatio})))`; | ||
| } | ||
| blurredTextures = [0, 1].map(() => | ||
| root['~unstable'].createTexture({ | ||
| size: [size.width, size.height], | ||
| format: 'rgba8unorm', | ||
| dimension: '2d', | ||
| }).$usage('sampled', 'render', 'storage') | ||
| ); | ||
| blurBindGroups = [ | ||
| root.createBindGroup(blurLayout, { | ||
| flip: zeroBuffer, | ||
| inTexture: blurredTextures[0], | ||
| outTexture: blurredTextures[1], | ||
| sampler, | ||
| }), | ||
| root.createBindGroup(blurLayout, { | ||
| flip: oneBuffer, | ||
| inTexture: blurredTextures[1], | ||
| outTexture: blurredTextures[0], | ||
| sampler, | ||
| }), | ||
| ]; | ||
| } | ||
|
|
||
| let videoFrameCallbackId: number | undefined; | ||
| let frameSize: { width: number; height: number } | undefined; | ||
|
|
||
| async function processVideoFrame( | ||
| _: number, | ||
| metadata: VideoFrameCallbackMetadata, | ||
| ) { | ||
| if (video.readyState < 2) { | ||
| videoFrameCallbackId = video.requestVideoFrameCallback(processVideoFrame); | ||
| return; | ||
| } | ||
|
|
||
| const frameWidth = metadata.width; | ||
| const frameHeight = metadata.height; | ||
|
|
||
| if (!frameSize) { | ||
| frameSize = { width: frameWidth, height: frameHeight }; | ||
| onVideoChange(frameSize); | ||
| } | ||
|
|
||
| blurredTextures[0].write(video); | ||
|
|
||
| for (const _ of Array(iterations)) { | ||
| blurPipeline | ||
| .with(blurBindGroups[0]) | ||
| .dispatchWorkgroups( | ||
| Math.ceil(frameWidth / blockDim), | ||
| Math.ceil(frameHeight / 4), | ||
| ); | ||
| blurPipeline | ||
| .with(blurBindGroups[1]) | ||
| .dispatchWorkgroups( | ||
| Math.ceil(frameHeight / blockDim), | ||
| Math.ceil(frameWidth / 4), | ||
| ); | ||
| } | ||
|
|
||
| drawWithMaskPipeline | ||
| .withColorAttachment({ | ||
| view: context.getCurrentTexture().createView(), | ||
| clearValue: [1, 1, 1, 1], | ||
| loadOp: 'clear', | ||
| storeOp: 'store', | ||
| }) | ||
| .with(root.createBindGroup(drawWithMaskLayout, { | ||
| inputTexture: device.importExternalTexture({ source: video }), | ||
| inputBlurredTexture: blurredTextures[0], | ||
| maskTexture: maskTexture, | ||
aleksanderkatan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| sampler, | ||
| })) | ||
| .draw(3); | ||
|
|
||
| videoFrameCallbackId = video.requestVideoFrameCallback(processVideoFrame); | ||
| } | ||
| videoFrameCallbackId = video.requestVideoFrameCallback(processVideoFrame); | ||
|
|
||
| // #region Example controls & Cleanup | ||
|
|
||
| export const controls = { | ||
| 'blur strength': { | ||
| initial: 10, | ||
| min: 0, | ||
| max: 20, | ||
| step: 1, | ||
| onSliderChange(newValue: number) { | ||
| iterations = newValue; | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export function onCleanup() { | ||
| if (videoFrameCallbackId !== undefined) { | ||
| video.cancelVideoFrameCallback(videoFrameCallbackId); | ||
| } | ||
| if (calculateMaskCallbackId !== undefined) { | ||
| video.cancelVideoFrameCallback(calculateMaskCallbackId); | ||
| } | ||
| session.release(); | ||
| if (video.srcObject) { | ||
| for (const track of (video.srcObject as MediaStream).getTracks()) { | ||
| track.stop(); | ||
| } | ||
| } | ||
| navigator.gpu.requestAdapter = oldRequestAdapter; | ||
| if (adapter) { | ||
| adapter.requestDevice = oldRequestDevice; | ||
| } | ||
|
|
||
| root.destroy(); | ||
| } | ||
|
|
||
| // #endregion | ||
5 changes: 5 additions & 0 deletions
5
apps/typegpu-docs/src/examples/image-processing/background-segmentation/meta.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "title": "Background Segmentation", | ||
| "category": "image-processing", | ||
| "tags": ["experimental", "camera", "onnx"] | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.