# LumiScan Text Reader 1.1.0.7 – complete documentation

> All pages of the LumiScan Text Reader 1.1.0.7 manual (language: en) by HD Vision Systems GmbH, in navigation order. Each page starts with a level-1 heading followed by its URL; cite that URL. Manual: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/

# Description – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/description.html

LumiScan Text Reader reads printed text out of a camera image: part numbers, batch codes, date codes, labels.

You point it at an image node, tell it where the text is and what it may contain, and it reports the text along with a confidence for every word.

It runs on ctrlX OS (ctrlX CORE or ctrlX IPC) with the ctrlX Data Layer, and on a PC (Windows 11 or Ubuntu 24.04) with LumiScan Keryx, the Data Layer of HD Vision Systems for PCs. Its nodes are the same on both, see [Data Layer Reference](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/datalayer-reference.html).

What is the same for every LumiScan app - how addresses are built, the handshake, execute and stream call patterns, status codes, settings and tools - is described in the [LumiScan Keryx](https://docs.hdvisionsystems.com/en/lumiscan-keryx/index.html) manual. This manual covers only what is specific to LumiScan Text Reader; its own nodes are listed in the [Data Layer Reference](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/datalayer-reference.html).

> [!NOTE]
> If the marking is a **code** rather than text - Data Matrix, QR, Aztec, or a barcode - use the sibling application for that code. A code carries its own error correction, so it either decodes exactly or fails; reading it as text is both slower and less reliable.

## Text is not a code, and that changes how you use it

This is the most important thing to understand before putting the application into production.

A code reader gives you a decoded value or nothing. An OCR reader always gives you **something**, and a misread looks exactly like a correct read:

- every word comes back with a **confidence** rather than a guarantee;
- an unfamiliar character is **substituted** rather than rejected - `O` for `0`, `S` for `5`;
- the same label reads differently depending on how it was cropped and which characters were permitted.

The application is built around that. Two settings turn a plausible read into a trustworthy one, and both default to the permissive value so that a fresh installation shows you what it can actually see:

| Setting | What it does |
| --- | --- |
| `input/characters/mode` | Restricts what characters may be returned. A part number that is only ever digits, read with `DIGITS`, cannot come back with an `O` in it. This removes a whole class of errors rather than filtering them afterwards. |
| `input/min-score` | The confidence a word must reach to be reported. At the default `0` everything is reported, including guesses the engine has almost no faith in. |

Set both before you rely on a reading. See [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html).

## What a reading contains

`output/result` carries the whole text as the engine laid it out, **and** a per-word list with a confidence and a position for each word. They answer different questions:

- read the whole text when the **shape** matters - a multi-line address, a label with a header;
- read the word list when the **content** matters and a wrong answer is worse than no answer.

Also in the result: the language, engine and layout that were **actually** used. That last point matters because a language whose model is not installed is quietly replaced by English rather than refused - see [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html).

## An empty reading is not a failure

A run that finds nothing reports success with zero words. That is the normal answer for an empty carrier, a part turned the wrong way, or a label out of frame.

Branch on the word count, not on the status code, to decide whether a part carried text. Every reason a run could genuinely fail - a camera that is not answering, an unusable setting, an engine that could not start - is reported as a negative status code instead.

> [!IMPORTANT]
> One case reads as empty when the text is perfectly legible: a colour image in which a photograph or other colourful scenery shares the frame. If a reading is empty and you can see the text in the image, set `input/preprocess = GRAYSCALE` before investigating anything else - see [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html).

# Requirements – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html

## Licence {#licence}

How LumiScan Text Reader is licensed depends on where you bought it and where it runs:

| Bought from | Runs on | Licensing |
| --- | --- | --- |
| HD Vision Systems | Windows 11 or Ubuntu 24.04 | The licence is held on your LumiScan Key. It is a one-time purchase and needs no renewal.<br><br>[Licensing path: bought from HD Vision Systems](https://docs.hdvisionsystems.com/en/lumiscan-licensing/path-hdvs.html) |
| ctrlX Store | Windows 11 | The licence is held in ctrlX WORKS. The LumiScan Key is required in addition; it protects the software against reverse engineering.<br><br>[Licensing path: ctrlX Store, Windows](https://docs.hdvisionsystems.com/en/lumiscan-licensing/path-ctrlx-windows.html) |
| ctrlX Store | ctrlX OS (ctrlX CORE or IPC) | ctrlX licence `SWL-W-XCx-TEXTxxRDSxHDVSx-V1NN`, one licence per ten modules.<br><br>[Licensing path: ctrlX Store, ctrlX OS](https://docs.hdvisionsystems.com/en/lumiscan-licensing/path-ctrlx-os.html) |

Each device on which LumiScan Text Reader runs at the same time needs its own LumiScan Key. Keys can be exchanged between devices.

How to install CodeMeter Runtime and how to request, renew and move licences is described in [LumiScan Licensing](https://docs.hdvisionsystems.com/en/lumiscan-licensing/index.html).

On ctrlX OS, the application shuts down shortly after starting if the licence is missing; the reason is written to the system log.

## A platform and its Data Layer

| Platform | Data Layer |
| --- | --- |
| ctrlX OS (ctrlX CORE or ctrlX IPC) | ctrlX Data Layer, provided by ctrlX OS |
| a PC with Windows 11 or Ubuntu 24.04 | LumiScan Keryx, the Data Layer of HD Vision Systems for PCs |

On ctrlX OS there is nothing to install for the Data Layer.

On a PC the application starts the LumiScan Keryx broker when none is running; installing and setting up LumiScan Keryx is described in [LumiScan Keryx on a PC](https://docs.hdvisionsystems.com/en/lumiscan-keryx/keryx.html).

## An image source on the same device

The application reads its images from another application’s Data Layer image node. That application must run on the **same device** as the Text Reader - the same ctrlX CORE or ctrlX IPC, or the same PC. Remote Data Layer access is not supported.

In practice this is LumiScan Cam Driver, but any application works as long as its image node provides these four children:

| Node | Type | Description |
| --- | --- | --- |
| `data` | array of uint8 | Raw, uncompressed pixel data |
| `width` | uint64 | Image width in pixels |
| `height` | uint64 | Image height in pixels |
| `pixel-type` | string | GenICam pixel format name |

### Supported pixel formats

`Mono8`, `Mono12`, `RGB8`, `RGB12`, `BGR8`, `BGR12`, `BayerGR8`, `BayerGR12`, `BayerRG8`, `BayerRG12`.

Bayer images are converted to colour and 12-bit images are scaled down to 8 bit before reading.

Any other pixel format is rejected with status code `-3`.

## Language models

The application ships with English (`eng`) and the orientation model (`osd`) installed, which is enough to read Latin-script text out of the box.

Other languages need their Tesseract `.traineddata` file placed in a directory the application can read - on ctrlX OS the `removable-media` interface means a USB stick works - and `system/set-tessdata-dir` pointed at it. See [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html).

## Image quality

OCR is more demanding of the image than code reading is, and no setting recovers text the camera did not capture. As a rule of thumb:

- characters at least **20 pixels tall**, and 30 or more for small or noisy print;
- good contrast between the marking and its background;
- the text in focus, and not glared out by the lighting;
- the text roughly upright, unless you enable an orientation-detecting layout.

Dot-matrix and laser-etched marking are harder than printed text. If `engine = DEFAULT` struggles with them, `LEGACY` is worth trying before anything else.

## Storage

The application stores nothing but its module list and their settings - a few kilobytes of JSON in the application’s configuration directory. On ctrlX OS that directory is part of the active solution. The language models are installed with the application and are not written to. Where the directory is on each platform is listed in [Kept in the settings file](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#saving).

The image stream (experimental) adds one folder beside them, `settings-history/`: one small JSON file for each settings hash a module publishes, never deleted. See [Processing an Image Stream](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html).

## Timing

Reading is **synchronous**: the call to `execute` returns when the read has finished.

How long that takes is driven mostly by how much image the engine is given, which makes `input/crop` the setting to reach for first when a cycle is too slow - a tight crop around the label is both faster and more accurate than the whole frame. The `AUTO_OSD` and `SPARSE_OSD` layouts cost noticeably more than the others, because they load a second model and work out the orientation before reading.

See [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html).

# Execute a Method – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execute-a-method.html

Every operation of this application - reading text, choosing the language models and module management - is started the same way, and reports its outcome the same way. They are execute methods of the Data Layer; how execute methods work in general is described in [Execute Methods](https://docs.hdvisionsystems.com/en/lumiscan-keryx/execute.html).

## The shape of an operation

An operation lives under its own address and always has three parts:

```
<operation>/execute                 write here to run it
<operation>/input/...               the values it works on
<operation>/output/status-code      what happened, as a number
<operation>/output/status-text      what happened, in words
```

Some operations have more under `output/` - the text of a reading, for instance - but `execute`, `status-code` and `status-text` are always there.

## Running an operation

1. Write the input nodes.
2. Write to `execute`. The value does not matter; the write itself triggers the operation.
3. Read `output/status-code` to find out whether it worked.

Writing to `execute` is a **blocking call**: it returns when the operation has finished, so there is no need to poll a "ready" flag or track state in your program. Inputs keep their values between runs, so a repeated operation only needs step 2.

## Checking the outcome

`output/status-code` is a signed number, and **the sign alone tells you whether it worked**:

| Value | Meaning |
| --- | --- |
| greater than 0 | The operation succeeded |
| 0 | The operation has not run since the application started |
| less than 0 | The operation failed and produced nothing |

So the check in a PLC is simply `status-code < 0`. You only need a specific code when you want to react differently to different failures - an uninstalled language, say, versus a camera that is not answering.

`output/status-text` carries the same outcome as a sentence, and names the offending value when there is one:

```
ok: 4 word(s), mean score 0.91 (eng, SINGLE_BLOCK)
error: crop/startX + crop/width (100 + 900 = 1000) exceeds the image width 640
```

Read it when something goes wrong; do not build logic on it, because the wording can change between releases. The full list of codes is in [Execution Status](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execution-status.html).

> [!NOTE]
> The reason arrives on `output/status-text`, not in the response to the `execute` write itself. A failed write answers with a status code only - over REST, a bare `400`. The Data Layer discards any detail a provider attaches to a failed call, so reading `output/status-text` afterwards is the way to find out what was wrong. The results a write can answer with, and their names on the ctrlX Data Layer and on LumiScan Keryx, are listed in [Results of a method call](https://docs.hdvisionsystems.com/en/lumiscan-keryx/status-codes.html#method-results).
>
> The same text is written to the application log, so a failure is also diagnosable without touching the Data Layer at all: on ctrlX OS from the ctrlX Logbook or `journalctl`, on a PC from the application’s console output. When several inputs are wrong, each one gets its own warning line.

## One operation at a time

A module runs one operation at a time. Starting a second one while the first is still running does not queue it - it returns immediately with status code `-1` (busy) and does nothing. The write itself answers `TooManyOperations` (ctrlX: `DL_TOO_MANY_OPERATIONS`), see [One operation at a time](https://docs.hdvisionsystems.com/en/lumiscan-keryx/execute.html#busy).

The lock is per module, so different modules read in parallel - a second camera is not held up by the first.

## The outputs belong to the run that is starting

Every method clears its own output nodes as soon as it begins - `read-text` its `output/result`. They are cleared **before** anything that can fail, so a run that fails for any reason leaves them empty rather than holding what an earlier run produced.

That is what makes it safe to read an output without checking `output/status-code` first: what you see is either this run’s work or nothing, never a stale value from a previous image.

`output/status-code` and `output/status-text` are the exception - they always describe the latest attempt, which is the whole point of them.

> [!WARNING]
> **Read an output before triggering the next run.** The outputs describe the most recent run, and the next call empties them the moment it starts. Calling `execute` again and then reading the previous result gives you nothing.
>
> The one case that does **not** clear anything is a call rejected as `-1` (busy): it never ran, so it leaves the running operation’s outputs alone. So a second `execute` fired immediately after the first has two possible outcomes, and they differ:
>
> - the first run is **still going** - the second is refused with `-1`, and the outputs still hold the run before it;
> - the first run has **just finished** - the second is accepted, and it clears the outputs the first run had only just written.
>
> Wait for `execute` to return, read the outputs, and only then trigger the next run.

# Execution Status – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execution-status.html

Every operation reports its outcome on two nodes beside it: `output/status-code` for your program, and `output/status-text` for a person. These codes are the execution status of the finders, not the module status of the Cam Driver, AI Object Matching or Calibration; see [Execution status of the finders](https://docs.hdvisionsystems.com/en/lumiscan-keryx/status-codes.html#execution-status).

## Status codes

| Code | Meaning | When it happens |
| --- | --- | --- |
| `0` | Idle | The operation has not run since the application started |
| `1` | OK | The operation completed. A reading may still have found no words - that is a normal result, not an error |
| `-1` | Busy | Another operation of the same module was still running. Nothing was done; try again when it has finished |
| `-2` | Invalid input | An input value cannot be used - a required node left empty, an unknown `input/engine`, `input/layout`, `input/preprocess` or `input/characters/mode`, an empty character set, a language with no model installed, a crop that does not fit the image, or an image address pointing at this application itself |
| `-3` | Image read failed | The image node at the given address is missing, empty, inconsistent, or uses an unsupported pixel format. Check that the camera application is running and has captured an image |
| `-4` | Source not found | A required file or directory is missing or unreadable - for `system/set-tessdata-dir`, a path that is not a directory |
| `-5` | Invalid crop region | The region of interest does not fit inside the image. `read-text` reports a crop that does not fit as `-2`, see [Reading Text](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-text.html) |
| `-6` | Write failed | A file could not be written - usually a permission problem or a full disk |
| `-7` | Algorithm failed | The OCR engine could not start, or a language model could not be loaded - for example a model whose upload is still in progress |
| `-8` | Publish failed | The result could not be written back to the Data Layer |
| `-9` | Module not found | The named module does not exist. Check `system/modules` for the current list |
| `-10` | Module already exists | A module with that name is already there |
| `-11` | Licence unavailable | No licence covers the module. See [Module Management](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/module-management.html) |

> [!NOTE]
> These numbers are part of the application’s interface. New codes may be added in later releases, but existing ones never change meaning, so logic written against them keeps working after an update.
>
> This is also why checking the **sign** is the more robust test: `status-code < 0` stays correct even when a release introduces a failure your program has never seen.

## Which operation reports which

| Operation | Possible codes |
| --- | --- |
| `read-text` | `1`, `-1`, `-2`, `-3`, `-7` |
| `system/set-tessdata-dir` | `1`, `-2`, `-4`, `-6` |
| `system/save-module-inputs` | `1`, `-6`, `-9` |

## Status text

`output/status-text` always starts with `ok:` or `error:` and gives the reason in words. It is written at the same time as the status code, so both always agree.

Each operation has its own pair of nodes, and they keep their values until that operation runs again. A failed `set-tessdata-dir` therefore does not disturb the result of the last reading.

# Reading Text – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-text.html

One reading, on demand. Nothing happens until a client writes `<module>/read-text/execute`.

## What a run does

1. Refuses the call if this module is already busy (status code `-1`).
2. **Clears the previous reading**, so a failure never leaves old text behind.
3. Checks every setting and reports **all** the problems at once.
4. Reads the image from `input/input-image-address`.
5. Applies `input/crop`.
6. Runs the OCR engine and publishes the text.

## The settings

| Node | Default | What it does |
| --- | --- | --- |
| `input/input-image-address` | (empty) | Address of the image node to read, e.g. `hdvisionsystems/lumiscan-cam-driver/camera1/output/image`. |
| `input/crop/startX`<br> `input/crop/startY`<br> `input/crop/width`<br> `input/crop/height` | 0 | The part of the image to read. All zero reads the whole image; a zero width or height reaches the right or bottom edge. **Crop tight around the text** - see [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html). |
| `input/language` | `eng` | Which language model to read with. The models installed on this device are listed on `system/languages`. |
| `input/engine` | `DEFAULT` | `DEFAULT` picks the best available, `LSTM` is the modern neural-network engine, `LEGACY` the original pattern-matching one, and `COMBINED` runs both. `LEGACY` is worth trying on dot-matrix and laser-etched marking. |
| `input/layout` | `SINGLE_BLOCK` | How much structure to look for in the image. |
| `input/preprocess` | `NONE` | What is done to the image before it is read. Reach for `GRAYSCALE` when a colour frame comes back with no words at all, which happens when a photograph shares the image with the text - see [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html). |
| `input/characters/mode`<br> `input/characters/allow`<br> `input/characters/deny` | `ALL_SYMBOLS`<br> (empty)<br> (empty) | Which characters may be returned. |
| `input/min-score` | `0` | The confidence a word must reach to be reported, between 0 and 1. |

## Reading the result

`output/result` is a `text_read_result` flatbuffer.

| Field | Meaning |
| --- | --- |
| `text` | The whole reading exactly as the engine produced it, lines separated by newlines. **Not filtered by `min-score`.** |
| `words[].text`<br> `words[].score`<br> `words[].bbox_x` … `bbox_height` | One recognised word, its confidence from 0 to 1, and a box around it in the coordinates of the region that was read. |
| `word_count` | How many words are reported. |
| `mean_score` | Average confidence over the reported words; 0 when there are none. |
| `min_score` | The threshold that was applied, so a short word list reads as "strict" rather than "bad image". |
| `language` | The language **actually** used - not always the one asked for. |
| `engine`, `layout` | As actually used. |
| `image_width`, `image_height` | The region that was read - the whole image unless a crop was set. |
| `timestamp` | Microseconds since the epoch, when the reading finished. |

### `text` and `words[]` can disagree, on purpose

`text` is the engine’s own output, with the line breaks it found. `words[]` is filtered by `min-score`. With `min-score` at `0.8`, a word the engine reported at `0.4` appears in `text` and not in `words[]`.

If you need them to agree, read `text` only when `word_count` is the number you expected.

## When it fails

| Situation | Status code | What you get |
| --- | --- | --- |
| Another operation on this module is running | `-1` | Nothing done; the previous reading is left alone |
| Any setting is unusable | `-2` | **Every** problem named in one `status-text` |
| The image node cannot be read | `-3` | Empty result |
| The crop does not fit the image | `-2` | Empty result |
| The OCR engine could not start | `-7` | Empty result, with the reason in `status-text` |
| Nothing readable in the image | `1` (success) | Empty text, `word_count` 0 |

The last two rows are the distinction that matters most in production: a reader that could not start reports `-7` and says why, while a working reader looking at a blank part reports success with no words. They used to be indistinguishable.

## The outputs belong to the run that is starting

Every `execute` empties `output/result` the moment it begins, so a client must read a result before triggering the next reading. `status-code` and `status-text` are the exception - they always describe the latest attempt.

# Reading Text on an Image Stream (Experimental) – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html

> [!IMPORTANT]
> The image stream is **experimental**. Its nodes and settings may still change. Use it for evaluation, and verify it with your cameras and your Data Layer before relying on it.

`read-text` reads one image per call: your program sets `input/input-image-address` and writes to `execute`. An image stream works the other way round. The Cam Driver publishes every image it takes on `<camera name>/control/publish/output/stream/frame`, with its size, its pixel format and its lineage (see the Cam Driver manual, *Streaming Images*). A module subscribes to the stream of one camera, reads each frame as it arrives, and publishes one result per frame on `control/subscribe/output/stream/result`. The stream works the same way on the ctrlX Data Layer and on LumiScan Keryx; the concept shared by all LumiScan apps is described in [Streams](https://docs.hdvisionsystems.com/en/lumiscan-keryx/streams.html). This page describes what is particular to the Text Reader.

Each frame is read with the inputs of `read-text` as they are at that moment: the language, the engine, the layout, the character set, the crop region and every other node under `read-text/input/`. The one input the stream does not use is `input/input-image-address`: the frame takes its place. If you change an input while the stream runs, the next frame is read with the new value.

Each result carries the lineage of its frame - which camera took it, in which run of the camera’s stream, as which frame of the run - with the module’s own step added. See [The lineage of a result](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#lineage).

By default the module never falls behind its camera. When frames arrive faster than the module reads them, it takes the newest frame each time the previous read is finished. The frames in between are counted as skipped. How many frames may wait is set with the queue size, see [The queue size](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#queue-size).

A module takes the stream of one camera. For another camera, or other settings, use another module.

## The stream’s nodes {#addresses}

Every module has the stream’s nodes, whether it subscribes to a stream or not. No restart is needed to start using them. All addresses below start with `hdvisionsystems/lumiscan-text-reader/<module-id>/`. They sit beside `read-text/`, not inside it.

**Table 1. Methods**

| Address | Meaning |
| --- | --- |
| `control/subscribe/execute` | Checks the five inputs of `control/subscribe` together, saves them in the module’s settings file, then applies them: a running stream follows on its next cycle, with no restart. Writing an input alone changes nothing. See [Changing the source](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#subscribing). |
| `control/unsubscribe/execute` | Drops the source named by `control/unsubscribe/input/address`, which ends stream mode too. The tag stays. Saved in the module’s settings file like a subscribe. |

**Table 2. Inputs of control/subscribe**

| Address | Type | Meaning |
| --- | --- | --- |
| `control/subscribe/input/enable` | bool | Stream mode: `true` runs the stream, `false` stops it. |
| `control/subscribe/input/address` | string | The one source: a camera’s stream folder, for example `hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream`. The module reads its `frame` node. Another address replaces the source. |
| `control/subscribe/input/tag` | string | The tag the module puts on its results: the module’s step in the lineage of each result carries it, so whoever reads a result downstream sees which module made it. Empty: the module id, the last segment of the module’s address, for example `module-1`. A new tag marks the next result; the source stays subscribed. |
| `control/subscribe/input/queue-size` | uint32 | How many frames may wait while a frame is read: `1` to `64`. See [The queue size](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#queue-size). |
| `control/subscribe/input/keep-latest-output` | bool | `true` to write each stream result to `read-text/output/result` as well. See [Keeping the latest output](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#keep-latest-output). |

The inputs start with what is applied at the start of the application, so a client can change one of them and execute.

What the source must hold: read `hdvisionsystems/lumiscan-text-reader/system/meta/subscription-schema`, the same for every module. It is a JSON Schema of the `frame` node of the source, a `stream_image` with its `lineage` and its `image`, in its JSON form. See [Data Layer Reference](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/datalayer-reference.html).

**Table 3. Input of control/unsubscribe**

| Address | Type | Meaning |
| --- | --- | --- |
| `control/unsubscribe/input/address` | string | The source to drop: the address `setting/stream/address` shows. A `/` at either end does not count. |

**Table 4. Settings**

| Address | Type | Meaning |
| --- | --- | --- |
| `setting/stream/enable`, `setting/stream/address`, `setting/stream/tag`, `setting/stream/queue-size`, `setting/stream/keep-latest-output` | as the inputs | What `control/subscribe/execute` or `control/unsubscribe/execute` applied last, as the settings file keeps it. Read-only. |
| `setting/settings-hash` | string | The hash of what the results are made with, as 16 hex digits. Read-only. See [The settings hash](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#settings-hash). |

**Table 5. Outputs**

| Address | Type | Meaning |
| --- | --- | --- |
| `control/subscribe/output/stream/active` | bool | `true` while the stream runs. |
| `control/subscribe/output/stream/result` | flatbuffers `stream_texts` | The text read from each frame, and the frame’s lineage. Subscribe to it to receive each one. See [The result of a frame](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#result). |
| `control/subscribe/output/stream/last-failure` | string | Why the last frame that could not be read failed. It names the source by its address, see [The result of a frame](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#result). Emptied when the stream starts. |
| `control/subscribe/output/stream/statistics/…` | uint64 | Counters of the current or the last stream, see [Reading the statistics](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#statistics). |

The stream publishes its results on `control/subscribe/output/stream/result`. Of the nodes of `read-text` it writes only `read-text/output/result`, and only with `keep-latest-output` on: `status-code` and `status-text` keep what the last call of the method left there.

## Setting it up {#setting-up}

1. Make sure the Cam Driver’s stream runs: its `hdvisionsystems/lumiscan-cam-driver/<camera name>/control/publish/output/stream/active` is `true`.
2. Set the inputs of `read-text` as for a single read, see [Reading Text](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-text.html). A call of `read-text/execute` on the camera’s `output/image` is a quick way to check them.
3. Write the camera’s stream folder, for example `hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream`, to `control/subscribe/input/address`. Leave `control/subscribe/input/tag` empty to mark the results with the module id, or write a tag of your own to it.
4. Subscribe to `control/subscribe/output/stream/result`.
5. Set `control/subscribe/input/enable` to `true`, then write to `control/subscribe/execute`. Wait until `control/subscribe/output/stream/active` is `true`. The results arrive from then on.
6. To stop, set `control/subscribe/input/enable` to `false` and write to `control/subscribe/execute` again. The frame being read is finished first.

On ctrlX OS, use the Data Layer view of the ctrlX OS web interface or your PLC program for these steps.

On a PC, the same steps with `krxclient`, the command line client of LumiScan Keryx that stands in for a PLC (see [Tools](https://docs.hdvisionsystems.com/en/lumiscan-keryx/tools.html#krxclient)). Add the `--keryx-backend` and `--keryx-url` options your broker needs:

```none
krxclient write hdvisionsystems/lumiscan-text-reader/module-1/control/subscribe/input/address hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream
krxclient write hdvisionsystems/lumiscan-text-reader/module-1/control/subscribe/input/enable true
krxclient execute hdvisionsystems/lumiscan-text-reader/module-1/control/subscribe/execute
krxclient subscribe hdvisionsystems/lumiscan-text-reader/module-1/control/subscribe/output/stream/result --count 5
```

`krxclient subscribe` shows a flatbuffer by its size only. It tells you that results arrive, not what they hold.

Good to know:

- Stream mode is a **level**, not a command: the stream runs for as long as the applied `enable` is `true`.
- The stream can be started before the Cam Driver’s stream. Results arrive once frames do.
- What `control/subscribe/execute` applies is saved in the module’s settings file. After a restart of the application the module starts as it was: one that streamed streams again at once, without your program executing anything. See [Kept in the settings file](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#saving).

## Changing the source {#subscribing}

A module takes one source. A PLC or another client changes it while the application runs, with or without a running stream:

1. Write the new values to the inputs of `control/subscribe` - only those you change: the others hold what is applied.
2. Write to `control/subscribe/execute`. The inputs are checked together, saved in the module’s settings file, and only then applied: `setting/stream/` shows them at once, and a running stream follows on its next cycle. Another address replaces the source; the frames the old one had waiting are counted as skipped. A new tag, queue size or `keep-latest-output` needs no new subscription: a new tag marks the next result.
3. To drop the source, write its address to `control/unsubscribe/input/address`, then write to `control/unsubscribe/execute`. Stream mode ends with it. The inputs of `control/subscribe` are set to what it applied: stream mode off, no address. The tag stays: it marks the module’s results, not the source.

When the settings file cannot be written, nothing changes. See [Kept in the settings file](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#saving).

The methods answer with a result of the Data Layer. Its name differs between the two Data Layers, see [Results of a method call](https://docs.hdvisionsystems.com/en/lumiscan-keryx/status-codes.html#method-results):

| ctrlX OS | PC (LumiScan Keryx) | When |
| --- | --- | --- |
| `DL_OK` | `Ok` | Done. |
| `DL_INVALID_VALUE` | `Rejected` | `control/subscribe/execute`: stream mode without an address, or a queue size outside `1` to `64`. `control/unsubscribe/execute`: the address is not the source, or there is no source. |
| `DL_CREATION_FAILED` | `CreationFailed` | The settings file could not be written, for example because it is not valid JSON. Nothing changed. |

These methods have no `status-text`. The reason for a refusal is written to the application log.

When the source cannot be subscribed to, the module tries it again every 5 seconds while the stream runs, and logs the first failure.

## The result of a frame {#result}

`control/subscribe/output/stream/result` holds a FlatBuffers table of type `stream_texts` (file identifier `HSTX`), registered on the Data Layer as `types/hdvisionsystems/lumiscan-text-reader/stream_texts`. Its compiled schema is installed as `bfbs/stream_texts.bfbs`.

| Field | Meaning |
| --- | --- |
| `lineage` | Where the result came from, see [The lineage of a result](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#lineage). |
| `result` | The reading, as a `hdv.text_read.Result`: the same table `read-text` writes to `output/result` for the same picture, described in [Reading Text](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-text.html). |

A read of `control/subscribe/output/stream/result` returns the latest result; before the first frame, an empty one. The last result stays after the stream stops.

A frame is decoded as `read-text` decodes the image node, so the same pixel formats are accepted, see [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html). A frame and the same picture read through `input/input-image-address` give the same reading.

A frame that cannot be read gives no result. It is counted in `statistics/failed-frames`, and `control/subscribe/output/stream/last-failure` says why. The reasons are the ones `read-text` gives in its `status-text`: an input it would refuse - a language with no model among them - a crop region that does not fit the frame, a region that cannot be read, a detection that failed.

### A frame read in another language is dropped {#language}

`read-text` checks the language before it reads. If the model disappears between that check and the read, the engine reads with `eng` instead: `read-text` then publishes the reading, with a `NOTE` in its `status-text` (see [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html)).

The stream does not publish such a reading. Its settings hash names the model that was asked for, not the one that was used, so the frame fails instead:

```
a frame of 'hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream' failed: language 'deu' is no longer installed; the frame was read with 'eng' instead and its result dropped
```

A model you upload is used from the next frame on, as for `read-text` - a new one, and one overwritten in place under the same name alike: the frame is read with the model as it is on disk, and the hash names that model.

## The lineage of a result {#lineage}

`lineage.steps` lists where the result came from, origin first:

1. The camera’s step, as the Cam Driver made it: `producer` is the camera’s address, `hdvisionsystems/lumiscan-cam-driver/<camera name>`; `run` when the camera’s stream started; `sequence` the frame’s number in that run; `external_id` the camera’s own frame number; `settings_hash` the camera’s settings; `tag` the Cam Driver’s tag for the camera, by default its module id, for example `camera1`.
2. The module’s step: `producer` is the module’s address, `hdvisionsystems/lumiscan-text-reader/<module-id>`; `run` when the module’s stream started (each start is a new run); `sequence` the result’s number in the run, from `1`; `settings_hash` the module’s settings, see [The settings hash](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#settings-hash); `elapsed_us` the microseconds from the frame’s arrival at the module to its result, waiting included; `time_us` when the result was made, in microseconds since the epoch; `tag` the module’s tag, `control/subscribe/input/tag`, or the module id when it is empty.

So a result names its frame exactly: the camera by `producer`, the frame by `run` and `sequence`. To know which camera a result came from, read the `producer` of its first step. A gap in the camera step’s `sequence` between two results of the same camera is frames that were skipped, failed or lost.

A lineage holds at most 16 steps. A longer one keeps the origin and the newest steps, and `dropped` counts the steps left out.

A frame that carries no lineage gives a result with the module’s step alone. A value on a stream’s `frame` node that is not a `stream_image` is ignored, and logged once.

## The settings hash {#settings-hash}

`setting/settings-hash`, and the `settings_hash` of the module’s step in each result, identify what the results are made with. The node shows the hash as 16 hex digits; the step holds the same value as a number. The hash covers:

- every input of `read-text` the read uses, exactly as it was read for the frame - all of them but `input/input-image-address`;
- the language model, `<language>.traineddata`, by its content: a new model uploaded under the old name is a new hash. With the layouts `AUTO_OSD` and `SPARSE_OSD`, the orientation model `osd.traineddata` beside it too. The models are hashed only when the inputs pass the check;
- the application’s name and version.

It does not cover stream mode, the source, the queue size or keep-latest-output: they choose and pace the frames, they do not change what is read from them.

`setting/settings-hash` is updated about every half second, also while the stream is off. The first time a hash appears, the module writes the settings it stands for to `settings-history/<module-id>-<hash>.json` in the application’s configuration directory, so a hash found in a result can be looked up later. The application does not delete these files. A file looks like this, shortened:

```json
{
    "format": 1,
    "settings": {
        "read-text/input/language": "eng",
        "read-text/input/layout": "SINGLE_BLOCK",
        "read-text/language-model": {
            "file": "<model directory>/eng.traineddata",
            "fnv1a64": "<16 hex digits>"
        },
        "software.name": "lumiscan-text-reader",
        "software.version": "<version>"
    },
    "settingsHash": "<16 hex digits>"
}
```

> [!NOTE]
> A result carries the hash only. A different hash says that something the results are made with changed, not what changed. To find out, compare the two files in `settings-history` that the hashes name, by hand. Every input the read uses is hashed, so a change that does not alter a result can still change the hash.

## Reading the statistics {#statistics}

The counters start from zero each time the stream starts, are updated about four times a second while it runs, and keep their last values after it stops. All addresses start with `control/subscribe/output/stream/statistics/`.

| Counter | Meaning |
| --- | --- |
| `elapsed-ms` | Milliseconds since the stream started. Divide a counter by it for a rate. |
| `received-frames` | Frames the subscription delivered. |
| `processed-frames` | Frames read, each with a result published on `control/subscribe/output/stream/result`. |
| `skipped-frames` | Frames never read: pushed out of the queue by newer ones while a frame was read (see [The queue size](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#queue-size)), or waiting when the source was replaced or dropped. |
| `failed-frames` | Frames that could not be read. `control/subscribe/output/stream/last-failure` says why the last one failed, and the log names the failures, at most one line every 5 seconds. |
| `lost-frames` | Frames dropped before they arrived, counted from gaps in the camera’s `sequence` within one run of its stream. A new source, or a new run of the camera’s stream, starts the count again. |
| `processing-us` | Microseconds all reads took, summed, without the time a frame waited for `read-text`. Divide by `processed-frames` for the mean. |

Frames still waiting when the stream stops are dropped without being counted.

## Kept in the settings file {#saving}

Stream mode and the source are settings of the module. They are kept in the `stream` member of the module’s settings file, `<module-id>.json` in the application’s configuration directory - the file `system/save-module-inputs` writes the inputs to.

`control/subscribe/execute` and `control/unsubscribe/execute` save what they apply there themselves, all five values in one write, before they apply them. When the file cannot be written, the method answers `DL_CREATION_FAILED` (on a PC `CreationFailed`) and nothing changes. After a restart the module subscribes to the same source and, when `stream.enable` is `true`, starts its stream at once: nobody needs to execute anything.

Saving changes only the `stream` member; the rest of the file stays as it was. The module makes the file when there is none yet - a module whose inputs were never saved has none. `system/save-module-inputs` in turn keeps the `stream` member when it rewrites the file; the inputs of `control/subscribe` are not saved as the module’s inputs.

The configuration directory is:

| Platform | Configuration directory |
| --- | --- |
| ctrlX OS | `/var/snap/rexroth-solutions/common/solutions/activeConfiguration/lumiscan-text-reader` |
| PC (Windows) | `%APP_RESOURCE_DIR%\etc\lumiscan-text-reader`; without `APP_RESOURCE_DIR`, `C:\ProgramData\HD Vision Systems GmbH\LumiScan Vision Suite\lumiscan-text-read\etc\lumiscan-text-reader` |

The `stream` member, beside the inputs of `read-text` (shortened here):

```json
{
    "read-text": {
        "language": "eng"
    },
    "stream": {
        "enable": true,
        "address": "hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream",
        "queueSize": 1,
        "keepLatestOutput": false
    }
}
```

| Key | Required | Meaning |
| --- | --- | --- |
| `enable` | No | Stream mode: `true` starts the stream at the start of the application. Needs an `address`. `false` if left out. |
| `address` | No | The one source: a camera’s stream folder. |
| `tag` | No | What marks the module’s results: the `tag` of the module’s step in their lineage. The module id if left out. |
| `queueSize` | No | How many frames may wait: `1` to `64`, `1` if left out. |
| `keepLatestOutput` | No | `true` to write each stream result to `read-text/output/result` as well. `false` if left out. |
| `onEntry` | No | The steps applied to each frame as it arrives, each an object with a `kind`, as every LumiScan application’s `stream` has them. This application has none yet: leave it out, or empty (`[]`). A step starts the module out of stream mode, with a warning in the log; saving keeps it. |

`control/subscribe/execute` and `control/unsubscribe/execute` write all five.

Good to know:

- The file is read once, when the module is restored at the start of the application: a module in the saved module list, or `module-1` on a fresh installation. Changes made to the file by hand take effect at the next start.
- A module made with `system/create-module` starts with the stream off, no source, a queue size of `1` and `keep-latest-output` off, whatever a file of its name holds. What it applies from then on is saved to that file.
- To have a module and its stream back after a restart, save the module list too, with `system/save-current-modules`. Saving a list that no longer names a module deletes that module’s file, and its stream settings with it. See [Module Management](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/module-management.html).
- A value that cannot be used is ignored, and logged: an `enable` or `keepLatestOutput` that is not `true` or `false`, an `address` or `tag` that is not a string, a `queueSize` outside `1` to `64` (`1` is used). Stream mode without an `address` is ignored: the module starts out of stream mode.
- The `subscriptions` member of earlier versions is no longer read, and the module logs that it moved: give the source as `address`.

> [!WARNING]
> Do not add comments to `<module-id>.json`. The stream reads a file with comments, but the module’s inputs are then not restored at start: the log reports the file as unreadable.

## The queue size {#queue-size}

`control/subscribe/input/queue-size` sets how many frames may wait while a frame is read: `1` to `64`, `1` by default. It is saved as `stream.queueSize`, and `setting/stream/queue-size` shows the value in use. A frame that arrives when that many wait pushes out the oldest of them, which is counted in `statistics/skipped-frames`. A new queue size applies from the next frame that arrives, with no new subscription.

| Queue size | Effect |
| --- | --- |
| 1 (default) | The module reads the newest frame. It never falls behind; when reading is slower than the camera, it skips frames. |
| More than 1 | Short bursts of frames are all read, and fewer are skipped. When reading is slower than the camera for longer, the module falls behind: it reads frames up to that many reads old. Each waiting frame holds a whole image in memory. |

## Keeping the latest output {#keep-latest-output}

With `control/subscribe/input/keep-latest-output` `true` - applied by `control/subscribe/execute`, saved as `stream.keepLatestOutput` - the module also writes each stream result to `read-text/output/result`, as `read-text/execute` writes it, once the result is published on `control/subscribe/output/stream/result`. A client that reads `read-text/output/result` then has the newest frame’s result without subscribing to the stream. `status-code` and `status-text` are not written: they keep what the last call of `read-text` left there. It costs a copy of each result. Off by default.

## read-text while the stream runs {#while-streaming}

The stream and `read-text` use the same inputs and the module’s one recognition engine, and they never run at the same time on one module:

- A frame that arrives while `read-text` runs waits until the method has finished. It is delayed, not failed.
- A call of `read-text/execute` while a frame is read answers busy: `output/status-code` is `-1`, the write answers `DL_TOO_MANY_OPERATIONS` (on a PC `TooManyOperations`), and nothing is done. `output/result` keeps its value, as for any busy call (see [Execute a Method](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execute-a-method.html)). With a camera streaming, this happens often. Stop the stream before you call the method, or call it again.
- `system/remove-module` also answers busy (`-1`) while a frame of that module is read. Stop the stream first. Removing a module stops its stream.
- `system/set-tessdata-dir` and `system/refresh-languages` are not held up by the stream.

Other modules are not affected: each module has its own lock, its own engine and its own stream.

## Limitations {#limitations}

- A frame read with another language than the one asked for fails, see [A frame read in another language is dropped](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html#language).
- The stream has been tested on Windows with LumiScan Keryx and with simulated images only. It has not been verified on Linux, on the ctrlX Data Layer, or with a real camera.
- LumiScan Cockpit does not draw the stream’s results yet.
- Changes made to the settings file by hand take effect at the next start of the application.
- The hash covers every input the read uses. It changes with some edits that do not change a result.

# Reading Accurately – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html

OCR does not fail cleanly. A barcode either decodes or does not; text always comes back with **something**, and a misread looks exactly like a correct read until somebody checks it.

Five settings decide whether what comes back can be trusted. They matter roughly in this order.

## 1. Crop to the text

`input/crop` is the most effective setting here, and the one most often left at its default.

A full 1920x1080 frame containing a 200x60 label gives the engine an enormous amount of background to find text in - and it finds some: edges of the part, shadows, the conveyor. Those become low-scoring words that pad the reading, and they push the layout analysis toward "this is a complicated page" when it is one line of print.

Crop tight around where the text will be. It is faster, and it removes a whole class of wrong answers instead of filtering them afterwards.

> [!NOTE]
> A crop that does not fit the image is **refused**, not trimmed to fit. A region silently shrunk would show up much later as words that are simply missing.

## 2. If you cannot crop, merge the colour channels

`input/preprocess` exists for one failure, and it is a nasty one: **a colour frame in which a photograph or other colourful scenery shares the image with the text can come back with no words at all**. Not garbled - empty, while `status-code` reports success. That is indistinguishable from an image with no text in it, so it does not look like a setting to change; it looks like the camera is pointed at the wrong thing.

Measured on one such frame - a 300x168 capture, a parrot photograph on the left, a title on a dark panel on the right:

| `preprocess` | Read |
| --- | --- |
| `NONE` (default) | *nothing at all* |
| `GRAYSCALE` | `Title Of The Presentation` |
| `BINARIZE_OTSU` | `Title Of The Presentation` |

No `layout` and no `engine` rescues the first row - all 24 combinations of the two return nothing. What defeats the engine is how it reduces a *colour* image before reading it, not the size of the text: the same frame flattened to grey reads perfectly, and so does the colour frame cropped to the panel.

> [!IMPORTANT]
> Crop first. Cropping fixes this case in colour, costs nothing, and helps for all the reasons above. Reach for `preprocess` when the text and the scenery cannot be separated by a rectangle - a label on a patterned part, or a screen photographed whole.

`GRAYSCALE` is the one to use. `BINARIZE_OTSU` additionally forces every pixel to black or white, and is worth trying only if `GRAYSCALE` did not help: it discards the smooth edges the engine reads character shapes from, and on the frame above it reads `Title 01 The` where `GRAYSCALE` reads `Title OF The`.

> [!NOTE]
> Neither setting invents text. On a frame with no text in it - the photograph alone, or a flat grey field - all three settings return nothing, so turning this on cannot manufacture a reading.

## 3. Restrict the character set

`input/characters/mode` picks a set, `allow` adds to it, and `deny` removes from it.

The classic OCR errors are all substitutions between character classes: `O`/`0`, `I`/`1`/`l`, `S`/`5`, `B`/`8`, `Z`/`2`. A part number that is only ever digits, read with `mode = DIGITS`, cannot produce any of them. This is nearly free accuracy and costs one setting.

| Marking | Setting |
| --- | --- |
| Numeric part or batch number | `mode = DIGITS` |
| Upper-case alphanumeric code | `mode = UPPERCASE_LETTERS`, `allow = 0123456789` |
| Date code with separators | `mode = DIGITS`, `allow = /-.` |
| Anything with a fixed alphabet | `mode = CUSTOM`, `allow =` the alphabet |
| Free text, human readable | `mode = ALL_SYMBOLS` (the default) |

> [!WARNING]
> **An empty character set is refused.** `mode = CUSTOM` with an empty `allow`, or a `deny` that removes everything the mode selected, would be passed to the engine as **no filtering at all** - the exact opposite of what was configured, and invisible in the result. Both are rejected with status code `-2`, naming the node.

## 4. Match the layout to what you are looking at

`input/layout` tells the engine how much structure to look for. The default is `SINGLE_BLOCK`, because a cropped label is one block of text.

| Value | Use for |
| --- | --- |
| `SINGLE_BLOCK` | A cropped label - **the default** |
| `SINGLE_LINE` | One line of print, nothing else in the crop |
| `SINGLE_WORD` | One part number, one code |
| `SINGLE_CHAR` | One character, e.g. a cavity or station mark |
| `RAW_LINE` | One line, with no attempt to find structure at all |
| `AUTO` | Several blocks in unknown positions |
| `SPARSE` | Text scattered across the frame with no layout |
| `AUTO_OSD`, `SPARSE_OSD` | As above, and work out the orientation as well |
| `SINGLE_COLUMN`, `SINGLE_BLOCK_VERT`, `CIRCLE_WORD` | The shapes their names describe |

Narrower is better when you know what you are looking at: telling the engine "this is one word" stops it inventing a second one.

> [!NOTE]
> The `_OSD` variants cost more than they look. They load a second model and spend time deciding which way up the text is. In an inspection cell the part arrives the same way up every time - a fact the mechanics already guarantee - so paying for it on every reading is waste. Use them only when the orientation genuinely varies.

## 5. Set a confidence threshold

`input/min-score` is the confidence a word must reach to appear in the word list. **It defaults to 0**, which reports everything, including guesses the engine has almost no faith in.

That default is deliberate - a fresh installation should show you what it can see rather than silently hide most of it - but it is not a production setting.

To choose one:

1. Run a few known-good parts with `min-score` at 0.
2. Look at `mean_score` and the individual word scores in the result.
3. A correct reading on a decent image scores well above 0.8; the spurious words a loose crop picks up usually sit below 0.5.
4. Set the threshold in the gap, closer to the correct readings than to the noise.

`min_score` is reported back in the result, so a reader looking at a short word list can tell a strict threshold from a bad image.

## What this does not fix

- **Poor contrast, glare, or a part out of focus.** No setting recovers text the camera did not capture. Fix the lighting.
- **Dot-matrix and laser-etched marking.** These defeat the `LSTM` engine more often than printed text; `engine = LEGACY` sometimes does better and is worth trying first.
- **A part that moved.** The reader has no idea where the label should be. If it drifts outside the crop the reading is empty rather than wrong - which is the better failure, and another reason to crop.

# Language Models – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html

The reader needs a Tesseract language model - a `.traineddata` file - to read anything at all.

Which models are installed is **application-wide**: ten modules reading the same labels want the same models, so the setting lives on `system/` and not once per module.

On ctrlX OS, a model is uploaded over the ctrlX file interface.

On a PC, to upload a model is to copy its file into the directory `system/tessdata-dir` names.

```
hdvisionsystems/lumiscan-text-reader/system/
├── tessdata-dir              where you upload your own models   read-only
├── tessdata-dirs             every directory searched, in order read-only
├── languages                 the codes found across all of them read-only
├── set-tessdata-dir/         changes the upload directory
└── refresh-languages/        re-reads the directories
```

## Two directories, searched in order

1. **Your upload directory**, `tessdata-dir`. By default that is the folder `tessdata` in the application’s configuration directory, beside the application’s configuration file. It is created empty on first start.

   | Platform | Default `tessdata-dir` |
   | --- | --- |
   | ctrlX OS | `/var/snap/rexroth-solutions/common/solutions/activeConfiguration/lumiscan-text-reader/tessdata` |
   | PC (Windows) | `%APP_RESOURCE_DIR%\etc\lumiscan-text-reader\tessdata`; without `APP_RESOURCE_DIR`, `C:\ProgramData\HD Vision Systems GmbH\LumiScan Vision Suite\lumiscan-text-read\etc\lumiscan-text-reader\tessdata` |

2. **The models shipped inside the application**, which are read-only and always searched.

`tessdata-dirs` shows both, in the order they are searched.

> [!IMPORTANT]
> **Uploading adds languages, it never removes the shipped ones.** You do not have to copy our files anywhere, and English keeps working whatever you do. A model uploaded under the same name as a shipped one wins, which is how you would replace English with a different English model.

## What is installed

English (`eng`) and `osd`. Together about 34 MB, which is why only these two ship - every extra language would grow the application for every customer, and uploading is how you get the rest.

> [!NOTE]
> `osd` is **not a language**. It is the orientation-and-script model that the `AUTO_OSD` and `SPARSE_OSD` layouts need. It appears in `languages` because it is a file in the directory, but setting `input/language` to `osd` will not read anything.

## Adding a language

1. **Download the model.** The Tesseract project publishes them at `github.com/tesseract-ocr/tessdata_fast` and `github.com/tesseract-ocr/tessdata_best`. `fast` is smaller and quicker, `best` is more accurate; `fast` is the right default for part markings.
2. **Upload it** into the directory `system/tessdata-dir` names: on ctrlX OS over the ctrlX file interface, on a PC by copying the file there.
3. **Use it**: set `read-text/input/language` to the file name without `.traineddata`.

```
upload deu.traineddata  ->  <tessdata-dir>/deu.traineddata
write  module-1/read-text/input/language = "deu"
write  module-1/read-text/execute
```

### There is no code to look up

**The file name is the language code.** Tesseract requires the model for language `deu` to be called `deu.traineddata`, so whatever you upload defines what you type. You never have to find a code in a table, and you must never rename the file.

> [!WARNING]
> **The codes are not all three letters.** Chinese is `chi_sim` (simplified) or `chi_tra` (traditional), and there are others such as `srp_latn` and `aze_cyrl`. Take the name exactly as the Tesseract project publishes it.

| Language | File to upload | `input/language` |
| --- | --- | --- |
| German | `deu.traineddata` | `deu` |
| French | `fra.traineddata` | `fra` |
| Chinese, simplified | `chi_sim.traineddata` | `chi_sim` |
| Japanese | `jpn.traineddata` | `jpn` |

### No restart, and no refresh either

A model you upload is usable **on the very next `read-text`**. The reader looks the file up on disk each time it runs, so a new one is found without anything being refreshed or restarted.

> [!NOTE]
> This holds for **replacing a model that is already in use** too. The recognition engine is built once per module and kept, because building it means loading the model - 23 MB for English - and doing that on every read made a small read three times slower than it needed to be. It is rebuilt whenever the language, the directory, the character set, the engine or the layout changes, and whenever the model file itself changed: before every read the reader checks the file’s size and time, and when either changed it compares the file’s content with the one the engine was built from. So a file overwritten in place, under the same name and directory, is read with on the next `read-text` - no restart.
>
> The check costs a file status per read. A changed model is hashed once, about 40 ms for 23 MB, and then loaded as any new model is; the log says `The '<language>' model in '<directory>' changed on
> disk; loading it again`. A file that cannot be loaded - an upload still in progress, say - makes the read fail with `-7`, and the next read after the upload completes loads it.

`system/languages` is the one thing that does not update on its own - it is a listing, refreshed at startup, by `set-tessdata-dir` and by `refresh-languages`:

```
write  system/refresh-languages/execute
read   system/languages           -> [chi_sim, deu, eng, osd]
```

Call it to make the listing agree with the directory. You do **not** need it in order to read.

## Several languages at once

`input/language` is a per-module input, so each module reads in its own language from the same shared set of models:

```
module-1/read-text/input/language = "eng"
module-2/read-text/input/language = "deu"
module-3/read-text/input/language = "chi_sim"
```

Each module reports what it actually used in its own `output/result.language`. Nothing is shared between them but the models on disk.

## Using a different upload directory

`set-tessdata-dir` points the **first** search directory somewhere else. On ctrlX OS a USB stick, say, since the application has the `removable-media` interface:

```
write  system/set-tessdata-dir/input/tessdata-dir = "/media/usb/tessdata"
```

On Windows a folder on another drive, say:

```
write  system/set-tessdata-dir/input/tessdata-dir = "D:\models\tessdata"
```

Then, on either:

```
write  system/set-tessdata-dir/execute
read   system/set-tessdata-dir/output/status-code   -> 1
read   system/languages                             -> [deu, eng, fra, osd]
```

The shipped models stay available, so an empty directory is accepted - a reasonable thing to point at just before uploading into it. The choice is saved and survives a restart.

> [!NOTE]
> Unlike every other path this application takes, an absolute path outside the configuration directory is accepted here. Language models are large third-party files that are legitimately kept where they are - on ctrlX OS outside the ctrlX solution - rather than copied into the configuration directory. The application only ever lists and reads them.

## osd has to sit beside the language

Tesseract resolves **one** directory per run, so the `AUTO_OSD` and `SPARSE_OSD` layouts need `osd.traineddata` in the *same* directory as the language model - not merely somewhere on the search path. Upload `deu.traineddata` on its own, pick `AUTO_OSD`, and the run is refused saying exactly that:

```
error: invalid input: layout AUTO_OSD also needs osd.traineddata beside 'deu.traineddata' in
'<tessdata-dir>'; copy it from '<shipped dir>', or choose a layout that does not detect orientation
```

Either upload `osd.traineddata` alongside your model, or use a layout that does not detect orientation. The default, `SINGLE_BLOCK`, does not - this only comes up if you asked for orientation detection.

## When set-tessdata-dir refuses

It validates before it publishes anything, so a rejected call leaves `tessdata-dir`, `tessdata-dirs` and `languages` exactly as they were - you never see a directory and a listing that disagree.

| Situation | `status-code` |
| --- | --- |
| `input/tessdata-dir` is empty | `-2` invalid input |
| The path is not a directory | `-4` source not found |
| Switched, but the choice could not be saved | `-6` write failed - **the switch is live**, only persistence failed |

An empty directory is **not** refused. It was, back when this was the only place models were read from and an empty one meant every read silently fell back to English. The shipped models are always searched now.

## Getting the language wrong

`read-text` validates the language **before** it runs and refuses an uninstalled one with `status-code -2`, naming every directory searched and what is available:

```
error: invalid input: language 'fra' has no fra.traineddata in any model directory
('<tessdata-dir>', '<shipped dir>'); available: eng, osd. Upload fra.traineddata to
'<tessdata-dir>' - the file name is the language code
```

If a model disappears between that check and the read - someone pulled the USB stick - `output/result.language` reports what was **actually** used, and `status-text` says so:

```
ok: 4 word(s), mean score 0.91 (eng, SINGLE_BLOCK) - NOTE: language 'deu' is not installed, read with 'eng' instead
```

> [!IMPORTANT]
> Read `output/result.language`, not `input/language`, when you need to know how a result was produced. They are usually the same. When they differ, only the output is telling the truth.

# Module Management – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/module-management.html

A **module** is one configured instance of the application. It owns the reading operation, with its own settings and results:

```
hdvisionsystems/lumiscan-text-reader/<module-id>/read-text/...
```

Modules can be created and removed while the application runs. A fresh installation serves one module, named `module-1`.

> [!IMPORTANT]
> **On ctrlX OS, one licence covers ten modules.** The eleventh module acquires a second licence of the same key, and a module is refused with status `-11` when no licence on the device covers it.

The management node sits outside the modules:

```
hdvisionsystems/lumiscan-text-reader/system/
```

## Nodes

| Node | Type | Description |
| --- | --- | --- |
| `modules` | string array | The modules that currently exist |
| `create-module/execute` | method | Create a module and its node subtree |
| `create-module/input/module-id` | string | Name of the module to create. Lower case letters, digits, `-` and `_` only; must not be `system` and must not already exist |
| `remove-module/execute` | method | Remove a module and its node subtree |
| `remove-module/input/module-id` | string | Name of the module to remove |
| `save-current-modules/execute` | method | Persist the module list, so the same modules are recreated on the next start |
| `save-module-inputs/execute` | method | Persist one module’s settings |
| `save-module-inputs/input/module-id` | string | Name of the module whose settings to save |

Every method also has `output/status-code` and `output/status-text`. How these modules compare with those of the other LumiScan apps is described in [Settings, Modules and Restart](https://docs.hdvisionsystems.com/en/lumiscan-keryx/settings-and-modules.html#registry-apps).

## Creating a module

1. Set `system/create-module/input/module-id` to the new name
2. Write to `system/create-module/execute`
3. Check `system/create-module/output/status-code` is `1`

The module is live at once, with default settings, and appears in `system/modules`. It is **not** permanent until the module list is saved.

Removing a module is the mirror image, through `remove-module`. A module is refused with `-1` (busy) while one of its own operations is running, rather than having its nodes destroyed mid-execution.

> [!NOTE]
> A module created now is a **new** module: it starts with the default input values, even if a module of that name existed before and its settings were saved. Those settings stay on disk until the removal is made permanent - `save-current-modules` deletes the saved inputs of every module the new list no longer names, because until the list is written a removal is not permanent and a restart brings the module back with its settings intact.

## Saving

Two things are saved separately, and both must be saved to survive a restart:

- **the module list** - `save-current-modules`, so the same modules are recreated;
- **each module’s settings** - `save-module-inputs`, once per module.

To save a module’s settings:

1. Set `system/save-module-inputs/input/module-id`
2. Write to `system/save-module-inputs/execute`
3. Check `system/save-module-inputs/output/status-code` is `1`

Every input of that module is written to the application configuration directory and restored on the next start. Results are not saved - they are produced by the next run.

A setting whose node no longer exists after a software update is skipped with a log entry, so an older configuration never prevents the application from starting.

## Why the address contains a module even with one module

The `<module-id>` level is always present, so addresses do not change when a second module is added: a program written today against `module-1` keeps working. It also matches the path convention the other LumiScan applications document.

## What is shared between modules

Modules are independent in everything except the language models: each has its own camera, its own reading settings, its own recognition engine and its own results. The models are application-wide - `system/tessdata-dir` names one upload directory for the whole application, and every module reads from the same set, each in its own `input/language`. See [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html).

# Data Layer Reference – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/datalayer-reference.html

Every address below is relative to

```
hdvisionsystems/lumiscan-text-reader/
```

## Data Layer {#data-layer}

LumiScan Text Reader offers its functions on the Data Layer under `hdvisionsystems/lumiscan-text-reader`.

On ctrlX OS (ctrlX CORE or ctrlX IPC), LumiScan Text Reader uses the [ctrlX Data Layer](https://docs.hdvisionsystems.com/en/lumiscan-keryx/ctrlx-data-layer.html).

On a PC with Windows 11 or Ubuntu 24.04, LumiScan Text Reader uses [LumiScan Keryx](https://docs.hdvisionsystems.com/en/lumiscan-keryx/keryx.html).

The node addresses, types and call patterns are the same on both. LumiScan Text Reader offers:

- [execute methods](https://docs.hdvisionsystems.com/en/lumiscan-keryx/execute.html)
- a [stream](https://docs.hdvisionsystems.com/en/lumiscan-keryx/streams.html) subscription to one source (experimental)

How addresses are built, how to call functions, the status codes and the tools are described in [LumiScan Keryx](https://docs.hdvisionsystems.com/en/lumiscan-keryx/index.html).

## Application-wide nodes

| Address | Type | Access | Description |
| --- | --- | --- | --- |
| `system/modules` | string[] | read | The modules that exist right now |
| `system/create-module/execute` | method | write | Add a module |
| `system/create-module/input/module-id` | string | read/write | Name for the new module |
| `system/remove-module/execute` | method | write | Remove a module |
| `system/remove-module/input/module-id` | string | read/write | Module to remove |
| `system/save-current-modules/execute` | method | write | Persist the module list |
| `system/save-module-inputs/execute` | method | write | Persist one module’s settings |
| `system/save-module-inputs/input/module-id` | string | read/write | Module to save |
| `system/tessdata-dir` | string | read | Where to upload your own models; the first directory searched |
| `system/tessdata-dirs` | string[] | read | Every directory searched, in order |
| `system/languages` | string[] | read | Language codes found across all of them |
| `system/set-tessdata-dir/execute` | method | write | Use another upload directory |
| `system/set-tessdata-dir/input/tessdata-dir` | string | read/write | Directory to upload to and search first |
| `system/refresh-languages/execute` | method | write | Re-read the directories and update `languages` |
| `system/meta/subscription-schema` | string | read | What the node a module subscribes to must hold, as a JSON Schema (draft-04) of its JSON form: here a `stream_image`, the `frame` node of the stream folder, with `lineage` and `image`. The same for every module. Experimental |

Every method also serves `output/status-code` and `output/status-text`.

`system/tessdata-dir`, `system/tessdata-dirs` and `system/languages` are read-only outputs of `set-tessdata-dir` and `refresh-languages`, so the listing can never describe a directory that is not in use. Note that `languages` is a listing only: reading with a newly uploaded model does not wait for a refresh. See [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html).

## Per-module nodes

All addresses below are under `<module-id>/read-text/`.

| Address | Type | Default | Description |
| --- | --- | --- | --- |
| `execute` | method | - | Run one reading |
| `input/input-image-address` | string | (empty) | Address of the image node to read |
| `input/crop/startX` | uint16 | 0 | Left edge of the region to read |
| `input/crop/startY` | uint16 | 0 | Top edge |
| `input/crop/width` | uint16 | 0 | Width; 0 reaches the right edge |
| `input/crop/height` | uint16 | 0 | Height; 0 reaches the bottom edge |
| `input/language` | string | `eng` | A code from `system/languages` |
| `input/engine` | string | `DEFAULT` | `DEFAULT`, `LSTM`, `LEGACY` or `COMBINED` |
| `input/layout` | string | `SINGLE_BLOCK` | See the table below |
| `input/preprocess` | string | `NONE` | `NONE`, `GRAYSCALE` or `BINARIZE_OTSU` |
| `input/characters/mode` | string | `ALL_SYMBOLS` | `ALL_SYMBOLS`, `ALL_LETTERS`, `DIGITS`, `LOWERCASE_LETTERS`, `UPPERCASE_LETTERS`, `SPECIAL_SYMBOLS` or `CUSTOM` |
| `input/characters/allow` | string | (empty) | Characters added to the set |
| `input/characters/deny` | string | (empty) | Characters removed from the set |
| `input/min-score` | float64 | 0.0 | Confidence a word must reach to be reported, 0 to 1 |
| `output/result` | flatbuffers | empty | `text_read_result` - the reading |
| `output/status-code` | int16 | 0 | Outcome as a number; negative is always an error |
| `output/status-text` | string | (empty) | Outcome in words |

### Accepted `input/layout` values

|   |   |
| --- | --- |
| `SINGLE_BLOCK` | One uniform block of text - the default |
| `SINGLE_LINE` | Exactly one line |
| `SINGLE_WORD` | Exactly one word |
| `SINGLE_CHAR` | Exactly one character |
| `RAW_LINE` | One line, with no structure analysis |
| `AUTO` | Find and segment text regions across the whole image |
| `AUTO_OSD` | As `AUTO`, and detect the orientation and script |
| `SPARSE` | Find text wherever it appears, assuming no layout |
| `SPARSE_OSD` | As `SPARSE`, and detect the orientation |
| `SINGLE_COLUMN` | A single column of text |
| `SINGLE_BLOCK_VERT` | A single block of vertically aligned text |
| `CIRCLE_WORD` | A single word arranged in a circle |

## Per-module image stream nodes (experimental)

All addresses below are under `<module-id>/`, beside `read-text/`. The stream reads each frame with the inputs of `read-text`, all but `input/input-image-address`. See [Reading Text on an Image Stream](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html).

| Address | Type | Default | Description |
| --- | --- | --- | --- |
| `control/subscribe/input/enable` | bool | as applied | Stream mode: `true` runs the stream, `false` stops it |
| `control/subscribe/input/address` | string | as applied | The one source, a camera’s stream folder, e.g. `hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream`. Another address replaces it |
| `control/subscribe/input/tag` | string | as applied | The tag on the module’s results: the module’s step in the lineage of each stream result carries it. Empty: the module id, e.g. `module-1`. A new tag keeps the source; the next result carries it |
| `control/subscribe/input/queue-size` | uint32 | as applied | Frames that may wait while one is read, 1-64 |
| `control/subscribe/input/keep-latest-output` | bool | as applied | `true` also writes each stream result to `read-text/output/result` |
| `control/subscribe/execute` | method | - | Check the inputs of `control/subscribe` together, save them in `<module-id>.json` as `stream`, then apply them. Writing an input alone changes nothing |
| `control/unsubscribe/execute` | method | - | Drop the source named by `control/unsubscribe/input/address`, which ends stream mode; the tag stays. Saved in `<module-id>.json` like a subscribe |
| `control/unsubscribe/input/address` | string | (empty) | The source to drop, as `setting/stream/address` shows it |
| `setting/stream/{enable, address, tag, queue-size, keep-latest-output}` | as the inputs | `stream` in `<module-id>.json` | What `control/subscribe/execute` or `control/unsubscribe/execute` applied last. Read-only |
| `setting/settings-hash` | string | - | Hash of what the results are made with, 16 hex digits. Read-only |
| `control/subscribe/output/stream/active` | bool | `false` | `true` while the stream runs |
| `control/subscribe/output/stream/result` | flatbuffers | empty | `stream_texts` - one per frame read. Subscribe to receive each one |
| `control/subscribe/output/stream/last-failure` | string | (empty) | Why the last frame that could not be read failed, naming the source by its address |
| `control/subscribe/output/stream/statistics/elapsed-ms` | uint64 | `0` | Milliseconds since the stream started |
| `control/subscribe/output/stream/statistics/received-frames` | uint64 | `0` | Frames the subscription delivered |
| `control/subscribe/output/stream/statistics/processed-frames` | uint64 | `0` | Frames read, each with a result |
| `control/subscribe/output/stream/statistics/skipped-frames` | uint64 | `0` | Frames pushed out of the queue, or waiting when the source was replaced or dropped |
| `control/subscribe/output/stream/statistics/failed-frames` | uint64 | `0` | Frames that could not be read |
| `control/subscribe/output/stream/statistics/lost-frames` | uint64 | `0` | Frames lost before they arrived |
| `control/subscribe/output/stream/statistics/processing-us` | uint64 | `0` | Reading time of all frames read, summed, microseconds |

The inputs of `control/subscribe` start with what is applied, and only `control/subscribe/execute` applies them. `control/subscribe/execute` is refused for stream mode without an address or a queue size outside 1-64, `control/unsubscribe/execute` for an address that is not the source. When `<module-id>.json` cannot be written, both answer `DL_CREATION_FAILED` (on a PC `CreationFailed`) and change nothing.

## The result flatbuffer

`output/result` is registered as `types/hdvisionsystems/lumiscan-text-reader/text_read_result`.

| Field | Type | Description |
| --- | --- | --- |
| `text` | string | The whole reading, lines separated by newlines. **Not filtered by `min_score`** |
| `words[].text` | string | One recognised word |
| `words[].score` | float | Its confidence, 0 to 1 |
| `words[].bbox_x` | int | Box around the word, in the coordinates of the region that was read |
| `words[].bbox_y` | int |   |
| `words[].bbox_width` | int |   |
| `words[].bbox_height` | int |   |
| `word_count` | uint | How many words are reported |
| `mean_score` | float | Average confidence over the reported words; 0 when there are none |
| `min_score` | float | The threshold that was applied |
| `language` | string | The language **actually** used |
| `engine` | string | The engine actually used |
| `layout` | string | The layout actually used |
| `image_width` | uint | The region that was read |
| `image_height` | uint |   |
| `timestamp` | ulong | Microseconds since the epoch, when the reading finished |

`control/subscribe/output/stream/result` is registered as `types/hdvisionsystems/lumiscan-text-reader/stream_texts`: a `hdv.stream.StreamTexts` with `lineage`, where the frame came from, and `result`, the table above.

## Starting from an Example (PC) {#generate-settings}

On a PC, the Text Reader writes an example of its settings into a folder of your choice, and ends without starting. On Windows:

```shell
lumiscan-text-read --generate-settings C:\work\text-reader-settings
```

Without a folder (`-g` alone) it writes into the current folder. The example is the application settings file `lumiscan-text-reader.json`, which names the module `module-1`, and the module file `module-1.json` beside it: every input of `read-text` with the value it has by default, except the image address, which is the Cam Driver’s `camera1`. Beside them, in `schemas/`, are the schemas the two files name in `"$schema"`: `lumiscan-text-reader.schema.json`, `lumiscan-text-reader-module.schema.json`, and `stream.schema.json` for the `stream` section of a module file. They describe every key the Text Reader reads - each input of `read-text` with its type, its default and the range a run accepts - so an editor that understands JSON Schema can check the files and show what each value means. Copy the `schemas` folder along with the files: `system/save-module-inputs` keeps the `"$schema"` of a module file it saves. Existing files are never overwritten: when one of them is in the folder already, nothing is written and the Text Reader says which.

The Text Reader then names the folder it reads its settings from, on Windows by default `C:\ProgramData\HD Vision Systems GmbH\LumiScan Vision Suite\lumiscan-text-read\etc\lumiscan-text-reader`. Edit the example, copy it there, and start the Text Reader. See also [Where the files are](https://docs.hdvisionsystems.com/en/lumiscan-keryx/keryx.html#files).

# How To: Read a Part Marking from a Camera Image – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/how-tos/read-a-part-marking-from-a-camera-image.html

Reading a printed part number off a camera image, from a fresh installation to a setting you can put into production.

The example is a six-digit part number printed on a label. Adapt the character set and the layout to your marking as you go.

## Before you start

- LumiScan Cam Driver, or another image source, running on the **same device** - the same ctrlX CORE or ctrlX IPC, or the same PC - and delivering images.
- Its image node address to hand, e.g. `hdvisionsystems/lumiscan-cam-driver/camera1/output/image`.
- A part in front of the camera, with the marking in shot, in focus and well lit.

## 1. Point the reader at the camera

```
write  hdvisionsystems/lumiscan-text-reader/module-1/read-text/input/input-image-address
       = "hdvisionsystems/lumiscan-cam-driver/camera1/output/image"
```

## 2. Take a first reading

Leave everything else at its default and run it:

```
write  .../module-1/read-text/execute
read   .../module-1/read-text/output/status-code   -> 1
read   .../module-1/read-text/output/status-text
read   .../module-1/read-text/output/result
```

A negative status code means it did not run - `status-text` says why. `-3` is almost always a wrong image address.

At this point the reading is usually poor: too many words, some of them nonsense picked out of the background. That is expected, and the next three steps fix it.

## 3. Crop to the marking

Find the pixel coordinates of the label in the image - on ctrlX OS the ctrlX camera view will show them - and set a region a little larger than the text:

```
write  .../input/crop/startX = 620
write  .../input/crop/startY = 310
write  .../input/crop/width  = 260
write  .../input/crop/height = 90
```

Run it again. Most of the spurious words should be gone.

> [!TIP]
> This is the step that helps most, and it also makes every reading faster. If the part can shift between cycles, make the crop large enough to cover the whole range it can occupy - a marking that drifts outside the crop reads as **nothing**, which is a better failure than reading as something wrong.

## 4. Restrict the character set

A six-digit part number contains only digits, so say so:

```
write  .../input/characters/mode = "DIGITS"
```

This is what stops `0` being read as `O` and `5` as `S`. If the number also contains letters, use `UPPERCASE_LETTERS` with `allow = "0123456789"` instead, or `CUSTOM` with `allow` set to exactly the characters your marking can contain.

## 5. Tell it what it is looking at

One line of print in a tight crop:

```
write  .../input/layout = "SINGLE_LINE"
```

Run it again. `output/result.text` should now be the part number and nothing else.

## 6. Choose a confidence threshold

Run five or ten known-good parts and note `mean_score` from each result, and the individual `words[].score`.

Correct readings on a decent image score well above `0.8`. Set the threshold below your worst known-good reading and above the noise:

```
write  .../input/min-score = 0.7
```

> [!NOTE]
> `min-score` filters `words[]` only; `text` is always the engine’s full output. Check `word_count` rather than `text` when you need to know whether the reading was good.

## 7. Save the settings

Nothing above survives a restart until you save it:

```
write  hdvisionsystems/lumiscan-text-reader/system/save-module-inputs/input/module-id = "module-1"
write  hdvisionsystems/lumiscan-text-reader/system/save-module-inputs/execute
```

## 8. Wire it into the control program

Per cycle:

1. Write `read-text/execute` - a blocking call, so there is no ready flag to poll.
2. Check `output/status-code`. Negative means the reading failed and produced nothing.
3. Read `output/result`.
4. Check `word_count` is 1 and `words[0].score` is acceptable, then compare `words[0].text` against the expected part number.

Read the result **before** triggering the next reading: the next `execute` clears it.

## Troubleshooting

The problems you can meet while setting up a reading are described step by step in [Troubleshooting](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html):

- [Status code -3 (image cannot be read)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-3)
- [Status code -2 (a setting is unusable)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-2)
- [Status code -7 (OCR engine could not start)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-7)
- [The reading is empty although the text is clearly visible in a colour image](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-empty-colour-image)
- [Status code 1, but no words](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-empty-reading)
- [Digits are read as letters (O for 0, S for 5)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-digits-letters)
- [The reading contains extra words from the background](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-extra-words)
- [The text is read in another language than input/language](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#ts-wrong-language)
- [Reading is too slow](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-reading-too-slow)
- [Dot-matrix or laser-etched marking reads badly](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-dot-matrix)

# FAQ – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/faq.html

Short answers to common questions about LumiScan Text Reader. Each entry links to the page with the details. Problems and error messages are described in [Troubleshooting](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html).

> [!TIP]
> Each entry has a fixed anchor (for example `#faq-read-text`). To find an error message, search the [Troubleshooting](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html) page for it.

## Requirements and setup

### Which image source and pixel formats does the Text Reader need? {#faq-image-source}

- **Answer:** The application reads the image node of another application that runs on the **same device** - the same ctrlX CORE or ctrlX IPC, or the same PC (remote Data Layer access is not supported), normally LumiScan Cam Driver, for example `hdvisionsystems/lumiscan-cam-driver/camera1/output/image`. The node must provide `data` (raw, uncompressed uint8 pixel data), `width`, `height` and `pixel-type`. Supported pixel formats are `Mono8`, `Mono12`, `RGB8`, `RGB12`, `BGR8`, `BGR12`, `BayerGR8`, `BayerGR12`, `BayerRG8` and `BayerRG12`; Bayer images are converted to colour and 12-bit images scaled down to 8 bit before reading. Any other pixel format is rejected with status code `-3`.
- **More:** [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html)

### What image quality does OCR need? {#faq-image-quality}

- **Answer:** No setting recovers text the camera did not capture. As a rule of thumb:
  - characters at least 20 pixels tall, and 30 or more for small or noisy print;
  - good contrast between the marking and its background;
  - text in focus and not glared out by the lighting;
  - text roughly upright, unless you use an orientation-detecting layout.
- **More:** [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html)

## Reading text

### How do I take a reading and evaluate the result? {#faq-read-text}

- **Answer:** Set `<module>/read-text/input/input-image-address`, write to `<module>/read-text/execute` (a blocking call) and check `output/status-code`. On `1`, read `output/result`: `text` is the whole reading as the engine laid it out and is **not** filtered by `min-score`; `words[]` holds each word with its `score` and box, filtered by `min-score`; `word_count`, `mean_score` and the `language`, `engine` and `layout` actually used are also reported. In production, check `word_count` and `words[0].score` before comparing `words[0].text` with the expected value.
- **More:** [Reading Text](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-text.html), [How To: Read a Part Marking from a Camera Image](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/how-tos/read-a-part-marking-from-a-camera-image.html)

## Troubleshooting {#troubleshooting}

Problems and error messages are described step by step in [Troubleshooting](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html):

- [Status code 1, but no words](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-empty-reading)
- [The reading is empty although the text is clearly visible in a colour image](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-empty-colour-image)
- [Digits are read as letters (O for 0, S for 5)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-digits-letters)
- [The reading contains extra words from the background](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-extra-words)
- [Dot-matrix or laser-etched marking reads badly](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-dot-matrix)
- [Reading is too slow](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-reading-too-slow)
- [Status code -2 (a setting is unusable)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-2)
- [Status code -3 (image cannot be read)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-3)
- [Status code -7 (OCR engine could not start)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-7)
- [A reading with AUTO_OSD or SPARSE_OSD is refused ("also needs osd.traineddata beside …")](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-osd-beside-language)
- [An uploaded model does not appear on system/languages](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-languages-list)
- [The text is read in another language than input/language](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#ts-wrong-language)
- [read-text answers -1 (busy) while the stream runs](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-stream-busy)
- [A stream frame fails with "the frame was read with 'eng' instead and its result dropped"](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-stream-language-dropped)
- [My settings or modules are gone after a restart](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-settings-lost)

## Language models

### How do I read another language? {#faq-add-language}

- **Answer:** English (`eng`) and `osd` ship with the application.
  - Download the `.traineddata` file from `github.com/tesseract-ocr/tessdata_fast` (smaller and quicker, the right default for part markings) or `tessdata_best`.
  - Upload it, without renaming, into the directory named on `system/tessdata-dir`: on ctrlX OS over the ctrlX file interface, on a PC by copying the file there.
  - Set `<module>/read-text/input/language` to the file name without `.traineddata`, for example `deu`. Codes are not all three letters (for example `chi_sim`).
  - The model is usable on the very next reading, with no restart. Uploading never removes the shipped models.
- **More:** [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html)

### I replaced a model file - does the reader use the new one? {#faq-replaced-model}

- **Answer:** Yes, from the next `read-text` on, without a restart. The recognition engine is built once per module and kept, but before every read the reader checks the model file’s size and time and, when either changed, compares its content with the one the engine was built from. A changed model is loaded again; the log says `The '<language>' model in '<directory>' changed on disk; loading it again`. An image stream also reads the next frame with the model as it is on disk.
- **Cause:** If a read fails with `-7` right after the replacement, the file could not be loaded yet - an upload still in progress, for example.
- **Solution:** Read again once the upload has completed; that read loads the new model.
- **More:** [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html)

## Image stream

### How do I read the text on every frame of a camera stream? {#faq-stream}

- **Answer:** Subscribe a module to the Cam Driver’s stream (experimental): write the camera’s stream folder, for example `hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream`, to `<module>/control/subscribe/input/address`, set `control/subscribe/input/enable` to `true` and write to `control/subscribe/execute`. Each frame is read with the inputs of `read-text`, and one result per frame is published on `control/subscribe/output/stream/result`. To read the latest result as a plain `read-text/output/result`, set `control/subscribe/input/keep-latest-output` to `true`.
- **More:** [Reading Text on an Image Stream](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/processing-an-image-stream.html), [Streams](https://docs.hdvisionsystems.com/en/lumiscan-keryx/streams.html)

## Status codes and errors

### How do I know whether a reading worked, and where is the reason for a failure? {#faq-status-codes}

- **Answer:** Check the sign of `output/status-code`: greater than 0 is success, `0` means the operation has not run since the application started, less than 0 means it failed and produced nothing. The reason is on `output/status-text`. A failed write to `execute` answers only with a status code (over REST, a bare `400`), so read `output/status-text` afterwards; the text is also in the application log: on ctrlX OS in the ctrlX Logbook or `journalctl`, on a PC in the application’s console output. `-1` means the module was busy; nothing was done and the previous reading is left alone. Every other `execute` empties `output/result` as it starts, so read the result before triggering the next reading.
- **More:** [Execute a Method](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execute-a-method.html), [Execution Status](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execution-status.html), [Reading Text](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-text.html)

## Modules, licensing and support

### Which licence do I need, and how many modules does it cover? {#faq-licence}

- **Answer:** One licence covers ten modules; the eleventh module acquires a second licence of the same key, and a module is refused with status `-11` when no licence on the device covers it. Purchase routes:
  - Bought from HD Vision Systems: the licence is on a LumiScan Key (Windows 11 or Ubuntu 24.04). One LumiScan Key is needed per device running the software at the same time; keys can be exchanged between devices.
  - Bought in the ctrlX Store for Windows 11: ctrlX WORKS plus a LumiScan Key.
  - Bought in the ctrlX Store for ctrlX OS (ctrlX CORE or IPC): a ctrlX licence, no key.
  - On ctrlX OS, the application shuts down shortly after starting if the licence is missing; the reason is written to the system log.
- **More:** [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html#licence), [Module Management](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/module-management.html), [Licensing FAQ](https://docs.hdvisionsystems.com/en/lumiscan-licensing/faq.html)

### Where can I find more answers or get support? {#faq-support}

- **Answer:** See the site-wide [LumiScan FAQ](https://docs.hdvisionsystems.com/en/faq.html). Contact HD Vision Systems customer success at [customer.success@hdvisionsystems.com](mailto:customer.success@hdvisionsystems.com) or +49 6221 6721905; for licences, [licenses@hdvisionsystems.com](mailto:licenses@hdvisionsystems.com).
- **More:** [Licensing FAQ](https://docs.hdvisionsystems.com/en/lumiscan-licensing/faq.html)

# Troubleshooting – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html

This page starts with the general checks that apply to every problem. The problems follow, grouped into reading results, status codes, language models, the image stream, and modules and settings. Do the general checks first, then find your symptom.

All addresses are relative to `hdvisionsystems/lumiscan-text-reader/`.

## General checks {#general-checks}

- Read `output/status-code` and `output/status-text` of the operation, for a reading `<module>/read-text/output/status-code` and `<module>/read-text/output/status-text`. The sign of the status code tells you whether it worked: greater than 0 is success, `0` means the operation has not run since the application started, less than 0 means it failed and produced nothing. The codes are listed in [Execution Status](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/execution-status.html).
- A failed write to `execute` answers with a status code only (over REST, a bare `400`). Read `output/status-text` afterwards: it starts with `ok:` or `error:` and gives the reason.
- Read the application log. On ctrlX OS, read it in the ctrlX Logbook or with `journalctl`; on a PC, in the application’s console output. The log has the same text as `output/status-text`; when several inputs are wrong, each one gets its own warning line.
- Check what the reading actually used: `language`, `engine` and `layout` in `output/result`. They are not always the inputs you set.
- Check that the image source runs on the **same device** as the Text Reader - the same ctrlX CORE or ctrlX IPC, or the same PC. Remote Data Layer access is not supported.
- Check the image quality. No setting recovers text the camera did not capture: characters at least 20 pixels tall (30 or more for small or noisy print), good contrast, text in focus and not glared out, text roughly upright unless you use an orientation-detecting layout. See [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html).
- Check the licence. On ctrlX OS, the application shuts down shortly after starting if the licence is missing; the reason is written to the system log. A module that no licence on the device covers is refused with status `-11`. See [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html#licence).
- Problems that all LumiScan apps share - an app or module missing from the tree, a node that is "not found", a method call that is refused, a stream that delivers nothing - are described in [Troubleshooting](https://docs.hdvisionsystems.com/en/lumiscan-keryx/troubleshooting.html) of the LumiScan Keryx manual.

## Reading results

### Problem: Status code 1, but no words {#faq-empty-reading}

**Symptoms**

`output/status-code` is `1` (OK), but `output/result` has `word_count` `0`.

**Possible causes**

- Nothing readable is in the image: an empty carrier, a part turned the wrong way, or a label out of frame. This is the normal answer, not an error.
- The marking is outside the crop. A marking that drifts outside the crop reads as nothing.
- The marking is not in focus, or not the right way up.
- `input/min-score` filters out every word. `words[]` is filtered by `min-score`; `text` is not.
- A colour image in which a photograph or other colourful scenery shares the frame with the text, see [Problem: The reading is empty although the text is clearly visible in a colour image](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-empty-colour-image).

**Diagnostic procedure**

1. Check `text` in `output/result`. If `text` is not empty but `word_count` is `0`, the words were filtered: compare the word scores with `min_score` in the result, which is the threshold that was applied.
2. Check that the marking is inside `input/crop/startX`, `input/crop/startY`, `input/crop/width` and `input/crop/height`. `image_width` and `image_height` in the result show the region that was read.
3. Check in the camera image that the marking is in focus and the right way up.
4. If the image is a colour image and you can see the text, continue with [Problem: The reading is empty although the text is clearly visible in a colour image](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-empty-colour-image).

**Resolution**

- **If there is no text in the image:** this is a valid result. Branch on `word_count`, not on the status code, to decide whether a part carried text.
- **If the marking is outside the crop:** make the crop large enough to cover the whole range the part can occupy, then read again.
- **If the marking is out of focus or not the right way up:** correct the camera set-up. For text whose orientation really varies, use an orientation-detecting layout (`AUTO_OSD` or `SPARSE_OSD`).
- **If `input/min-score` filters out every word:** set the threshold again as described in [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html), step 5.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `output/status-text`, the content of `output/result` and the values of the `read-text/input/` nodes.

### Problem: The reading is empty although the text is clearly visible in a colour image {#faq-empty-colour-image}

**Symptoms**

A colour frame in which a photograph or other colourful scenery shares the image with the text comes back with no words at all, while `output/status-code` reports success (`1`). The text is not garbled - the reading is empty.

**Possible causes**

- The engine reduces a colour image before reading it, and with colourful scenery in the frame this loses the text. The size of the text is not the cause. No `input/layout` and no `input/engine` rescues this case.

**Diagnostic procedure**

1. Check that `pixel-type` of the image node is a colour format (for example `RGB8` or `BGR8`).
2. Check `input/preprocess`. Its default is `NONE`.
3. Check whether a rectangle can separate the text from the scenery.

**Resolution**

- **If a rectangle can separate the text from the scenery:** crop to the text first with `input/crop`. Cropping fixes this case in colour.
- **If the text and the scenery cannot be separated by a rectangle** (for example a label on a patterned part, or a screen photographed whole):
  1. Set `input/preprocess` to `GRAYSCALE` and read again.
  2. Only if `GRAYSCALE` did not help, try `BINARIZE_OTSU`.

Neither setting invents text: on a frame with no text in it, all three settings return nothing.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `output/status-text`, the values of `input/crop` and `input/preprocess`, and the pixel format of the image.

### Problem: Digits are read as letters (O for 0, S for 5) {#faq-digits-letters}

**Symptoms**

Characters are substituted between character classes, for example `O` for `0` or `S` for `5`. Other typical substitutions are `I`/`1`/`l`, `B`/`8` and `Z`/`2`.

**Possible causes**

- An unfamiliar character is substituted rather than rejected, and the default `input/characters/mode` `ALL_SYMBOLS` permits every class.

**Diagnostic procedure**

1. Read `input/characters/mode`, `input/characters/allow` and `input/characters/deny`. Expected for a restricted marking: a mode other than `ALL_SYMBOLS`.

**Resolution**

- **If the character set is not restricted:** set `input/characters/mode` to match the marking:
  - numeric part or batch number: `DIGITS`;
  - upper-case alphanumeric code: `UPPERCASE_LETTERS` with `input/characters/allow` = `0123456789`;
  - date code with separators: `DIGITS` with `allow` = `/-.`;
  - fixed alphabet: `CUSTOM` with `allow` set to exactly that alphabet.

An empty character set - `CUSTOM` with an empty `allow`, or a `deny` that removes everything the mode selected - is refused with status code `-2`.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the values of `input/characters/mode`, `allow` and `deny`, and `output/result`.

### Problem: The reading contains extra words from the background {#faq-extra-words}

**Symptoms**

`output/result` contains words that are not part of the marking, picked out of the background - edges of the part, shadows, the conveyor. These words usually have a low `words[].score`.

**Possible causes**

- `input/crop` is loose or not set. All zero reads the whole image.
- `input/layout` looks for more structure than the marking has.
- `input/min-score` is `0` (the default), which reports everything.

**Diagnostic procedure**

1. Check `input/crop`, and `image_width` and `image_height` in the result: they show the region that was read.
2. Check `layout` in the result.
3. Check the `words[].score` of the extra words and `min_score` in the result. Spurious words usually sit below 0.5.

**Resolution**

- **If the crop is loose:** tighten `input/crop` around where the text will be. This is the most effective setting, and it also makes reading faster.
- **If the layout does not match:** set `input/layout` to the marking, for example `SINGLE_LINE` for one line of print.
- **If `input/min-score` is too low:**
  1. Run known-good parts with `min-score` at `0`.
  2. Note `mean_score` and the individual word scores in the result.
  3. Set `input/min-score` between the noise (usually below 0.5) and the correct readings (usually well above 0.8), closer to the correct readings.

`min-score` filters `words[]` only; `text` is always the engine’s full output. Check `word_count` rather than `text` when you need to know whether the reading was good.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `output/result` and the values of `input/crop`, `input/layout` and `input/min-score`.

### Problem: Dot-matrix or laser-etched marking reads badly {#faq-dot-matrix}

**Symptoms**

A dot-matrix or laser-etched marking is read badly, while printed text reads well.

**Possible causes**

- These markings defeat the `LSTM` engine more often than printed text.
- Lighting, contrast or focus are poor. No setting fixes them.

**Diagnostic procedure**

1. Check `engine` in the result: the engine that was actually used.
2. Check the image against the image quality rules in [Requirements](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html): character height, contrast, focus and glare.

**Resolution**

- **If the engine is the cause:** set `input/engine` to `LEGACY` and compare the readings.
- **If the image quality is poor:** fix the lighting and the focus.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `output/result` and the value of `input/engine`.

### Problem: Reading is too slow {#faq-reading-too-slow}

**Symptoms**

The call to `read-text/execute` takes too long for your cycle. Reading is synchronous: the call returns when the read has finished.

**Possible causes**

- The engine is given too much image. Reading time is driven mostly by how much image the engine is given.
- The `AUTO_OSD` or `SPARSE_OSD` layout is used. These layouts load a second model and work out the orientation first.

**Diagnostic procedure**

1. Check `input/crop`, and `image_width` and `image_height` in the result.
2. Check `layout` in the result.

**Resolution**

- **If the engine is given too much image:** tighten `input/crop` around the label. A tight crop is both faster and more accurate than the whole frame.
- **If an `_OSD` layout is used:** use a layout without orientation detection, unless the orientation really varies.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the values of `input/crop` and `input/layout` and the image size.

## Status codes

### Problem: Status code -2 (a setting is unusable) {#faq-status-minus-2}

**Symptoms**

`output/status-code` is `-2` (Invalid input). `output/status-text` starts with `error:` and names every offending node and what it expected, for example `min-score must be between 0 and 1, got 1.5` or `error: crop/startX + crop/width (100 + 900 = 1000) exceeds the image width 640`.

**Possible causes**

- A required node is left empty.
- An unknown value in `input/engine`, `input/layout`, `input/preprocess` or `input/characters/mode`.
- A value out of range, for example `input/min-score` outside 0 to 1.
- An empty character set: `CUSTOM` with an empty `allow`, or a `deny` that removes everything the mode selected.
- A language with no model installed. The message names every directory searched and what is available, for example `error: invalid input: language 'fra' has no fra.traineddata in any model directory ('<tessdata-dir>', '<shipped dir>'); available: eng, osd. Upload fra.traineddata to '<tessdata-dir>' - the file name is the language code`.
- A layout with orientation detection without `osd.traineddata` beside the language model, see [Problem: A reading with AUTO_OSD or SPARSE_OSD is refused ("also needs osd.traineddata beside …")](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-osd-beside-language).
- A crop that does not fit the image. It is refused, not trimmed to fit.
- An image address that points at this application itself.
- For `system/set-tessdata-dir`: `input/tessdata-dir` is empty.

**Diagnostic procedure**

1. Read `output/status-text`. A run checks every setting and reports **all** the problems at once.
2. Read the application log: each wrong input has its own warning line.

**Resolution**

- **If a value is unknown, out of range or empty:** correct each node named in `output/status-text`. The accepted values are listed in [Data Layer Reference](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/datalayer-reference.html).
- **If the character set is empty:** set `input/characters/allow`, or remove characters from `input/characters/deny`, so that the set is not empty.
- **If the language has no model:** upload `<language>.traineddata` into the directory `system/tessdata-dir` names, without renaming it, or set `input/language` to a code on `system/languages`. See [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html).
- **If the crop does not fit:** change `input/crop` so that `startX` + `width` and `startY` + `height` are inside the image.
- **If the image address points at this application:** set `input/input-image-address` to the image node of the camera application.
- **If `input/tessdata-dir` is empty:** write a directory to `system/set-tessdata-dir/input/tessdata-dir`, then write to `system/set-tessdata-dir/execute`.

Then run the operation again and check that `output/status-code` is `1`.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the full `output/status-text` and the values of the `read-text/input/` nodes.

### Problem: Status code -3 (image cannot be read) {#faq-status-minus-3}

**Symptoms**

`output/status-code` is `-3` (Image read failed), and the result is empty.

**Possible causes**

- A wrong image address in `input/input-image-address`. This is almost always the cause.
- The source application is not running on the same device, or it has not captured an image.
- The image node is missing, empty or inconsistent.
- The image uses an unsupported pixel format.

**Diagnostic procedure**

1. Check `input/input-image-address`, for example `hdvisionsystems/lumiscan-cam-driver/camera1/output/image`.
2. Check that the image node has the children `data`, `width`, `height` and `pixel-type`, and that `data` holds pixel data.
3. Check that `pixel-type` is one of `Mono8`, `Mono12`, `RGB8`, `RGB12`, `BGR8`, `BGR12`, `BayerGR8`, `BayerGR12`, `BayerRG8` or `BayerRG12`.
4. Check that the source application runs on the same device - the same ctrlX CORE or ctrlX IPC, or the same PC - and delivers images.
5. Read `output/status-text` for the reason.

**Resolution**

- **If the address is wrong:** correct `input/input-image-address` and read again.
- **If the source application is not running or has not captured an image:** start it on the same device, capture an image, then read again.
- **If the image node is empty or inconsistent:** capture a new image with the source application, then read again.
- **If the pixel format is not supported:** use an image source that delivers one of the supported pixel formats.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `input/input-image-address`, `output/status-text` and the `pixel-type`, `width` and `height` of the image node.

### Problem: Status code -7 (OCR engine could not start) {#faq-status-minus-7}

**Symptoms**

`output/status-code` is `-7` (Algorithm failed). The result is empty, and the reason is in `output/status-text`.

**Possible causes**

- The OCR engine could not start, usually because of the language model directory.
- A language model could not be loaded, for example a model whose upload is still in progress. This happens when a read follows right after a model was replaced.

**Diagnostic procedure**

1. Read `output/status-text` for the reason.
2. Check that `system/languages` is not empty. `system/languages` is refreshed only at startup, by `set-tessdata-dir` and by `refresh-languages`: write to `system/refresh-languages/execute` to update it.
3. Check `system/tessdata-dirs`: every directory searched, in order.
4. Check whether a model upload was in progress when the read failed.

**Resolution**

- **If an upload was in progress:** read again once the upload has completed. That read loads the new model.
- **If the reason names the language model directory:** check the directories on `system/tessdata-dirs` against the reason in `output/status-text`. If you changed the upload directory with `system/set-tessdata-dir`, check that it is the directory that holds your models.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the full `output/status-text` and the values of `system/tessdata-dir`, `system/tessdata-dirs` and `system/languages`.

## Language models

### Problem: A reading with AUTO_OSD or SPARSE_OSD is refused ("also needs osd.traineddata beside …") {#faq-osd-beside-language}

**Symptoms**

A reading is refused with `output/status-code` `-2` and the message

```
error: invalid input: layout AUTO_OSD also needs osd.traineddata beside 'deu.traineddata' in '<tessdata-dir>'; copy it from '<shipped dir>', or choose a layout that does not detect orientation
```

**Possible causes**

- The `AUTO_OSD` and `SPARSE_OSD` layouts need `osd.traineddata` in the **same** directory as the language model, not merely somewhere on the search path. Tesseract resolves one directory per run. For example, `deu.traineddata` was uploaded on its own.

**Diagnostic procedure**

1. Check `input/layout`. Expected for this message: `AUTO_OSD` or `SPARSE_OSD`.
2. Check whether the directory named in the message holds `osd.traineddata` beside the language model.

**Resolution**

- **If you need orientation detection:** upload `osd.traineddata` into the same directory as your model. The message names the shipped directory to copy it from. Then read again.
- **If you do not need orientation detection:** use a layout that does not detect orientation, such as the default `SINGLE_BLOCK`.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the full message and the values of `system/tessdata-dirs` and `system/languages`.

### Problem: An uploaded model does not appear on system/languages {#faq-languages-list}

**Symptoms**

You uploaded a `.traineddata` file, but its language code is not listed on `system/languages`.

**Possible causes**

- `system/languages` is a listing. It is refreshed only at startup, by `set-tessdata-dir` and by `refresh-languages`.
- The file is not in the directory `system/tessdata-dir` names, or it was renamed. The file name is the language code.

**Diagnostic procedure**

1. Check that the file is in the directory named on `system/tessdata-dir`.
2. Check that the file name is unchanged, exactly as the Tesseract project publishes it (for example `deu.traineddata`, `chi_sim.traineddata`).

**Resolution**

- **If the listing is not refreshed:** write to `system/refresh-languages/execute`, then read `system/languages`. You do not need this in order to read: an uploaded model is usable on the very next `read-text`.
- **If the file is in another directory or was renamed:** upload it again, without renaming, into the directory `system/tessdata-dir` names.

`osd` appears in the list but is not a language: setting `input/language` to `osd` does not read anything.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the values of `system/tessdata-dir`, `system/tessdata-dirs` and `system/languages` and the name of the uploaded file.

### Problem: The text is read in another language than input/language {#ts-wrong-language}

**Symptoms**

`language` in `output/result` is not the language set in `input/language`, and `output/status-text` contains a `NOTE`, for example

```
ok: 4 word(s), mean score 0.91 (eng, SINGLE_BLOCK) - NOTE: language 'deu' is not installed, read with 'eng' instead
```

**Possible causes**

- The language model disappeared between the language check and the read - someone pulled the USB stick, for example. The engine then reads with `eng`.

**Diagnostic procedure**

1. Compare `language` in `output/result` with `input/language`.
2. Read `output/status-text` for the `NOTE`.
3. Check that the model file is still in its directory (see `system/tessdata-dirs`).

**Resolution**

- **If the model disappeared:** make the model available again in its directory, for example insert the USB stick again or upload the model again. A model you upload is usable on the very next `read-text`. Then read again and check `language` in `output/result`.

Read `language` in `output/result`, not `input/language`, when you need to know how a result was produced. If the language is not installed at all, the reading is refused with `-2`, see [Problem: Status code -2 (a setting is unusable)](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-status-minus-2). For the image stream, see [Problem: A stream frame fails with "the frame was read with 'eng' instead and its result dropped"](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#faq-stream-language-dropped).

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `output/status-text`, `output/result` and the values of `input/language`, `system/tessdata-dirs` and `system/languages`.

## Image stream

### Problem: read-text answers -1 (busy) while the stream runs {#faq-stream-busy}

**Symptoms**

While the image stream (experimental) runs, a call of `read-text/execute` is refused:

- `output/status-code` is `-1` (Busy);
- the write answers `DL_TOO_MANY_OPERATIONS` (ctrlX OS) or `TooManyOperations` (PC);
- nothing is done, and `output/result` keeps its value.

`system/remove-module` also answers busy (`-1`) while a frame of that module is read.

**Possible causes**

- The stream and `read-text` share the module’s one recognition engine and never run at the same time on one module. With a camera streaming, this happens often.

**Diagnostic procedure**

1. Read `control/subscribe/output/stream/active` of the module. `true` means the stream runs.

**Resolution**

- **If you need `read-text` while the stream runs:** call `read-text/execute` again, or stop the stream first:
  1. Set `control/subscribe/input/enable` to `false` and write to `control/subscribe/execute`. The frame being read is finished first.
  2. Check that `control/subscribe/output/stream/active` is `false`.
  3. Call `read-text/execute`.
- **If you want to remove the module:** stop the stream first, then call `system/remove-module/execute`.

Other modules are not affected: each module has its own lock, its own engine and its own stream.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the values of `control/subscribe/output/stream/active` and the counters under `control/subscribe/output/stream/statistics/`.

### Problem: A stream frame fails with "the frame was read with 'eng' instead and its result dropped" {#faq-stream-language-dropped}

**Symptoms**

A frame of the image stream (experimental) gives no result. `control/subscribe/output/stream/statistics/failed-frames` grows, and `control/subscribe/output/stream/last-failure` and the log say, for example:

```
a frame of 'hdvisionsystems/lumiscan-cam-driver/camera1/control/publish/output/stream' failed: language 'deu' is no longer installed; the frame was read with 'eng' instead and its result dropped
```

**Possible causes**

- The language model disappeared between the language check and the read, so the engine read with `eng`. `read-text` publishes such a reading with a `NOTE` in its `status-text`, but the stream drops it, because its settings hash names the model that was asked for.

**Diagnostic procedure**

1. Read `control/subscribe/output/stream/last-failure`.
2. Check `control/subscribe/output/stream/statistics/failed-frames`.
3. Check that the model file for `input/language` is still in its directory (see `system/tessdata-dirs`).

**Resolution**

- **If the model disappeared:** make the model for `input/language` available in its directory again. A model you upload is used from the next frame on. Keep it available while the stream runs - do not remove the USB stick it is on, for example.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with `control/subscribe/output/stream/last-failure`, the counters under `control/subscribe/output/stream/statistics/` and the values of `input/language` and `system/tessdata-dirs`.

## Modules and settings

### Problem: My settings or modules are gone after a restart {#faq-settings-lost}

**Symptoms**

After a restart of the application, a module is missing from `system/modules`, a module is back with its default inputs, or a module that streamed before does not stream.

**Possible causes**

- The module list was not saved with `system/save-current-modules`. Nothing survives a restart until it is saved; the module list and each module’s settings are saved separately.
- The module’s settings were not saved with `system/save-module-inputs`.
- The image stream’s mode and source are saved by `control/subscribe/execute` in the module’s `<module-id>.json`, but they come back only if the module list is saved too.
- `<module-id>.json` contains comments. The module’s inputs are then not restored at start; the log reports the file as unreadable.
- A setting whose node no longer exists after a software update is skipped, with a log entry.

**Diagnostic procedure**

1. Read `system/modules`.
2. Check the log for a module file reported as unreadable, or for skipped settings.
3. Check that `<module-id>.json` exists in the configuration directory: on ctrlX OS `/var/snap/rexroth-solutions/common/solutions/activeConfiguration/lumiscan-text-reader`; on a PC (Windows) `%APP_RESOURCE_DIR%\etc\lumiscan-text-reader`, or without `APP_RESOURCE_DIR` `C:\ProgramData\HD Vision Systems GmbH\LumiScan Vision Suite\lumiscan-text-read\etc\lumiscan-text-reader`.

**Resolution**

- **If the module’s settings were not saved:**
  1. Set `system/save-module-inputs/input/module-id`, for example `module-1`.
  2. Write to `system/save-module-inputs/execute`.
  3. Check that `system/save-module-inputs/output/status-code` is `1`.
- **If the module list was not saved:** write to `system/save-current-modules/execute`. The stream mode and source of a module come back after a restart only if the module list is saved too.
- **If `<module-id>.json` contains comments:** remove the comments from the file. Changes made to the file by hand take effect at the next start.

A directory chosen with `system/set-tessdata-dir` is saved automatically.

**If the problem persists**

Send the information listed under [Information for support](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html#support-information) to support, together with the content of `system/modules`, the files in the configuration directory and the log of the start.

## Information for support {#support-information}

If a problem persists, send the following information to support:

- The version of LumiScan Text Reader.
- The platform: ctrlX OS on a ctrlX CORE or ctrlX IPC, with the ctrlX OS version; or a PC with Windows 11 or Ubuntu 24.04 and LumiScan Keryx.
- The application log: on ctrlX OS from the ctrlX Logbook or `journalctl`, on a PC the application’s console output.
- The date and time of the error.
- The Data Layer status: which Data Layer you use (ctrlX Data Layer or LumiScan Keryx; on a PC, the Data Layer address), the `output/status-code` and `output/status-text` of the operation that failed and, for a refused write, the result of the write. For the image stream, also `control/subscribe/output/stream/last-failure` and the counters under `control/subscribe/output/stream/statistics/`.
- The configuration: the values of the `read-text/input/` nodes, `system/modules`, `system/tessdata-dir`, `system/tessdata-dirs` and `system/languages`, and the files in the configuration directory (the module files `<module-id>.json`).

Contact HD Vision Systems customer success at [customer.success@hdvisionsystems.com](mailto:customer.success@hdvisionsystems.com) or +49 6221 6721905. For licences, contact [licenses@hdvisionsystems.com](mailto:licenses@hdvisionsystems.com).

# Release Notes: v1.1.0 – LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/release-notes/v1-1-0-2026-08-10.html

First functioning release of LumiScan Text Reader. Earlier versions registered the application’s Data Layer nodes but returned a fixed placeholder result, so nothing below was usable before.

## New

**Text reading.** `<module>/read-text/execute` reads the printed text in a camera image and reports it twice over: the whole text as the engine laid it out, and a per-word list carrying a confidence and a position for every word.

**A choice of recognition engine**, `input/engine`: the modern neural-network `LSTM`, the original pattern-matching `LEGACY`, both together, or `DEFAULT` to let the engine choose. `LEGACY` is worth trying on dot-matrix and laser-etched marking.

**Twelve text layouts**, `input/layout`, from `SINGLE_CHAR` up to whole-page analysis with orientation detection. The default `SINGLE_BLOCK` suits a cropped label.

**A character set restriction**, `input/characters/mode` with `allow` and `deny`. Reading a numeric part number with `DIGITS` removes the whole class of `O`/`0` and `S`/`5` substitutions rather than filtering them afterwards - the cheapest accuracy improvement available.

**A confidence threshold**, `input/min-score`, so a control program never sees a word the engine had little faith in. The result reports the threshold that was applied, so a short word list can be told apart from a bad image.

**A region of interest per reading**, `input/crop`, which is both the biggest accuracy improvement and the biggest speed improvement available.

**Uploadable language models.** English and the orientation model ship with the application, and you add more by uploading `.traineddata` files into `…/activeConfiguration/lumiscan-text-reader/tessdata`. Uploading **adds** languages - the shipped ones are always searched as well, so English keeps working and you never copy our files anywhere. The file name is the language code, so there is nothing to look up: upload `deu.traineddata` and set `input/language` to `deu`. A model is usable on the very next reading, with no restart. `system/tessdata-dirs` shows every directory searched, `system/languages` what was found across them, and `system/set-tessdata-dir` points the upload directory somewhere else - a USB stick, for instance.

**Several modules**, each with its own settings and results, so one installation can watch several cameras. One licence covers ten modules.

**A status on every operation**: `output/status-code` and `output/status-text`, where a negative code always means the run failed and produced nothing.

## Worth knowing

**Text is not a code.** An OCR reading always returns something, and a misread looks exactly like a correct reading. Set `input/characters/mode` and `input/min-score` before relying on one in production - see [Reading Accurately](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/reading-accurately.html).

**An empty reading means no text was found, and nothing else.** Every other reason a run could produce nothing - an unreachable camera, an unusable setting, an engine that could not start - is caught and reported as a negative status code. A reader that could not start reports `-7` and says why, rather than looking like a working reader in front of a blank part.

**A language that is not installed is replaced by English rather than refused.** A reading validates the language up front and refuses an uninstalled one, naming what **is** available; and if a model disappears afterwards, `output/result.language` reports what was actually used. Read that field, not your own input, when you need to know how a reading was produced. See [Language Models](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/language-models.html).

**Every invalid input is reported at once**, and each message names the **node** to change rather than an internal field name - `min-score must be between 0 and 1, got 1.5`.

**The whole text is not filtered by the confidence threshold**, only the word list is. They answer different questions and can legitimately disagree.

# LumiScan Text Reader

URL: https://docs.hdvisionsystems.com/en/lumiscan-text-reader/index.html

|   |   |   |
| --- | --- | --- |
| ![Label with part number, lot and date; the part number line is marked](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/_images/intro/step-1.svg)<br><br>**Say where the text is.** A crop around the marking, and which characters may occur. | ![The words PN and 482913 outlined, each with its score](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/_images/intro/step-2.svg)<br><br>**Read it word by word.** With a confidence for every word. | Example result<br><br><br><br><br><br><br>Text<br>PN 482913<br><br><br>Words<br>2<br><br><br>Mean score<br>0.95<br><br><br>Status<br>1<br><br>**Check it in your PLC.** Compare the text and the score before you rely on it. |

LumiScan Text Reader reads printed text such as part numbers, batch and date codes and labels from a camera image. You tell it where the text is and which characters it may contain, and it returns the text with a confidence for every word, so your PLC can check the read before relying on it. It runs on ctrlX OS or a Windows or Ubuntu PC, with settings for dot-matrix and laser-etched markings.

For licensing, see [Licence](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/lumiscan-text-reader/requirements.html#licence). Questions? See the [FAQ](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/faq.html). A problem or an error message? See [Troubleshooting](https://docs.hdvisionsystems.com/en/lumiscan-text-reader/troubleshooting.html).
