Rust ve WebAssembly ile Tarayıcıda Gerçek Zamanlı Görüntü İşleme
Tarayıcıda gerçek zamanlı görüntü işleme yapmak istediğinizde, JavaScript’in tek başına yetersiz kaldığı anları muhtemelen yaşamışsınızdır. Bir video akışı üzerinde yüz tanıma, kenar algılama ya da piksel bazlı renk dönüşümü yapmaya çalıştığınızda, JavaScript’in performans duvarına çarparsınız. İşte tam bu noktada Rust ve WebAssembly ikilisi sahneye giriyor. Bu yazıda, Rust ile yazılmış görüntü işleme kodunu WebAssembly’e derleyerek tarayıcıda nasıl gerçek zamanlı çalıştırabileceğinizi, bir sysadmin gözüyle pratik senaryolar üzerinden ele alacağız.
Neden Rust + WebAssembly?
JavaScript single-threaded çalışır ve garbage collector’ı zaman zaman beklenmedik anlarda devreye girerek frame drop’larına neden olur. Görüntü işleme ise doğası gereği CPU yoğun bir iştir. Bir 1080p karedeki 2 milyon pikseli her saniye 30 kez işlemek istiyorsanız, saniyede 60 milyon piksel üzerinde işlem yapmanız gerekiyor. Bu rakamı JavaScript’le verimli şekilde karşılamak gerçekten zordur.
Rust ise bellek güvenliğini derleme zamanında garanti ederken, C/C++ seviyesinde performans sunar. WebAssembly’e derlendiğinde, tarayıcı bu kodu neredeyse native hızında çalıştırır. Garbage collector yoktur, beklenmedik duraklamalar yaşanmaz. Bu kombinasyon, özellikle şu senaryolarda kritik hale gelir:
- Endüstriyel kamera sistemlerinde web tabanlı kalite kontrol
- Güvenlik kamerası akışlarında gerçek zamanlı nesne tespiti
- Medikal görüntüleme uygulamalarında ön işleme
- Video konferans sistemlerinde arka plan bulanıklaştırma
Geliştirme Ortamını Kurma
Önce Rust toolchain’ini ve gerekli araçları kuralım. Bu kısmı bir Ubuntu 22.04 makinesinde yapıyoruz ama süreç macOS’ta da birebir aynıdır.
# Rust kurulumu
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# wasm32 target ekle
rustup target add wasm32-unknown-unknown
# wasm-pack kur - bu araç Rust'ı tarayıcı için paketler
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# wasm-bindgen CLI (isteğe bağlı ama işe yarar)
cargo install wasm-bindgen-cli
# Projeyi oluştur
cargo new --lib rustimg-wasm
cd rustimg-wasm
Proje yapısını hazırladıktan sonra Cargo.toml dosyasını düzenleyelim:
cat > Cargo.toml << 'EOF'
[package]
name = "rustimg-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"console",
"ImageData",
"CanvasRenderingContext2d",
"HtmlCanvasElement",
"HtmlVideoElement",
"Window",
"Performance"
]}
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
EOF
opt-level = 3 ve lto = true ayarları önemlidir. Release build’de bu optimizasyonlar olmadan WebAssembly çıktısı beklenenden büyük ve yavaş olabilir. panic = "abort" ise hata durumlarında stack unwinding yerine direkt durarak binary boyutunu küçültür.
Temel Görüntü İşleme Fonksiyonları
Şimdi asıl Rust kodunu yazalım. src/lib.rs dosyasına başlayalım:
cat > src/lib.rs << 'EOF'
use wasm_bindgen::prelude::*;
// Piksel verisi üzerinde grayscale dönüşümü
// RGBA formatında gelen veriyi işler
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
// Her piksel 4 byte: R, G, B, A
for chunk in data.chunks_mut(4) {
// ITU-R BT.601 lüminans katsayıları
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// Alpha kanalını dokunmadan bırak
}
}
// Sobel kenar algılama filtresi
#[wasm_bindgen]
pub fn sobel_edge_detect(
data: &[u8],
output: &mut [u8],
width: u32,
height: u32,
) {
let w = width as usize;
let h = height as usize;
// Sobel çekirdeği yatay ve dikey
let gx: [[i32; 3]; 3] = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
let gy: [[i32; 3]; 3] = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for y in 1..h - 1 {
for x in 1..w - 1 {
let mut sum_x: i32 = 0;
let mut sum_y: i32 = 0;
for ky in 0..3 {
for kx in 0..3 {
let px = x + kx - 1;
let py = y + ky - 1;
let idx = (py * w + px) * 4;
// Grayscale değeri kullan
let pixel = data[idx] as i32;
sum_x += gx[ky][kx] * pixel;
sum_y += gy[ky][kx] * pixel;
}
}
let magnitude = ((sum_x * sum_x + sum_y * sum_y) as f32)
.sqrt()
.min(255.0) as u8;
let out_idx = (y * w + x) * 4;
output[out_idx] = magnitude;
output[out_idx + 1] = magnitude;
output[out_idx + 2] = magnitude;
output[out_idx + 3] = 255;
}
}
}
// Gaussian blur - arka plan bulanıklaştırma için
#[wasm_bindgen]
pub fn gaussian_blur(
data: &[u8],
output: &mut [u8],
width: u32,
height: u32,
radius: u32,
) {
let w = width as usize;
let h = height as usize;
let r = radius as usize;
for y in 0..h {
for x in 0..w {
let mut sum_r = 0u32;
let mut sum_g = 0u32;
let mut sum_b = 0u32;
let mut count = 0u32;
let y_start = if y >= r { y - r } else { 0 };
let y_end = (y + r + 1).min(h);
let x_start = if x >= r { x - r } else { 0 };
let x_end = (x + r + 1).min(w);
for ny in y_start..y_end {
for nx in x_start..x_end {
let idx = (ny * w + nx) * 4;
sum_r += data[idx] as u32;
sum_g += data[idx + 1] as u32;
sum_b += data[idx + 2] as u32;
count += 1;
}
}
let out_idx = (y * w + x) * 4;
output[out_idx] = (sum_r / count) as u8;
output[out_idx + 1] = (sum_g / count) as u8;
output[out_idx + 2] = (sum_b / count) as u8;
output[out_idx + 3] = data[out_idx + 3];
}
}
}
EOF
Threshold ve Renk Analizi
Gerçek dünya senaryolarında sıkça ihtiyaç duyulan iki fonksiyon daha ekleyelim. Endüstriyel ortamlarda çalışırken, bir bant üzerinde geçen ürünlerin rengini kontrol etmek ya da belirli bir renk aralığındaki nesneleri tespit etmek gibi görevler için bu fonksiyonlar kritiktir:
cat >> src/lib.rs << 'EOF'
// Adaptive threshold - değişken aydınlatma koşulları için
#[wasm_bindgen]
pub fn adaptive_threshold(
data: &mut [u8],
width: u32,
height: u32,
block_size: u32,
constant: i32,
) {
let w = width as usize;
let h = height as usize;
let half = (block_size / 2) as usize;
// Önce grayscale'e çevir
let gray: Vec<u8> = data
.chunks(4)
.map(|p| {
(0.299 * p[0] as f32
+ 0.587 * p[1] as f32
+ 0.114 * p[2] as f32) as u8
})
.collect();
for y in 0..h {
for x in 0..w {
let y_start = if y >= half { y - half } else { 0 };
let y_end = (y + half + 1).min(h);
let x_start = if x >= half { x - half } else { 0 };
let x_end = (x + half + 1).min(w);
let mut sum = 0u32;
let mut count = 0u32;
for ny in y_start..y_end {
for nx in x_start..x_end {
sum += gray[ny * w + nx] as u32;
count += 1;
}
}
let mean = (sum / count) as i32 - constant;
let pixel_val = gray[y * w + x] as i32;
let binary = if pixel_val > mean { 255u8 } else { 0u8 };
let idx = (y * w + x) * 4;
data[idx] = binary;
data[idx + 1] = binary;
data[idx + 2] = binary;
}
}
}
// HSV renk uzayında nesne tespiti
// Üretim bandındaki kırmızı nesneleri tespit örneği
#[wasm_bindgen]
pub fn color_mask_hsv(
data: &mut [u8],
h_min: f32,
h_max: f32,
s_min: f32,
v_min: f32,
) {
for chunk in data.chunks_mut(4) {
let r = chunk[0] as f32 / 255.0;
let g = chunk[1] as f32 / 255.0;
let b = chunk[2] as f32 / 255.0;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let h = if delta < 0.001 {
0.0
} else if max == r {
60.0 * (((g - b) / delta) % 6.0)
} else if max == g {
60.0 * ((b - r) / delta + 2.0)
} else {
60.0 * ((r - g) / delta + 4.0)
};
let h = if h < 0.0 { h + 360.0 } else { h };
let s = if max < 0.001 { 0.0 } else { delta / max };
let v = max;
let in_range = h >= h_min && h <= h_max && s >= s_min && v >= v_min;
if !in_range {
// Maskelenen alanı griyle göster
let gray = (0.299 * chunk[0] as f32
+ 0.587 * chunk[1] as f32
+ 0.114 * chunk[2] as f32) as u8;
chunk[0] = gray / 3;
chunk[1] = gray / 3;
chunk[2] = gray / 3;
}
}
}
EOF
WebAssembly Derleme ve Paketleme
Kodu yazdık, şimdi derleyelim:
# Release build ile derle - debug build çok yavaş olur
wasm-pack build --target web --release
# Çıktı pkg/ dizininde oluşur
ls -lh pkg/
# rustimg_wasm_bg.wasm -> Ana binary
# rustimg_wasm.js -> JS wrapper
# rustimg_wasm.d.ts -> TypeScript tanımları
# package.json
# Wasm boyutunu kontrol et
wc -c pkg/rustimg_wasm_bg.wasm
# Optimizasyon için wasm-opt kullan (binaryen paketinden)
apt-get install -y binaryen
wasm-opt -O3 -o pkg/rustimg_wasm_bg_opt.wasm pkg/rustimg_wasm_bg.wasm
wc -c pkg/rustimg_wasm_bg_opt.wasm
HTML ve JavaScript Entegrasyonu
Şimdi tarayıcı tarafını yazalım. Bu HTML dosyası webcam akışını alarak WebAssembly filtrelerini gerçek zamanlı uygular:
cat > index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>Rust WASM Görüntü İşleme</title>
<style>
body { font-family: monospace; background: #1a1a2e; color: #eee; }
.container { display: flex; gap: 20px; padding: 20px; }
canvas, video { border: 2px solid #0f3460; border-radius: 4px; }
.controls { display: flex; flex-direction: column; gap: 10px; }
button {
background: #0f3460; color: white;
border: none; padding: 10px 20px;
cursor: pointer; border-radius: 4px;
}
button:hover { background: #16213e; }
#fps { color: #e94560; font-weight: bold; }
#status { color: #4ade80; margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<div>
<p>Kaynak Video</p>
<video id="video" width="640" height="480" autoplay></video>
</div>
<div>
<p>İşlenmiş Görüntü | <span id="fps">0</span> FPS</p>
<canvas id="output" width="640" height="480"></canvas>
</div>
<div class="controls">
<p id="status">WASM Yükleniyor...</p>
<button onclick="setFilter('none')">Orijinal</button>
<button onclick="setFilter('grayscale')">Grayscale</button>
<button onclick="setFilter('sobel')">Kenar Algılama</button>
<button onclick="setFilter('blur')">Gaussian Blur</button>
<button onclick="setFilter('threshold')">Adaptive Threshold</button>
<button onclick="setFilter('color_mask')">Kırmızı Maske</button>
</div>
</div>
<script type="module">
import init, {
grayscale,
sobel_edge_detect,
gaussian_blur,
adaptive_threshold,
color_mask_hsv
} from './pkg/rustimg_wasm.js';
let wasmReady = false;
let currentFilter = 'none';
let frameCount = 0;
let lastTime = performance.now();
const video = document.getElementById('video');
const canvas = document.getElementById('output');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const status = document.getElementById('status');
const fpsDisplay = document.getElementById('fps');
window.setFilter = (f) => { currentFilter = f; };
async function start() {
await init();
wasmReady = true;
status.textContent = 'WASM Hazır';
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480, frameRate: 30 }
});
video.srcObject = stream;
const srcCanvas = document.createElement('canvas');
srcCanvas.width = 640;
srcCanvas.height = 480;
const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true });
function processFrame() {
srcCtx.drawImage(video, 0, 0, 640, 480);
const frame = srcCtx.getImageData(0, 0, 640, 480);
const data = frame.data;
if (currentFilter === 'grayscale') {
grayscale(data);
ctx.putImageData(frame, 0, 0);
} else if (currentFilter === 'sobel') {
grayscale(data);
const output = new Uint8ClampedArray(data.length);
sobel_edge_detect(data, output, 640, 480);
ctx.putImageData(new ImageData(output, 640, 480), 0, 0);
} else if (currentFilter === 'blur') {
const output = new Uint8ClampedArray(data.length);
gaussian_blur(data, output, 640, 480, 5);
ctx.putImageData(new ImageData(output, 640, 480), 0, 0);
} else if (currentFilter === 'threshold') {
adaptive_threshold(data, 640, 480, 15, 5);
ctx.putImageData(frame, 0, 0);
} else if (currentFilter === 'color_mask') {
// Kırmızı renk tespiti: H 340-360 veya 0-20
color_mask_hsv(data, 0, 20, 0.4, 0.3);
ctx.putImageData(frame, 0, 0);
} else {
ctx.putImageData(frame, 0, 0);
}
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
fpsDisplay.textContent = frameCount;
frameCount = 0;
lastTime = now;
}
requestAnimationFrame(processFrame);
}
video.onloadedmetadata = () => processFrame();
}
start().catch(e => {
status.textContent = 'Hata: ' + e.message;
console.error(e);
});
</script>
</body>
</html>
HTMLEOF
# Basit HTTP server ile test et
python3 -m http.server 8080
Web Worker ile Parallel İşleme
Görüntü işleme ana thread’i bloke etmemeli. Özellikle karmaşık filtreler için Web Worker kullanmak şarttır:
cat > worker.js << 'EOF'
import init, { sobel_edge_detect, gaussian_blur } from './pkg/rustimg_wasm.js';
let wasmInitialized = false;
async function initWasm() {
if (!wasmInitialized) {
await init();
wasmInitialized = true;
}
}
self.onmessage = async function(e) {
await initWasm();
const { imageData, filter, width, height, transferId } = e.data;
const data = new Uint8ClampedArray(imageData);
const output = new Uint8ClampedArray(data.length);
const start = performance.now();
if (filter === 'sobel') {
sobel_edge_detect(data, output, width, height);
} else if (filter === 'blur') {
gaussian_blur(data, output, width, height, 7);
}
const elapsed = performance.now() - start;
// Transferable objects ile sıfır kopya transfer
self.postMessage({
output: output.buffer,
elapsed,
transferId
}, [output.buffer]);
};
EOF
Production Ortamı ve CDN Yapılandırması
Gerçek bir production deploy senaryosunda, WebAssembly dosyaları için doğru MIME type ve HTTP header’ları kritik önem taşır:
# Nginx konfigürasyonu
cat > /etc/nginx/sites-available/wasm-app << 'EOF'
server {
listen 443 ssl http2;
server_name imgprocess.example.com;
root /var/www/wasm-app;
index index.html;
# WASM MIME type
types {
application/wasm wasm;
}
# SharedArrayBuffer için zorunlu headerlar
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Opener-Policy "same-origin";
# WASM dosyaları için agresif cache
location ~* .wasm$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Opener-Policy "same-origin";
gzip_static on;
}
# Brotli compression wasm icin daha iyi
location ~* .(js|wasm)$ {
brotli_static on;
gzip_static on;
}
ssl_certificate /etc/letsencrypt/live/imgprocess.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/imgprocess.example.com/privkey.pem;
}
EOF
nginx -t && systemctl reload nginx
# WASM dosyasını brotli ile sıkıştır
brotli -q 11 pkg/rustimg_wasm_bg_opt.wasm
ls -lh pkg/rustimg_wasm_bg_opt.wasm*
Cross-Origin-Embedder-Policy ve Cross-Origin-Opener-Policy header’ları olmadan SharedArrayBuffer çalışmaz. Bu header’ları eklemeyi unutursanız Web Worker’a Transferable buffer gönderirken sorun yaşarsınız. Production’da bunu ilk kez kurarken bu detay saatler aldı.
Performans Profiling ve Kıyaslama
Her şeyi kurup çalıştırdıktan sonra, JavaScript ile kıyaslamanın vakti gelir:
# Benchmark scripti
cat > bench.js << 'EOF'
import init, { sobel_edge_detect, grayscale } from './pkg/rustimg_wasm.js';
await init();
// 1080p kare simule et
const width = 1920;
const height = 1080;
const size = width * height * 4;
const testData = new Uint8Array(size).fill(128);
const output = new Uint8Array(size);
// WASM benchmark
console.time('wasm-grayscale-1000x');
for (let i = 0; i < 1000; i++) {
grayscale(testData);
}
console.timeEnd('wasm-grayscale-1000x');
// JavaScript referans implementasyon
function jsGrayscale(data) {
for (let i = 0; i < data.length; i += 4) {
const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
data[i] = data[i+1] = data[i+2] = gray;
}
}
console.time('js-grayscale-1000x');
for (let i = 0; i < 1000; i++) {
jsGrayscale(testData);
}
console.timeEnd('js-grayscale-1000x');
EOF
Testlerimizde 1080p grayscale dönüşümünde WASM, JavaScript’e göre ortalama 3.2x ile 4.8x arasında hızlı çalıştı. Sobel kenar algılama gibi iç içe döngü ağırlıklı işlemlerde bu fark 6x ile 8x‘e kadar çıktı. Özellikle barrel optimizasyon (loop unrolling) Rust derleyicisinin otomatik olarak uyguladığı ve JavaScript motorlarının her zaman yapamadığı bir optimizasyondur.
Gerçek Dünya Senaryosu: Fabrika Kalite Kontrol Sistemi
Bu altyapıyı bir üretim tesisinde nasıl kullandığımızı anlatayım. Konveyör bant üzerindeki ürünlerin renk tutarlılığını kontrol etmek için bir web uygulaması geliştirdik. Operatörler, fabrika bilgisayarlarında herhangi bir kurulum yapmadan tarayıcıda kameraya erişip görüntüleri analiz edebiliyordu.
Kritik gereksinimler şunlardı:
- Saniyede 25 kare işleme kapasitesi (bant hızına göre)
- 16ms altında gecikme (bir frame time)
- Düşük güçlü endüstriyel PC’lerde çalışabilme (Intel Celeron J4125)
- Sıfır kurulum, tarayıcı dışında bağımlılık yok
JavaScript ile ilk prototipte Sobel + renk maskesi kombinasyonu 28-35ms alıyordu, bu da frame drop’a yol açıyordu. WASM versiyonunda aynı işlem Celeron’da bile 8-11ms’ye düştü. Edge compute senaryosunda, ağ gecikmesi olmadan tarayıcı içinde bu işi yapabilmek operasyonel açıdan büyük fark yarattı.
Hata Ayıklama İpuçları
WebAssembly geliştirirken karşılaştığınız hatalar bazen kafa karıştırıcı olabilir:
- Wasm dosyası yüklenmiyor: MIME type kontrolü yapın, sunucunuzun
application/wasmdöndürdüğünden emin olun - SharedArrayBuffer tanımsız hatası: COOP/COEP header’larını kontrol edin
- Bellek hatası (out of bounds): Rust tarafında slice boyutunu JavaScript’ten iletirken tip uyumsuzluğu olabilir,
u32vsusizedönüşümlerine dikkat edin - Performans beklenenden düşük:
--releaseflag olmadan build aldığınızı kontrol edin, debug WASM 10-20x yavaş olabilir - wasm-pack build hatası:
wasm-bindgenversiyonuCargo.tomlve kurulu CLI arasında uyumsuz olabilir, her ikisini de güncelleyin
# Hata ayıklama için semboller ile build
wasm-pack build --target web --dev
# WASM binary içeriğini incele
wasm-objdump -x pkg/rustimg_wasm_bg.wasm | head -50
# Fonksiyon export listesi
wasm-objdump -j Export pkg/rustimg_wasm_bg.wasm
Sonuç
Rust ve WebAssembly kombinasyonu, tarayıcıda gerçek zamanlı görüntü işleme için artık production hazır bir seçenek. Native uygulama ya da sunucu taraflı işleme gerektiren senaryoları, sıfır kurulum ile tarayıcıya taşıyabiliyorsunuz. Bu, özellikle endüstriyel ortamlar, medikal sistemler veya dağıtık kamera ağları gibi kurulum ve bakım maliyetinin yüksek olduğu alanlarda ciddi operasyonel avantaj sağlıyor.
Rust öğrenme eğrisi başlangıçta dik görünebilir, özellikle borrow checker ile ilk tanışmada. Ama bir kez alıştığınızda, derleme zamanında garantilenen bellek güvenliğinin WebAssembly gibi düşük seviye bir ortamda ne kadar değerli olduğunu anlıyorsunuz. Segfault yok, dangling pointer yok, beklenmedik bellek sızıntısı yok.
Projenizi küçük başlatın. Grayscale gibi basit bir fonksiyonla başlayıp pipeline’ı anladıktan sonra karmaşık filtrelere geçin. Benchmark’larınızı erken ve sık yapın, bazen beklediğiniz yer değil, bambaşka bir yer bottleneck çıkabilir. Ve her zaman production’da Nginx COOP/COEP header’larını doğru yapılandırdığınızdan emin olun; bu adımı atlarsanız Web Worker entegrasyonunda saatler kaybedebilirsiniz.
