Packages

Idiomatic Elixir interface to the Tesseract OCR engine via a NIF, accepting Vix.Vips.Image structs, file paths, or in-memory image binaries.

Current section

Files

Jump to
image_ocr lib image ocr pool.ex
Raw

lib/image/ocr/pool.ex

defmodule Image.OCR.Pool do
@moduledoc """
A `NimblePool`-backed pool of `Image.OCR` instances for concurrent OCR.
Each pool worker owns one `Image.OCR` instance — and therefore one
`tesseract::TessBaseAPI*`. Because each instance is single-threaded, the
pool is the simplest way to get parallel recognition across many processes
without sharing state.
## Sizing
The default pool size is `System.schedulers_online()`, matching the number
of dirty-CPU schedulers Tesseract recognition runs on. Each worker holds
the loaded language model in memory (typically 2–50 MB depending on the
language and trained-data variant), so size the pool deliberately.
## Example
children = [
{Image.OCR.Pool, name: MyOcr, locale: "en", pool_size: 4}
]
Supervisor.start_link(children, strategy: :one_for_one)
{:ok, text} = Image.OCR.Pool.read_text(MyOcr, "page.png")
"""
@behaviour NimblePool
@default_checkout_timeout 30_000
@doc """
Starts a pool linked to the calling process.
### Arguments
* `options` is a keyword list. See the options below.
### Options
* `:name` is the registered name for the pool. Required.
* `:pool_size` is the number of worker processes (and therefore Tesseract
instances) to run. Defaults to `System.schedulers_online()`.
* `:lazy` controls lazy worker initialisation. Defaults to `false`.
* Remaining options are passed straight to `Image.OCR.new/1` (`:locale`,
`:datapath`, `:psm`, `:variables`).
### Returns
* `{:ok, pid}` on success.
* `{:error, reason}` on failure.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(options) do
{pool_size, options} = Keyword.pop(options, :pool_size, System.schedulers_online())
{lazy, options} = Keyword.pop(options, :lazy, false)
{name, options} = Keyword.pop(options, :name)
name || raise ArgumentError, "Image.OCR.Pool requires a :name option"
NimblePool.start_link(
worker: {__MODULE__, options},
pool_size: pool_size,
lazy: lazy,
name: name
)
end
@doc """
Standard supervisor child spec.
"""
@spec child_spec(keyword()) :: Supervisor.child_spec()
def child_spec(options) do
%{
id: Keyword.get(options, :name, __MODULE__),
start: {__MODULE__, :start_link, [options]},
type: :worker
}
end
@doc """
Recognises text in `input` using a worker checked out from `pool`.
### Arguments
* `pool` is the registered name of the pool.
* `input` is any value accepted by `Image.OCR.read_text/2`.
* `options` accepts `:timeout` (defaults to 30_000 ms).
### Returns
* `{:ok, text}` on success.
* `{:error, reason}` on failure.
"""
@spec read_text(NimblePool.pool(), Image.OCR.Input.t(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def read_text(pool, input, options \\ []) do
timeout = Keyword.get(options, :timeout, @default_checkout_timeout)
NimblePool.checkout!(
pool,
:checkout,
fn _from, instance ->
{Image.OCR.read_text(instance, input), :ok}
end,
timeout
)
end
@doc """
Recognises `input` and returns per-word results. See `Image.OCR.recognize/3`.
"""
@spec recognize(NimblePool.pool(), Image.OCR.Input.t(), keyword()) ::
{:ok, [Image.OCR.word_result()]} | {:error, term()}
def recognize(pool, input, options \\ []) do
timeout = Keyword.get(options, :timeout, @default_checkout_timeout)
NimblePool.checkout!(
pool,
:checkout,
fn _from, instance ->
{Image.OCR.recognize(instance, input), :ok}
end,
timeout
)
end
# NimblePool callbacks
@impl NimblePool
def init_pool(options), do: {:ok, options}
@impl NimblePool
def init_worker(options) do
case Image.OCR.new(options) do
{:ok, instance} -> {:ok, instance, options}
{:error, reason} -> {:stop, {:image_ocr_init_failed, reason}}
end
end
@impl NimblePool
def handle_checkout(:checkout, _from, instance, options) do
{:ok, instance, instance, options}
end
@impl NimblePool
def handle_checkin(:ok, _from, instance, options) do
{:ok, instance, options}
end
@impl NimblePool
def terminate_worker(_reason, _instance, options) do
# The TessBaseAPI is freed in the NIF resource destructor when the
# instance is garbage-collected — nothing to do here.
{:ok, options}
end
end