492 lines
21 KiB
Plaintext
492 lines
21 KiB
Plaintext
// OCR: the text in images, with where each line is. A batch of RGBA images goes in; each comes back
|
|
// as its text lines, with a box and a confidence for each.
|
|
//
|
|
// engine, ok, error := ocr_create(models_folder, "ppocr-v5-mobile");
|
|
// results := ocr_process(engine, images);
|
|
//
|
|
// The models are listed in models_folder/models.config (fetch_models.jai downloads them) and run
|
|
// with onnxruntime (modules/onnxruntime), on an NVIDIA GPU through CUDA when it can, else on the
|
|
// CPU. ocr_gpu_status says why the GPU is not in use, and what to install so it can be.
|
|
//
|
|
// It runs PP-OCRv5's pipeline: detection (detect.jai) finds each line as a rectangle at any angle,
|
|
// then recognition (recognize.jai) reads it level. When the model lists an orientation classifier,
|
|
// lines that look upside down are read both ways; lines read downwards or upwards are read too.
|
|
|
|
Ocr_Provider :: enum { CPU; CUDA; COREML; }
|
|
|
|
// Tightly packed or not: stride is the bytes from one row to the next.
|
|
Ocr_Image :: struct {
|
|
pixels: *u8; // RGBA
|
|
width, height, stride: s32;
|
|
}
|
|
|
|
Ocr_Line :: struct {
|
|
x, y, width, height: s32; // in the image: the bounds of corners
|
|
text: string;
|
|
confidence: float; // 0 to 1: the mean probability of its characters
|
|
corners: [4] Vector2; // in the image's pixels: the text's top left, top right, bottom right, bottom left
|
|
}
|
|
|
|
Ocr_Result :: struct {
|
|
lines: [] Ocr_Line; // top to bottom, then left to right
|
|
failed: bool;
|
|
error: string;
|
|
}
|
|
|
|
Ocr_Engine :: struct {
|
|
model: Ocr_Model_Info;
|
|
provider: Ocr_Provider;
|
|
gpu_explanation: string; // why the GPU is not in use; "" when it is
|
|
env: *OrtEnv;
|
|
memory_info: *OrtMemoryInfo;
|
|
det, rec: Ocr_Session;
|
|
#if OS == .MACOS det_coreml, rec_coreml: *Coreml_Model; // the detector and recognizer, when Core ML runs them
|
|
orientation: Ocr_Session; // the text line orientation classifier; none when its session is null
|
|
orientation_width, orientation_height: s32; // its input size
|
|
dictionary: [] string; // class n is dictionary[n - 1]
|
|
workers: s32; // threads ocr_process uses
|
|
rec_batch: s32; // text lines per recognizer run: the model's rec_batch_gpu or rec_batch_cpu
|
|
options: Ocr_Options;
|
|
stats: Ocr_Stats; // since creation, or the last ocr_reset_stats
|
|
pool: Ocr_Pool; // the worker threads, for the engine's life (run_on_workers)
|
|
|
|
// When set, and true, ocr_process stops starting work: the batch of lines or image each worker is
|
|
// on is finished, and then every image fails as "cancelled" (none is part read). Set by the caller.
|
|
cancel: *bool;
|
|
}
|
|
|
|
OCR_CANCELLED :: "cancelled";
|
|
|
|
ocr_cancelled :: (engine: *Ocr_Engine) -> bool {
|
|
return engine.cancel && engine.cancel.*;
|
|
}
|
|
|
|
// Time spent in each stage, summed over the worker threads (so it can exceed the elapsed time).
|
|
Ocr_Stats :: struct {
|
|
images: s64;
|
|
lines: s64;
|
|
detect_us: s64; // preparing images, the detector, and finding the boxes
|
|
recognize_us: s64; // cropping lines, the recognizer, and decoding the text
|
|
orientation_us: s64; // cropping lines and the orientation classifier
|
|
turned: s64; // lines that read better the other way up, or level as found
|
|
}
|
|
|
|
ocr_stats :: (engine: *Ocr_Engine) -> Ocr_Stats { return engine.stats; }
|
|
ocr_reset_stats :: (engine: *Ocr_Engine) { engine.stats = .{}; }
|
|
|
|
// How an engine runs on the GPU, beyond its model's models.config entry. Nearly every image and
|
|
// batch is a different size. onnxruntime's arena keeps each block it takes from CUDA and never joins
|
|
// or frees them, so inputs that grow one run after another leave a block of every size behind: the
|
|
// runs go largest first (ocr_process) so later ones fit in what the first took.
|
|
Ocr_Options :: struct {
|
|
gpu_workers: s32 = 4; // threads preparing images and decoding results while the GPU runs the networks
|
|
cuda_arena_extend_strategy := "kSameAsRequested"; // how much the arena takes when it runs short: what is asked; "kNextPowerOfTwo" takes twice as much each time, up to twice what the runs need
|
|
cudnn_max_workspace := false; // let cuDNN take as much working memory as it finds best (onnxruntime's own default)
|
|
rec_width_step: s32 = 64; // a recognizer batch's width is rounded up to a multiple of this, so shapes recur
|
|
}
|
|
|
|
// An engine for the model named in models_folder/models.config. With prefer_gpu it runs on CUDA when
|
|
// every library that needs is present; otherwise, or when CUDA fails to start, on the CPU.
|
|
ocr_create :: (models_folder: string, model_name: string, prefer_gpu := true, options := Ocr_Options.{}) -> *Ocr_Engine, ok: bool, error: string {
|
|
push_allocator(context.default_allocator);
|
|
|
|
loaded, load_error := ort_load();
|
|
if !loaded return null, false, load_error;
|
|
|
|
models, models_error := ocr_list_models(models_folder);
|
|
if models_error return null, false, models_error;
|
|
model: *Ocr_Model_Info;
|
|
for * models if it.name == model_name { model = it; break; }
|
|
if !model return null, false, tprint("% has no [model %].", OCR_MODELS_CONFIG, model_name);
|
|
|
|
engine := New(Ocr_Engine);
|
|
engine.model = model.*;
|
|
engine.options = options;
|
|
|
|
dictionary_text, dictionary_ok := read_entire_file(tprint("%/%", models_folder, model.dictionary), log_errors = false);
|
|
if !dictionary_ok { ocr_destroy(engine); return null, false, tprint("Cannot read the dictionary %/%.", models_folder, model.dictionary); }
|
|
entries := split(dictionary_text, "\n");
|
|
for * entries if it.count && it.*[it.count - 1] == #char "\r" it.count -= 1;
|
|
while entries.count && !entries[entries.count - 1].count entries.count -= 1; // the final newline
|
|
engine.dictionary = entries;
|
|
|
|
api := ort.api;
|
|
ok, error := ort_check(api.CreateEnv(ort.log_level, "ocr", *engine.env));
|
|
if !ok { ocr_destroy(engine); return null, false, error; }
|
|
ort_check(api.CreateCpuMemoryInfo(.OrtArenaAllocator, .OrtMemTypeDefault, *engine.memory_info));
|
|
|
|
cores := cast(s32) max(get_number_of_processors(.ALL_LOGICAL), 1); // efficiency cores included
|
|
|
|
use_gpu := false;
|
|
#if OS == .MACOS if prefer_gpu {
|
|
// Core ML on the GPU: the detector and recognizer, the orientation classifier on the CPU. On any
|
|
// failure, onnxruntime on the CPU below, with the reason.
|
|
coreml_ok, coreml_error := ocr_create_coreml(engine, models_folder, cores);
|
|
if coreml_ok {
|
|
engine.provider = .COREML;
|
|
engine.workers = max(options.gpu_workers, 1);
|
|
engine.rec_batch = max(engine.model.coreml_rec_batch, 1);
|
|
return engine, true, "";
|
|
}
|
|
ocr_release_coreml(engine);
|
|
engine.gpu_explanation = sprint("OCR runs on the CPU: %", coreml_error);
|
|
}
|
|
if prefer_gpu && OS != .MACOS {
|
|
available, explanation := ort_cuda_check("OCR"); // modules/onnxruntime/cuda.jai
|
|
use_gpu = available;
|
|
if !available engine.gpu_explanation = copy_string(explanation);
|
|
} else if !prefer_gpu {
|
|
engine.gpu_explanation = copy_string("OCR runs on the CPU, as it was asked to.");
|
|
}
|
|
|
|
if use_gpu {
|
|
// A few CPU threads prepare images and decode results while the GPU runs the networks.
|
|
engine.workers = max(options.gpu_workers, 1);
|
|
ort_gpu_lock(); // one CUDA engine made or released at a time (modules/onnxruntime/session.jai)
|
|
sessions_ok, sessions_error := ocr_create_sessions(engine, models_folder, true, 2);
|
|
if sessions_ok sessions_ok, sessions_error = ocr_warm_up(engine);
|
|
if !sessions_ok ocr_release_sessions(engine);
|
|
ort_gpu_unlock();
|
|
if sessions_ok {
|
|
engine.provider = .CUDA;
|
|
engine.rec_batch = max(engine.model.rec_batch_gpu, 1);
|
|
} else {
|
|
engine.gpu_explanation = sprint("OCR runs on the CPU: the CUDA libraries were found, but onnxruntime could not start on the GPU:\n%", sessions_error);
|
|
use_gpu = false;
|
|
}
|
|
}
|
|
if !use_gpu {
|
|
// Several images at once, each network run spread over the rest of the cores.
|
|
engine.workers = clamp(cores / 4, 1, 8);
|
|
threads := max(cores / engine.workers, 1);
|
|
sessions_ok, sessions_error := ocr_create_sessions(engine, models_folder, false, threads);
|
|
if !sessions_ok { ocr_destroy(engine); return null, false, sessions_error; }
|
|
engine.provider = .CPU;
|
|
engine.rec_batch = max(engine.model.rec_batch_cpu, 1);
|
|
}
|
|
ocr_pool_start(engine);
|
|
return engine, true, "";
|
|
}
|
|
|
|
ocr_provider :: (engine: *Ocr_Engine) -> Ocr_Provider {
|
|
return engine.provider;
|
|
}
|
|
|
|
// Whether the GPU is in use, and when not, why, and what to install so it can be.
|
|
ocr_gpu_status :: (engine: *Ocr_Engine) -> in_use: bool, explanation: string {
|
|
return engine.provider == .CUDA || engine.provider == .COREML, engine.gpu_explanation;
|
|
}
|
|
|
|
// What runs the detector and recognizer, for people and the files it reads: "CUDA", "Core ML" or "CPU".
|
|
ocr_provider_name :: (engine: *Ocr_Engine) -> string {
|
|
if engine.provider == {
|
|
case .CUDA; return "CUDA";
|
|
case .COREML; return "Core ML";
|
|
}
|
|
return "CPU";
|
|
}
|
|
|
|
ocr_destroy :: (engine: *Ocr_Engine) {
|
|
if !engine return;
|
|
ocr_pool_stop(engine);
|
|
if engine.provider == .CUDA ort_gpu_lock();
|
|
ocr_release_sessions(engine);
|
|
if engine.provider == .CUDA ort_gpu_unlock();
|
|
ocr_release_coreml(engine);
|
|
if engine.memory_info ort.api.ReleaseMemoryInfo(engine.memory_info);
|
|
if engine.env ort.api.ReleaseEnv(engine.env);
|
|
free(engine.gpu_explanation,, context.default_allocator);
|
|
free(engine,, context.default_allocator); // the dictionary and model strings are kept: small, and shared with ocr_list_models' caller
|
|
}
|
|
|
|
// The text in each image. Results (lines and their text) come from allocator. An image that fails
|
|
// is marked failed with the reason; the others are unaffected.
|
|
ocr_process :: (engine: *Ocr_Engine, images: [] Ocr_Image, allocator := context.allocator) -> [] Ocr_Result {
|
|
results := NewArray(images.count, Ocr_Result,, allocator);
|
|
if !images.count return results;
|
|
|
|
// Detection: images shared out among the workers.
|
|
per_image := NewArray(images.count, Image_Work,, context.default_allocator);
|
|
defer {
|
|
for * per_image array_free(it.boxes);
|
|
array_free(per_image,, context.default_allocator);
|
|
}
|
|
for * per_image it.image = *images[it_index];
|
|
// The largest first: the detector's arena then holds a block that fits every later image
|
|
// (Ocr_Options). Smallest first, it took a new one for each larger image and kept them all.
|
|
detect_order := NewArray(per_image.count, *Image_Work,, context.default_allocator);
|
|
defer array_free(detect_order,, context.default_allocator);
|
|
for * per_image detect_order[it_index] = it;
|
|
quick_sort(detect_order, (a: *Image_Work, b: *Image_Work) -> s64 {
|
|
return cast(s64) b.image.width * b.image.height - cast(s64) a.image.width * a.image.height;
|
|
});
|
|
run_on_workers(engine, detect_order.count, (engine: *Ocr_Engine, index: s64, data: *void) {
|
|
work := (cast(**Image_Work) data)[index];
|
|
start := current_time_monotonic();
|
|
work.boxes, work.ok, work.error = ocr_detect(engine, work.image.*);
|
|
atomic_add(*engine.stats.detect_us, to_microseconds(current_time_monotonic() - start));
|
|
if work.error work.error = copy_string(work.error,, context.default_allocator);
|
|
}, detect_order.data);
|
|
ocr_shrink_arena(engine, *engine.det); // before recognition, so the two stages' blocks are not held at once
|
|
if ocr_cancelled(engine) return ocr_all_cancelled(results, allocator);
|
|
|
|
// Recognition: every line of every image, batched by width so little of each batch is padding.
|
|
jobs: [..] Line_Job;
|
|
jobs.allocator = context.default_allocator;
|
|
defer {
|
|
for jobs free(it.text,, context.default_allocator);
|
|
array_free(jobs);
|
|
}
|
|
rec_height := engine.model.rec_height;
|
|
turned: [..] bool; // per first job: whether detection turned its box a quarter
|
|
turned.allocator = context.default_allocator;
|
|
defer array_free(turned);
|
|
for work: per_image {
|
|
if !work.ok continue;
|
|
for work.boxes {
|
|
array_add(*jobs, .{image = work.image, corners = it.corners, width = ocr_line_width(it.corners, rec_height)});
|
|
array_add(*turned, it.turned);
|
|
}
|
|
}
|
|
|
|
// Other ways to read a line, recognized beside it, the more confident reading kept: upside down,
|
|
// where the orientation classifier finds it more likely than not; and level as it was found, for a
|
|
// box detection turned a quarter, since a narrow word such as "1" is as tall as a line read downwards.
|
|
first_count := jobs.count;
|
|
upside_down := NewArray(first_count, bool,, context.default_allocator);
|
|
defer array_free(upside_down,, context.default_allocator);
|
|
Orientation_Context :: struct { jobs: [] Line_Job; upside_down: [] bool; size: s64; failed: bool; error: string; }
|
|
classify := Orientation_Context.{jobs = jobs, upside_down = upside_down, size = max(engine.rec_batch, 1)};
|
|
if engine.orientation.session && first_count {
|
|
run_on_workers(engine, (first_count + classify.size - 1) / classify.size, (engine: *Ocr_Engine, index: s64, data: *void) {
|
|
c := cast(*Orientation_Context) data;
|
|
first := index * c.size;
|
|
count := min(c.size, c.jobs.count - first);
|
|
batch := NewArray(count, *Line_Job,, temp);
|
|
for * batch it.* = *c.jobs[first + it_index];
|
|
start := current_time_monotonic();
|
|
ok, error := ocr_classify_batch(engine, batch, array_view(c.upside_down, first, count));
|
|
atomic_add(*engine.stats.orientation_us, to_microseconds(current_time_monotonic() - start));
|
|
if !ok { c.failed = true; c.error = copy_string(error,, context.default_allocator); } // the last error wins
|
|
}, *classify);
|
|
}
|
|
defer free(classify.error,, context.default_allocator);
|
|
for n: 0..first_count - 1 {
|
|
job := jobs[n];
|
|
if upside_down[n] array_add(*jobs, .{image = job.image, corners = ocr_corners_turned(job.corners, 2), width = job.width, primary = n});
|
|
if turned[n] {
|
|
level := ocr_corners_turned(job.corners, 3);
|
|
array_add(*jobs, .{image = job.image, corners = level, width = ocr_line_width(level, rec_height), primary = n});
|
|
}
|
|
}
|
|
|
|
order := NewArray(jobs.count, *Line_Job,, context.default_allocator);
|
|
defer array_free(order,, context.default_allocator);
|
|
for * jobs order[it_index] = it;
|
|
// Widest first, so each batch fits in the arena block the first one took (Ocr_Options). Narrowest
|
|
// first, every batch was wider than the last and took a new block: 5.7 GB of them for the mobile
|
|
// model on gpu-bench's 30 images, where the widest batch needs 0.9 GB.
|
|
quick_sort(order, (a: *Line_Job, b: *Line_Job) -> s64 { return b.width - a.width; });
|
|
|
|
batch_size := max(engine.rec_batch, 1);
|
|
batch_count := (order.count + batch_size - 1) / batch_size;
|
|
Batch_Context :: struct { order: [] *Line_Job; size: s64; failed: bool; error: string; }
|
|
batches := Batch_Context.{order = order, size = batch_size};
|
|
run_on_workers(engine, batch_count, (engine: *Ocr_Engine, index: s64, data: *void) {
|
|
b := cast(*Batch_Context) data;
|
|
first := index * b.size;
|
|
count := min(b.size, b.order.count - first);
|
|
start := current_time_monotonic();
|
|
ok, error := ocr_recognize_batch(engine, array_view(b.order, first, count));
|
|
atomic_add(*engine.stats.recognize_us, to_microseconds(current_time_monotonic() - start));
|
|
if !ok { b.failed = true; b.error = copy_string(error,, context.default_allocator); } // the last error wins; any is enough to say why
|
|
}, *batches);
|
|
if ocr_cancelled(engine) {
|
|
free(batches.error,, context.default_allocator);
|
|
return ocr_all_cancelled(results, allocator);
|
|
}
|
|
if classify.failed && !batches.failed {
|
|
batches.failed = true;
|
|
batches.error = copy_string(classify.error,, context.default_allocator);
|
|
}
|
|
|
|
// The more confident of each line's readings takes the first job's place.
|
|
for * other: array_view(jobs, first_count, jobs.count - first_count) {
|
|
first := *jobs[other.primary];
|
|
if other.confidence > first.confidence {
|
|
swap(*first.text, *other.text);
|
|
first.corners, first.confidence = other.corners, other.confidence;
|
|
engine.stats.turned += 1;
|
|
}
|
|
}
|
|
|
|
ocr_shrink_arena(engine, *engine.rec); // models.jai: what this call's runs took, given back
|
|
|
|
// Each image's lines, in reading order.
|
|
for work, image_index: per_image {
|
|
result := *results[image_index];
|
|
if !work.ok {
|
|
result.failed = true;
|
|
result.error = copy_string(work.error,, allocator);
|
|
free(work.error,, context.default_allocator);
|
|
continue;
|
|
}
|
|
if batches.failed {
|
|
result.failed = true;
|
|
result.error = copy_string(batches.error,, allocator);
|
|
continue;
|
|
}
|
|
lines: [..] Ocr_Line;
|
|
lines.allocator = allocator;
|
|
for job: array_view(jobs, 0, first_count) {
|
|
if job.image != work.image || !job.text.count continue;
|
|
x0, y0, x1, y1 := ocr_corner_bounds(job.corners, job.image.width, job.image.height);
|
|
array_add(*lines, .{x = x0, y = y0, width = x1 - x0, height = y1 - y0, text = copy_string(job.text,, allocator),
|
|
confidence = job.confidence, corners = job.corners});
|
|
}
|
|
// Rows by the middle of each line, then left to right within a row.
|
|
quick_sort(lines, (a: Ocr_Line, b: Ocr_Line) -> s64 {
|
|
ay, by := a.y + a.height / 2, b.y + b.height / 2;
|
|
if abs(ay - by) > min(a.height, b.height) / 2 return ifx ay < by then -1 else 1;
|
|
return a.x - b.x;
|
|
});
|
|
result.lines = lines;
|
|
engine.stats.lines += lines.count;
|
|
}
|
|
engine.stats.images += images.count;
|
|
free(batches.error,, context.default_allocator);
|
|
return results;
|
|
}
|
|
|
|
#scope_module
|
|
|
|
ocr_all_cancelled :: (results: [] Ocr_Result, allocator: Allocator) -> [] Ocr_Result {
|
|
for * results {
|
|
it.failed = true;
|
|
it.error = copy_string(OCR_CANCELLED,, allocator);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
Image_Work :: struct {
|
|
image: *Ocr_Image;
|
|
boxes: [..] Detected_Box;
|
|
ok: bool;
|
|
error: string;
|
|
}
|
|
|
|
Work_Proc :: #type (engine: *Ocr_Engine, index: s64, data: *void);
|
|
|
|
// Calls proc for each index below count, on engine.workers threads at once.
|
|
// The engine's worker threads: started with it and kept until ocr_destroy, never one set per call.
|
|
// onnxruntime (1.26 to 1.30 at least) sets up a CUDA stream with cuBLAS and cuDNN handles for every
|
|
// OS thread that runs a session, about 10 MB each, kept after the thread ends (microsoft/onnxruntime
|
|
// #33037). Workers made afresh for every stage of every call, as they were, gave the OCR sessions
|
|
// thousands of threads in a scan, and starting them cost time: 6% on gpu-bench's mobile OCR.
|
|
Ocr_Pool :: struct {
|
|
threads: [] Thread;
|
|
started: s64;
|
|
mutex: Mutex; // one run_on_workers at a time
|
|
wake: Semaphore; // signalled once per worker for each job, and for quitting
|
|
done: Semaphore; // each worker signals it when it has finished its part of a job
|
|
quit: bool;
|
|
// The job: items 0..count-1, taken in turn.
|
|
count, next: s64;
|
|
proc: Work_Proc;
|
|
data: *void;
|
|
}
|
|
|
|
ocr_pool_start :: (engine: *Ocr_Engine) {
|
|
pool := *engine.pool;
|
|
init(*pool.mutex);
|
|
init(*pool.wake);
|
|
init(*pool.done);
|
|
// thread_init allocates the thread's temporary storage from the context allocator.
|
|
push_allocator(context.default_allocator);
|
|
pool.threads = NewArray(max(engine.workers, 1), Thread);
|
|
for * pool.threads {
|
|
if !thread_init(it, ocr_pool_worker, temporary_storage_size = 1024 * 1024) break;
|
|
thread_start(it, engine); // sets thread.data
|
|
pool.started += 1;
|
|
}
|
|
}
|
|
|
|
ocr_pool_stop :: (engine: *Ocr_Engine) {
|
|
pool := *engine.pool;
|
|
if !pool.threads.count return;
|
|
pool.quit = true;
|
|
for 1..pool.started signal(*pool.wake);
|
|
for 0..pool.started - 1 {
|
|
thread_is_done(*pool.threads[it], -1);
|
|
thread_deinit(*pool.threads[it]);
|
|
}
|
|
array_free(pool.threads,, context.default_allocator);
|
|
destroy(*pool.wake);
|
|
destroy(*pool.done);
|
|
destroy(*pool.mutex);
|
|
pool.* = .{};
|
|
}
|
|
|
|
ocr_pool_worker :: (thread: *Thread) -> s64 {
|
|
// Workers allocate from the heap, and use their own temporary storage.
|
|
context.allocator = context.default_allocator;
|
|
engine := cast(*Ocr_Engine) thread.data;
|
|
pool := *engine.pool;
|
|
while true {
|
|
Default_Allocator.thread_idle(); // parked until the next job: give back what this thread's heap holds free
|
|
wait_for(*pool.wake);
|
|
if pool.quit break;
|
|
while true {
|
|
if ocr_cancelled(engine) break; // what is left fails as cancelled (ocr_process)
|
|
index := atomic_add(*pool.next, 1);
|
|
if index >= pool.count break;
|
|
pool.proc(engine, index, pool.data);
|
|
reset_temporary_storage();
|
|
}
|
|
signal(*pool.done);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
run_on_workers :: (engine: *Ocr_Engine, count: s64, proc: Work_Proc, data: *void) {
|
|
if count <= 0 return;
|
|
pool := *engine.pool;
|
|
if !pool.started { // no threads: do it here
|
|
for 0..count - 1 {
|
|
if ocr_cancelled(engine) break;
|
|
proc(engine, it, data);
|
|
}
|
|
return;
|
|
}
|
|
lock(*pool.mutex);
|
|
defer unlock(*pool.mutex);
|
|
pool.count, pool.next, pool.proc, pool.data = count, 0, proc, data;
|
|
// Every worker is woken and reports back, even with fewer items than workers: a worker that finds
|
|
// nothing left reports at once, so no wake is left over for the next job.
|
|
for 1..pool.started signal(*pool.wake);
|
|
for 1..pool.started wait_for(*pool.done);
|
|
}
|
|
|
|
#load "models.jai";
|
|
#load "detect.jai";
|
|
#load "recognize.jai";
|
|
|
|
#import "Basic";
|
|
#import "String";
|
|
#import "File";
|
|
#import "File_Utilities";
|
|
#import "Math";
|
|
#import "Sort";
|
|
#import "System";
|
|
#import "Thread";
|
|
#import "Atomics";
|
|
#import "onnxruntime";
|
|
Default_Allocator :: #import "Default_Allocator"; // rpmalloc (modules/Default_Allocator), for thread_idle where the workers wait
|
|
#if OS == .MACOS #import "Core_ML";
|
|
#if OS == .WINDOWS {
|
|
#import "Windows";
|
|
#import "Windows_Utf8";
|
|
} else {
|
|
#import "POSIX";
|
|
}
|