OCR_DATASET_MAKER / web /lib /render-worker.ts
Omarrran's picture
Performance optimizations: GPU acceleration, parallel processing, pause/resume
599349f
// Web Worker for parallel OCR image generation
// This worker uses OffscreenCanvas for true multi-threaded rendering
interface RenderTask {
id: number
index: number
text: string
config: {
width: number
height: number
textColor: string
direction: string
backgroundStyle: string
backgroundColor: string
}
fontFamily: string
shouldAugment: boolean
augValues: Record<string, number>
seed: number
}
interface RenderResult {
id: number
index: number
filename: string
blob: Blob
label: string
fontName: string
augmentations: string[]
backgroundStyle: string
isAugmented: boolean
error?: string
}
// Seeded random for reproducibility
function seededRandom(seed: number) {
let s = seed
return function () {
s = Math.sin(s) * 10000
return s - Math.floor(s)
}
}
// Apply augmentations to OffscreenCanvas
function applyAugmentation(
ctx: OffscreenCanvasRenderingContext2D,
canvas: OffscreenCanvas,
augValues: Record<string, number>,
random: () => number
): string[] {
const applied: string[] = []
// Rotation
if (augValues.rotation && random() > 0.5) {
const angle = (random() - 0.5) * 2 * augValues.rotation * Math.PI / 180
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate(angle)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
applied.push('rotation')
}
// Skew
if (augValues.skew && random() > 0.5) {
const skewAmount = (random() - 0.5) * augValues.skew * 0.01
ctx.transform(1, skewAmount, 0, 1, 0, 0)
applied.push('skew')
}
return applied
}
// Render a single sample
async function renderSample(task: RenderTask): Promise<RenderResult> {
const random = seededRandom(task.seed + task.index * 1000)
try {
// Create OffscreenCanvas
const canvas = new OffscreenCanvas(task.config.width, task.config.height)
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('Could not get 2d context')
}
// Fill background
ctx.fillStyle = task.config.backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Apply augmentation transforms if enabled
let appliedAugmentations: string[] = []
if (task.shouldAugment) {
ctx.save()
appliedAugmentations = applyAugmentation(ctx, canvas, task.augValues, random)
}
// Set text properties
const fontSize = Math.min(canvas.height * 0.6, 48)
ctx.font = `${fontSize}px "${task.fontFamily}", Arial, sans-serif`
ctx.fillStyle = task.config.textColor
ctx.textAlign = task.config.direction === 'rtl' ? 'right' : 'left'
ctx.textBaseline = 'middle'
// Draw text
const x = task.config.direction === 'rtl' ? canvas.width - 10 : 10
const y = canvas.height / 2
ctx.direction = task.config.direction as CanvasDirection
ctx.fillText(task.text, x, y)
if (task.shouldAugment) {
ctx.restore()
}
// Post-processing augmentations
if (task.shouldAugment) {
// Brightness
if (task.augValues.brightness && random() > 0.5) {
const adjustment = 1 + (random() - 0.5) * task.augValues.brightness / 50
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i] = Math.min(255, imageData.data[i] * adjustment)
imageData.data[i + 1] = Math.min(255, imageData.data[i + 1] * adjustment)
imageData.data[i + 2] = Math.min(255, imageData.data[i + 2] * adjustment)
}
ctx.putImageData(imageData, 0, 0)
appliedAugmentations.push('brightness')
}
// Noise
if (task.augValues.gaussian_noise && random() > 0.6) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const noiseLevel = task.augValues.gaussian_noise / 2
for (let i = 0; i < imageData.data.length; i += 4) {
const noise = (random() - 0.5) * noiseLevel
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise))
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise))
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise))
}
ctx.putImageData(imageData, 0, 0)
appliedAugmentations.push('noise')
}
}
// Convert to blob
const blob = await canvas.convertToBlob({ type: 'image/png' })
const filename = `image_${String(task.index).padStart(6, '0')}.png`
return {
id: task.id,
index: task.index,
filename,
blob,
label: `${filename}\t${task.text}`,
fontName: task.fontFamily,
augmentations: appliedAugmentations,
backgroundStyle: task.config.backgroundStyle,
isAugmented: task.shouldAugment
}
} catch (error) {
return {
id: task.id,
index: task.index,
filename: '',
blob: new Blob(),
label: '',
fontName: '',
augmentations: [],
backgroundStyle: '',
isAugmented: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
// Handle messages from main thread
self.onmessage = async (e: MessageEvent) => {
const { type, tasks } = e.data
if (type === 'render') {
// Process all tasks in this batch
const results: RenderResult[] = []
for (const task of tasks as RenderTask[]) {
const result = await renderSample(task)
results.push(result)
// Send progress for each completed task
self.postMessage({ type: 'progress', result })
}
// Send completion signal
self.postMessage({ type: 'complete', results })
}
}
// Signal that worker is ready
self.postMessage({ type: 'ready' })