- Import brings in videos (MP4, MOV, MKV, WebM, AVI, WMV, MPEG, MTS/M2TS, FLV) on Windows, read with FFmpeg: the file as it was in the original, a frame a tenth of the way in (at most 3 s), turned the right way up, as the rendered image (what the grid, Capture window, OCR and labels see), and import.json with Duration, FrameRate, VideoCodec, Rotation, PosterTime, Audio, and the camera, date and location from the container. Elsewhere videos are skipped as "video (Windows only)". - .mcap version 4: the original's size is a u64, so a video can be any size. Versions 1-3 still read. mcap_read_without_original and mcap_write_streamed read and rewrite a file with its original left on disk; mcap_store, mcap_erase, the import index and the import itself never hold a video in memory. - Animated GIFs: Frames and Duration in import.json (gif_info). - Grid: a play mark and duration on video thumbnails, GIF on animations. Capture window: what the video is, and Play in the default player (written out to %TEMP%\mcap). - modules/ffmpeg: FFmpeg 6.0 bindings and the five DLLs used (avformat, avcodec, avutil, swscale, swresample), copied beside the executable by the build. swscale converts into a buffer of its own allocator with padded rows: an exact-sized one let its SIMD write past the end, crashing on frames of odd widths (1916, 1106 ...). - image-probe shows videos and GIF frame counts; ocr-bench reads a video's frame. - Tests: new video formats, display-matrix rotation, ISO 6709, video dates, GIF frames, and v4 serialize/parse, v3 compatibility, streamed writes, store/erase and extraction. SnagitConverter's copy of Capture_File needs syncing to read version 4 files.
178 lines
8.3 KiB
Plaintext
178 lines
8.3 KiB
Plaintext
// ocr-bench: runs OCR over a folder of .mcap captures with each model, on the GPU when it can and
|
|
// on the CPU, and reports the time per image and per stage. Each capture's lines are written to
|
|
// OUT/<model>-<provider>/<name>.txt, to read against the images.
|
|
//
|
|
// ocr-bench MCAP_FOLDER [-models a,b] [-cpu | -gpu-only] [-batch N] [-limit N] [-out FOLDER]
|
|
// [-workers N] [-rec-batch N] [-warmup N]
|
|
//
|
|
// -models the models to run (default: every model in ocr-models/models.config)
|
|
// -cpu the CPU only; by default the GPU too, when it is available
|
|
// -gpu-only the GPU only
|
|
// -workers on the GPU, threads preparing images and decoding results (default Ocr_Options')
|
|
// -rec-batch text lines per recognizer run, in place of models.config's
|
|
// -warmup untimed passes over every image before the timed one (default 0)
|
|
// -batch images per ocr_process call (default 8)
|
|
// -limit at most this many captures (default all)
|
|
// -out where the text goes (default ocr-out beside this program)
|
|
//
|
|
// The models are read from ocr-models beside this program, where the build copies them.
|
|
|
|
USAGE :: "Usage: ocr-bench MCAP_FOLDER [-models a,b] [-cpu | -gpu-only] [-batch N] [-limit N] [-out FOLDER] [-workers N] [-rec-batch N] [-warmup N]\n";
|
|
|
|
main :: () {
|
|
runtime_support_headless = true; // a command-line tool: a failure is reported on the console and exits with 1, no message box
|
|
args := get_command_line_arguments();
|
|
if args.count < 2 { print(USAGE); exit(2); }
|
|
folder := args[1];
|
|
model_names: [..] string;
|
|
cpu_only := false;
|
|
gpu_only := false;
|
|
options: Ocr_Options;
|
|
rec_batch: s32 = 0; // 0: as models.config says
|
|
warmup := 0;
|
|
batch := 8;
|
|
limit := S64_MAX_VALUE;
|
|
exe_folder := path_strip_filename(get_path_of_running_executable());
|
|
// Lasting copies: temporary storage is reset after every batch.
|
|
out_folder := sprint("%ocr-out", exe_folder);
|
|
i := 2;
|
|
while i < args.count {
|
|
arg := args[i];
|
|
value := ifx i + 1 < args.count then args[i + 1] else "";
|
|
if arg == {
|
|
case "-models"; for split(value, ",") array_add(*model_names, trim(it)); i += 2;
|
|
case "-cpu"; cpu_only = true; i += 1;
|
|
case "-gpu-only"; gpu_only = true; i += 1;
|
|
case "-workers"; options.gpu_workers = cast(s32) max(1, string_to_int(value)); i += 2;
|
|
case "-rec-batch"; rec_batch = cast(s32) max(1, string_to_int(value)); i += 2;
|
|
case "-warmup"; warmup = max(0, string_to_int(value)); i += 2;
|
|
case "-batch"; batch = max(1, string_to_int(value)); i += 2;
|
|
case "-limit"; limit = max(1, string_to_int(value)); i += 2;
|
|
case "-out"; out_folder = value; i += 2;
|
|
case; print("Unknown option %.\n%", arg, USAGE); exit(2);
|
|
}
|
|
}
|
|
models_folder := sprint("%ocr-models", exe_folder);
|
|
if !model_names.count {
|
|
models, error := ocr_list_models(models_folder);
|
|
if error { print("%\n", error); exit(1); }
|
|
for models array_add(*model_names, it.name);
|
|
}
|
|
|
|
// The captures, decoded once: the original image of each, as the camera took it; a video's frame.
|
|
Capture :: struct { name: string; pixels: *u8; width, height: s32; }
|
|
captures: [..] Capture;
|
|
decode_start := current_time_monotonic();
|
|
for file_list(folder, recursive = true) {
|
|
if captures.count >= limit break;
|
|
if !ends_with_nocase(it, ".mcap") continue;
|
|
m, original, parsed, error := mcap_read_without_original(it,, temp);
|
|
if !parsed { print("Skipping %: %\n", it, error); continue; }
|
|
head, head_ok := mcap_read_original(.{original.path, original.offset, min(original.size, 256)},, temp);
|
|
bytes := m.rendered;
|
|
if !head_ok || image_kind(head) != .VIDEO {
|
|
ok: bool;
|
|
bytes, ok = mcap_read_original(original,, temp);
|
|
if !ok { print("Skipping %: it cannot be read.\n", it); continue; }
|
|
}
|
|
image, decoded, decode_error := image_decode(bytes);
|
|
if !decoded { print("Skipping %: its image cannot be decoded (%).\n", it, decode_error); continue; }
|
|
array_add(*captures, .{copy_string(path_strip_extension(path_filename(it))), image.pixels, image.width, image.height});
|
|
reset_temporary_storage();
|
|
}
|
|
if !captures.count { print("No .mcap files in %.\n", folder); exit(1); }
|
|
quick_sort(captures, (a: Capture, b: Capture) -> s64 { return compare(a.name, b.name); });
|
|
megapixels: float64;
|
|
for captures megapixels += cast(float64) it.width * it.height / 1_000_000;
|
|
print("% captures (% megapixels) decoded in % s.\n\n", captures.count, FF(megapixels, 1), FF(seconds_since(decode_start), 2));
|
|
|
|
images := NewArray(captures.count, Ocr_Image);
|
|
for captures images[it_index] = .{it.pixels, it.width, it.height, it.width * 4};
|
|
|
|
for model_name: model_names {
|
|
for prefer_gpu: bool.[true, false] {
|
|
if prefer_gpu && cpu_only continue;
|
|
if !prefer_gpu && gpu_only continue;
|
|
create_start := current_time_monotonic();
|
|
engine, ok, error := ocr_create(models_folder, model_name, prefer_gpu, options);
|
|
if !ok { print("% : %\n\n", model_name, error); break; }
|
|
if rec_batch engine.rec_batch = rec_batch;
|
|
create_seconds := seconds_since(create_start);
|
|
in_use, explanation := ocr_gpu_status(engine);
|
|
provider := ocr_provider(engine);
|
|
if prefer_gpu && !in_use {
|
|
// Said once; the CPU run that follows is the same thing.
|
|
print("%\n\n", explanation);
|
|
ocr_destroy(engine);
|
|
continue;
|
|
}
|
|
print("== % on % (sessions created in % s, % worker threads, % lines per recognizer run)\n", model_name, provider, FF(create_seconds, 2), engine.workers, engine.rec_batch);
|
|
|
|
text_folder := sprint("%/%-%", out_folder, model_name, to_lower_copy(tprint("%", provider),, temp));
|
|
make_directory_if_it_does_not_exist(text_folder, recursive = true);
|
|
|
|
// The first image alone: the first run pays for setting up kernels and memory.
|
|
first_start := current_time_monotonic();
|
|
ocr_process(engine, array_view(images, 0, 1),, temp);
|
|
first_seconds := seconds_since(first_start);
|
|
// Untimed passes over everything, so the timed one does not pay for setting up each new shape.
|
|
for 1..warmup {
|
|
for start: 0..images.count - 1 {
|
|
if start % batch continue;
|
|
ocr_process(engine, array_view(images, start, min(batch, images.count - start)),, temp);
|
|
reset_temporary_storage();
|
|
}
|
|
}
|
|
ocr_reset_stats(engine);
|
|
|
|
start := current_time_monotonic();
|
|
lines, characters, failures: s64;
|
|
done := 0;
|
|
while done < images.count {
|
|
count := min(batch, images.count - done);
|
|
results := ocr_process(engine, array_view(images, done, count),, temp);
|
|
for result, n: results {
|
|
name := captures[done + n].name;
|
|
if result.failed { failures += 1; print(" % failed: %\n", name, result.error); continue; }
|
|
builder: String_Builder;
|
|
builder.allocator = temp;
|
|
for result.lines {
|
|
print_to_builder(*builder, "% % % % % %\n", it.x, it.y, it.width, it.height, FF(it.confidence, 3), it.text);
|
|
characters += it.text.count;
|
|
}
|
|
lines += result.lines.count;
|
|
write_entire_file(tprint("%/%.txt", text_folder, name), *builder);
|
|
}
|
|
done += count;
|
|
reset_temporary_storage();
|
|
}
|
|
seconds := seconds_since(start);
|
|
stats := ocr_stats(engine);
|
|
print(" first image alone: % s (warm-up)\n", FF(first_seconds, 3));
|
|
print(" all % images: % s, % ms per image, % megapixels/s, % failed\n", images.count, FF(seconds, 2), FF(1000 * seconds / images.count, 1), FF(megapixels / seconds, 2), failures);
|
|
print(" found: % lines, % bytes of text\n", lines, characters);
|
|
print(" stage time (summed over threads): detection % s, orientation % s, recognition % s\n", FF(stats.detect_us / 1_000_000.0, 2), FF(stats.orientation_us / 1_000_000.0, 2), FF(stats.recognize_us / 1_000_000.0, 2));
|
|
print(" lines read better turned: %\n", stats.turned);
|
|
print(" text in %\n\n", text_folder);
|
|
ocr_destroy(engine);
|
|
}
|
|
}
|
|
}
|
|
|
|
seconds_since :: (start: Apollo_Time) -> float64 {
|
|
return to_float64_seconds(current_time_monotonic() - start);
|
|
}
|
|
|
|
S64_MAX_VALUE :: 0x7FFF_FFFF_FFFF_FFFF;
|
|
|
|
#import "Basic";
|
|
#import "String";
|
|
#import "File";
|
|
#import "File_Utilities";
|
|
#import "Sort";
|
|
#import "System";
|
|
#import "stb_image";
|
|
#import "Capture_File";
|
|
#import "Image_Decode";
|
|
#import "OCR";
|