Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ members = [
'examples/phi-3-vision',
'examples/modnet',
'examples/sentence-transformers',
'examples/training'
'examples/training',
'examples/wasm-emscripten',
]
default-members = [
'.',
Expand Down
13 changes: 13 additions & 0 deletions examples/wasm-emscripten/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[unstable]
build-std = ["std"]

[build]
target = "wasm32-unknown-emscripten"
rustflags = [
"-Clink-args=-fno-rtti -pthread -msimd128 -msse4.2 -sEXPORT_ALL -sSTACK_SIZE=5MB -sUSE_PTHREADS -sDEFAULT_PTHREAD_STACK_SIZE=2MB -sPTHREAD_POOL_SIZE=20 -sINITIAL_MEMORY=1GB -sEXPORT_ES6 --no-entry",
"-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals"
]

# Debug build might benefit from further flags, but one cannot yet set debug-only rustflags.
# See: https://github.com/rust-lang/cargo/issues/10271
# -fexceptions, -gsource-map, -sASSERTIONS, -sNO_DISABLE_EXCEPTION_CATCHING
3 changes: 3 additions & 0 deletions examples/wasm-emscripten/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
libonnxruntime.a
yolov8m.onnx
emsdk
14 changes: 14 additions & 0 deletions examples/wasm-emscripten/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
publish = false
name = "example-wasm-emscripten"
version = "0.0.0"
edition = "2021"

[dependencies]
ort = { path = "../../", default-features = false, features = ["ndarray", "webgpu", "download-binaries"] }
ndarray = "0.16"
image = "0.25"

[build-dependencies]
glob = "0.3"
reqwest = { version = "0.12", features = ["blocking"] }
19 changes: 19 additions & 0 deletions examples/wasm-emscripten/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# WASM-Emscripten Example
Example how to use `ort` to run `onnxruntime` in the Web with multi-threaded execution. Here inferencing the YoloV8 model.

## Prepare environment
1. Requires a recent Rust installation via `rustup`, `git`, `cmake` and `build-essentials`, `libssl-dev`, and `pkg-config` under Ubuntu or `xcode-select` under macOS.
1. Install the Rust nightly toolchain with `rustup install nightly`.
1. Add Emscripten as Rust target with `rustup target add wasm32-unknown-emscripten --toolchain nightly`.
1. Clone Emscripten SDK via `git clone https://github.com/emscripten-core/emsdk.git --depth 1`.
1. Install Emscripten SDK 4.0.4 locally to [match version used in ONNX runtime](https://github.com/microsoft/onnxruntime/blob/fe7634eb6f20b656a3df978a6a2ef9b3ea00c59d/.gitmodules#L10) via `./emsdk/emsdk install 4.0.4`.
1. Prepare local Emscripten SDK via `./emsdk/emsdk activate 4.0.4`.

Environment tested on Ubuntu 24.04 and macOS 14.7.1.

## Build example
1. Set local Emscripten SDK in current session via `source ./emsdk/emsdk_env.sh`.
1. Build the example via `cargo build` for a debug build or `cargo build --release` for a release build.

## Serve example
1. Serve a debug build via `python3 serve.py` or a release build via `python3 serve.py --release`. Pre-installed Python 3 should be sufficient.
46 changes: 46 additions & 0 deletions examples/wasm-emscripten/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
fn copy_dir_all(src: impl AsRef<std::path::Path>, dst: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::create_dir_all(&dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}

fn main() {
use std::{
fs::File,
io::{Read, Write}
};

use reqwest::blocking::get;

// Determine mode.
let mode = match cfg!(debug_assertions) {
true => "debug",
false => "release"
};

// Download model.
{
let mut request = get("https://parcel.pyke.io/v2/cdn/assetdelivery/ortrsv2/ex_models/yolov8m.onnx").expect("Cannot request model.");
let mut buf = Vec::<u8>::new();
request.read_to_end(&mut buf).expect("Cannot read model.");
let mut file = File::create("./yolov8m.onnx").expect("Cannot create model file.");
file.write_all(&buf).expect("Cannot store model.");
}

// Copy index.html and pictures to target directory.
{
println!("cargo:rerun-if-changed=index.html");
std::fs::copy("index.html", format!("../../target/wasm32-unknown-emscripten/{mode}/index.html")).expect("Cannot copy index.html.");

println!("cargo:rerun-if-changed=pictures/*");
copy_dir_all("pictures", format!("../../target/wasm32-unknown-emscripten/{mode}/pictures")).expect("Cannot copy pictures.");
}
}
76 changes: 76 additions & 0 deletions examples/wasm-emscripten/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Ort in the Web</title>
<script type="module">
import example from "./example-wasm-emscripten.js";
window.addEventListener("DOMContentLoaded", () => {
example().then(instance => {
// Get elements from page.
const canvas = document.getElementById("canvas");
const select = document.getElementById("select");
const button = document.getElementById("button");
const ctx = canvas.getContext("2d");

if (select !== undefined && button !== undefined && ctx !== undefined) {
// Paint the picture into the canvas.
const updateCanvas = () => {
const image = new Image();
image.src = select.value;
image.onload = () => {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
};
};
select.addEventListener("change", (event) => {
updateCanvas();
});
updateCanvas(); // Initial call.

// Classify the objects in the canvas.
button.addEventListener("click", () => {
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
const buffer = new Uint8Array(data.data.buffer);
const byteCount = canvas.width * canvas.height * 4;
let pointer = instance._alloc(byteCount);
for (let i = 0; i < byteCount; i++) {
instance.HEAPU8[pointer + i] = buffer[i]; // Inefficient copying.
}
instance._detect_objects(pointer, canvas.width, canvas.height);
instance._dealloc(pointer, byteCount);
});

console.log("Ready to classify. Classification results will be printed here.")
};
});
});


</script>
</head>

<body>
<h1>Example using the ort-crate in the Web</h1>
<div>
<canvas id="canvas" style="width: 512px; background: gray;"></canvas>
</div>
<div>
<select id="select">
<option value="pictures/banana.jpg">Banana</option>
<option value="pictures/rat.jpg">Rat</option>
<option value="pictures/bicycle.jpg">Bicycle</option>
<option value="pictures/baseball.jpg">Baseball</option>
</select>
<button id="button">
Classify objects!
</button>
</div>
<div>
See the developer console for classification results.
</div>
</body>

</html>
Binary file added examples/wasm-emscripten/pictures/banana.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/wasm-emscripten/pictures/baseball.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/wasm-emscripten/pictures/bicycle.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions examples/wasm-emscripten/pictures/licenses.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Pictures are taken from Pexels.com on 3rd February 2025.

"baseball.jpg": https://www.pexels.com/photo/woman-in-white-sleeveless-jersey-holding-a-black-baseball-bat-pitching-the-green-tennis-ball-160533/
"bicycle.jpg": https://www.pexels.com/photo/cyclist-at-potsdamer-platz-in-berlin-on-a-rainy-day-28608592/
"rat.jpg: https://www.pexels.com/photo/tilt-shot-of-brown-rat-209053/
"banana.jpg": https://www.pexels.com/photo/yellow-banana-fruit-on-white-surface-1166648/
Binary file added examples/wasm-emscripten/pictures/rat.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions examples/wasm-emscripten/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Threading requires to compile the standard library with "+atomics,+bulk-memory,+mutable-globals".
# See: https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html#building-the-demo
[toolchain]
channel = "nightly"
components = ["rust-src"]
28 changes: 28 additions & 0 deletions examples/wasm-emscripten/serve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from http.server import HTTPServer, SimpleHTTPRequestHandler
from os import chdir, path
from argparse import ArgumentParser

parser = ArgumentParser()
parser.add_argument("-d", "--debug", action="store_true", help="serve debug build")
parser.add_argument("-r", "--release", action="store_true", help="serve release build")
args = parser.parse_args()
if args.release:
mode = "release"
else:
mode = "debug"

# SharedArrayBuffer required for threaded execution need specific CORS headers.
# See: https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html#browser-requirements
# See: https://emscripten.org/docs/porting/pthreads.html
# See: https://stackoverflow.com/a/68358986
class CORSRequestHandler (SimpleHTTPRequestHandler):
def end_headers (self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
SimpleHTTPRequestHandler.end_headers(self)

# Serve index.html.
chdir(path.join(path.dirname(__file__), f"../../target/wasm32-unknown-emscripten/{mode}"))
httpd = HTTPServer(("localhost", 5555), CORSRequestHandler)
print(f"Serving {mode} build at: http://localhost:5555")
httpd.serve_forever()
117 changes: 117 additions & 0 deletions examples/wasm-emscripten/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#![no_main]

// Below YoloV8 implementation partly copied from the "yolov8" example.
#[derive(Debug, Clone, Copy)]
struct BoundingBox {
x1: f32,
y1: f32,
x2: f32,
y2: f32
}

#[rustfmt::skip]
static YOLOV8_CLASS_LABELS: [&str; 80] = [
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant",
"bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
"sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle",
"wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli",
"carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet",
"tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
];

#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut std::os::raw::c_void {
unsafe {
let layout = std::alloc::Layout::from_size_align(size, std::mem::align_of::<u8>()).expect("Cannot create memory layout.");
return std::alloc::alloc(layout) as *mut std::os::raw::c_void;
}
}

#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut std::os::raw::c_void, size: usize) {
unsafe {
let layout = std::alloc::Layout::from_size_align(size, std::mem::align_of::<u8>()).expect("Cannot create memory layout.");
std::alloc::dealloc(ptr as *mut u8, layout);
}
}

#[no_mangle]
pub extern "C" fn detect_objects(ptr: *const u8, width: u32, height: u32) {
ort::init()
.with_global_thread_pool(ort::environment::GlobalThreadPoolOptions::default())
.commit()
.expect("Cannot initialize ort.");

let mut builder = ort::session::Session::builder()
.expect("Cannot create Session builder.")
.with_optimization_level(ort::session::builder::GraphOptimizationLevel::Level3)
.expect("Cannot optimize graph.")
.with_parallel_execution(true)
.expect("Cannot activate parallel execution.")
.with_intra_threads(2)
.expect("Cannot set intra thread count.")
.with_inter_threads(1)
.expect("Cannot set inter thread count.");

let use_webgpu = true; // TODO: Make `use_webgpu` a parameter of `detect_objects`? Or say in README to change it here.
if use_webgpu {
use ort::execution_providers::ExecutionProvider;
let ep = ort::execution_providers::WebGPUExecutionProvider::default();
if ep.is_available().expect("Cannot check for availability of WebGPU ep.") {
ep.register(&mut builder).expect("Cannot register WebGPU ep.");
} else {
println!("WebGPU ep is not available.");
}
}

let mut session = builder
.commit_from_memory(include_bytes!("../yolov8m.onnx"))
.expect("Cannot commit model.");

let image_data = unsafe { std::slice::from_raw_parts(ptr, (width * height * 4) as usize).to_vec() }; // Copy via .to_vec might be not necessary as memory lives long enough.
let image = image::ImageBuffer::<image::Rgba<u8>, Vec<u8>>::from_raw(width, height, image_data).expect("Cannot parse input image.");
let image640 = image::imageops::resize(&image, 640, 640, image::imageops::FilterType::CatmullRom);
let tensor =
ort::value::Tensor::from_array(ndarray::Array4::from_shape_fn((1, 3, 640, 640), |(_, c, y, x)| image640[(x as u32, y as u32)][c] as f32 / 255_f32))
.unwrap();

let outputs: ort::session::SessionOutputs = session.run(ort::inputs!["images" => tensor]).unwrap();
let output = outputs["output0"].try_extract_tensor::<f32>().unwrap().t().into_owned();

let mut boxes = Vec::new();
let output = output.slice(ndarray::s![.., .., 0]);
for row in output.axis_iter(ndarray::Axis(0)) {
let row: Vec<_> = row.iter().copied().collect();
let (class_id, prob) = row
.iter()
// skip bounding box coordinates
.skip(4)
.enumerate()
.map(|(index, value)| (index, *value))
.reduce(|accum, row| if row.1 > accum.1 { row } else { accum })
.unwrap();
if prob < 0.5 {
continue;
}
let label = YOLOV8_CLASS_LABELS[class_id];
let xc = row[0] / 640. * (width as f32);
let yc = row[1] / 640. * (height as f32);
let w = row[2] / 640. * (width as f32);
let h = row[3] / 640. * (height as f32);
boxes.push((
BoundingBox {
x1: xc - w / 2.,
y1: yc - h / 2.,
x2: xc + w / 2.,
y2: yc + h / 2.
},
label,
prob
));
}

// Just print the results to the console for now.
println!("{:#?}", boxes);
}
2 changes: 1 addition & 1 deletion ort-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ fn static_link_prerequisites(using_pyke_libs: bool) {
println!("cargo:rustc-link-lib=D3D12");
println!("cargo:rustc-link-lib=DirectML");
}
if cfg!(feature = "webgpu") && using_pyke_libs {
if cfg!(feature = "webgpu") && !target_triple.contains("wasm32") && using_pyke_libs {
println!("cargo:rustc-link-lib=webgpu_dawn");
}
}
Expand Down
Loading