osrforge.tracing.registration
Grid registration: the fit of the printed grid to each region of a map image, and the alignment of two halves.
The tracing stage's second step. A region is one framed drawing on a map
image, and registering it means fitting the printed grid to it: one fit
over the whole region, with a separate x and y pitch, a shear, and a smooth,
low-order correction for scan drift, written out as the pixel position of
every grid corner (a Registration).
A gate with three parts judges the fit, and a region whose fit fails any part
is unregistered. Whether a registered region is refused, for a style the
stage can't trace or a scale it can't use, is decided after the whole-map
reading, not here.
Two regions printed as the halves of one map on facing pages get a row alignment and three candidate column offsets from their two registrations and the ink along the fold. The reading provider later picks one of the offsets, or none.
The functions here use numpy and Pillow only, call no model, and give the same result for the same image on every run.
BACKGROUND_RADIUS
module-attribute
Pixels. A pixel's local background is the lightest pixel within this distance of it.
COVERAGE_LINE_SHARE
module-attribute
A cell is covered when ink lies under at least this share of each of its two fitted lines.
COVERAGE_MINIMUM
module-attribute
The least share of a region's counted cells that must have ink under the fitted grid lines through them.
Measured with the reference fit of the registration measurement: the committed real maps give 0.857 to 0.996, the synthetic maps over their true grids 0.815 to 1.0, a copy of Chaotic Caves page 47 bowed by 0.3 of a pitch 0.875, and a copy of page 38 stretched by 4% 0.939. Copies of page 47 warped by a sine of 0.45 to 0.8 of a pitch over two periods give 0.38 to 0.55, and the sine of 0.6 over three periods 0.526.
COVERAGE_TOLERANCE
module-attribute
Pitches. Ink within this distance of a fitted grid line counts as lying under it.
EXPLAINED_MINIMUM
module-attribute
The least share of detected lines the fit must explain within LINE_TOLERANCE.
FILL_DARKNESS
module-attribute
A pixel whose darkest channel is at most 255 - FILL_DARKNESS is fill or solid ink, not paper.
GAP_TOLERANCE
module-attribute
Cells. A candidate offset's gap at the fold may be this far below zero, for the fits' error.
INK_MARGIN
module-attribute
How much darker than its local background a pixel must be to count as ink.
The implementation sets this value. On the synthetic TSR blue maps the blurred grid lines are 16 to 47 darker than the paper beside them, so the reference fit's 35 loses part of every line there. A build from this contract passed every synthetic map at 20: at 15 fold-east fails, and at 25 old-stair-2 finds no region. Whatever the value, the committed maps and the perturbed copies must stay on their sides of the gate.
LINE_TOLERANCE
module-attribute
Cells. A detected line is explained when it lies within this distance of the fitted line.
OFFSET_CANDIDATES
module-attribute
How many column offsets align_halves returns for the reading provider to choose among.
REJECT_TOLERANCE
module-attribute
Pitches. A detected line farther than this from every fitted line is rejected as not part of the grid (a wall, a stroke of a symbol, or a label). It counts against the explained share all the same.
GateFigures
Bases: BaseModel
The registration gate's figures for one region's fit.
The detected lines are found band by band: the region is cut, along
each axis in turn, into bands about three pitches wide (at least three
pitches, and at most twelve bands), and each peak of a band's profile of
thin ink runs that stands out is one detected line. One printed line that
crosses twelve bands is twelve detected lines, and a band with no grid in
it contributes none. A detected line within REJECT_TOLERANCE of a pitch
of a fitted line gets that line's index. The others are rejected, and
stay in the denominator of the explained share.
The gate passes when explained_share is at least EXPLAINED_MINIMUM,
index_jumps is 0, and coverage is at least COVERAGE_MINIMUM.
detected_lines
class-attribute
instance-attribute
Every detected line, indexed or rejected, over both axes.
rejected_lines
class-attribute
instance-attribute
The detected lines the fit rejected.
explained_share
class-attribute
instance-attribute
The share of detected_lines that lie within LINE_TOLERANCE of the fitted line they're indexed to.
index_jumps
class-attribute
instance-attribute
Pairs of neighbouring indexed lines in one band, with no other indexed line between them, whose index difference isn't their distance in pitches rounded to a whole number. Rejected lines between them don't matter. The jumps catch a numbering that slips along a band: a fitted pitch wrong enough that the lattice drifts a whole index across the band, so two printed lines one pitch apart take indices two apart where the lattice crosses from one line to the next. A pitch taken as half the printed one passes both the jumps and the share, because every printed line lies on that lattice and neighbours two indices apart are two pitches apart. The coverage fails it: half the lattice's lines run over paper.
counted_cells
class-attribute
instance-attribute
The cells of the fitted grid that hold ink and aren't fill. Ink is what register_image defines by
INK_MARGIN. A cell is blank when it holds fewer ink pixels, as a count, than three tenths of the mean pitch
in pixels, and fill when FILL_DARKNESS or darker covers at least half its pixels. Neither is counted.
covered_cells
class-attribute
instance-attribute
The counted cells whose west and north fitted lines each have ink within COVERAGE_TOLERANCE of a pitch
along at least COVERAGE_LINE_SHARE of their length.
coverage
class-attribute
instance-attribute
covered_cells over counted_cells, or 0 when no cell is counted.
HalfAlignment
Bases: BaseModel
How two halves of one map line up, from their registrations and the ink along the fold.
The fold is the first half's right image edge and the second half's left
image edge. The halves share one grid when the second half's first grid
line sits a whole number of columns from the first half's grid. Each
candidate column offset is that whole number counted past the first half's
last column, as HalfPair.columns records it, and its gap is the paper
the two scans would leave between them at the fold, in cells. The
candidates are the OFFSET_CANDIDATES offsets with the smallest gaps that
aren't below -GAP_TOLERANCE, in increasing order.
rows
instance-attribute
rows: int
The row shift: the second half's row 0 lies this many rows south of the first half's row 0, as
HalfPair.rows records it. The grid phase at the fold gives the fraction of a row, and the whole rows are the
shift at which the ink along the fold agrees best between the two halves. Two shifts tie when their
correlations lie within 0.01 of each other. On a tie, the shift that puts matching rows at the nearest heights
on their two pages wins, and if that ties too, the shift north wins.
columns
class-attribute
instance-attribute
columns: tuple[int, ...] = Field(min_length=OFFSET_CANDIDATES, max_length=OFFSET_CANDIDATES)
The candidate column offsets, consecutive and increasing.
gaps
class-attribute
instance-attribute
gaps: tuple[float, ...] = Field(min_length=OFFSET_CANDIDATES, max_length=OFFSET_CANDIDATES)
Each candidate's gap at the fold in cells, increasing, none below -GAP_TOLERANCE.
RegionFit
Bases: BaseModel
One gridded region of a map image, with its fit and the gate's verdict.
The region's rectangle is the framed drawing. Where a frame is drawn, the grid runs from the frame's west line to its east line and from its north line to its south line, so a title, a legend, or a compass outside the frame is no part of the region, and rock inside the frame is. Where no frame is drawn, the grid runs from the first printed grid line to the last on each axis. On a side that the image edge cuts, the grid runs to the outermost fitted line that lies no more than half a pitch outside the image, so a cell at least half inside the image is part of the grid. A legend or a label inside the rectangle doesn't move the fit. Regions are disjoint, one per framed drawing: two candidate grids that reach the same frame are one region.
rect
instance-attribute
(x0, y0, x1, y1) in pixels of the image, the end exclusive.
pitch
class-attribute
instance-attribute
The fitted x and y pitch in pixels, present whenever a fit was made.
verdict
instance-attribute
verdict: Literal['registered', 'unregistered']
Whether the fit passed the gate.
reason
class-attribute
instance-attribute
reason: str | None = None
Why the region is unregistered, or None when registered.
gate
class-attribute
instance-attribute
gate: GateFigures | None = None
The gate's figures, present whenever a fit was made. A region with too few lines for a fit has none.
registration
class-attribute
instance-attribute
registration: Registration | None = None
The fitted grid, present exactly when the verdict is registered.
align_halves
align_halves(first: Path, first_fit: RegionFit, second: Path, second_fit: RegionFit) -> HalfAlignment
Align the registered halves of one map across the fold.
The first half is the one cut off at the right edge of its image and the second the one cut off at the left edge of its image, in page order. The column candidates come from the two registrations alone: where the first half's grid reaches its right image edge, and where the second half's first grid line lies from its left image edge. The row shift's fraction comes from the two grids' phases at the fold, and its whole rows from the ink within a cell and a half of the fold on each side, measured from the image edges: the first half's strip against the second half's, over every whole-row shift.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
first
|
Path
|
The first half's image file. |
required |
first_fit
|
RegionFit
|
Its registered fit. |
required |
second
|
Path
|
The second half's image file. |
required |
second_fit
|
RegionFit
|
Its registered fit. |
required |
Returns:
| Type | Description |
|---|---|
HalfAlignment
|
The row shift and the candidate column offsets with their gaps. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If either fit is unregistered. |
OSError
|
If an image can't be read. |
register_image
Find the gridded regions of a map image and fit the printed grid to each.
The image is read as RGB. Ink is what's darker than its local background
by INK_MARGIN, so a grid line on grey fill counts and the inside of a
solid wall doesn't. Each framed drawing with a printed grid is one
region, found without any rectangle given by hand, and two drawings on
one image at different pitches are two regions with a fit each, because
each drawing's pitch is found on its own, not from one pitch estimated
over the whole image. A drawing with no grid yields no region, so an
image of a gridless map yields an empty tuple.
Each region gets one fit over the whole region: a separate x and y
pitch, a shear, and a smooth, low-order correction for scan drift, never
a fit tile by tile. The fit is judged by the three parts of the gate (see
GateFigures), and the region's registration holds every corner of the
fitted grid when the gate passes. A copy of a map bowed by three tenths of
a pitch, or stretched by 4% from top to bottom, registers, because the
drift correction follows it. A copy with part of its grid shifted by half
a pitch doesn't. The gate has a limit: a warp whose crests and troughs lie
about half a pitch apart can register on a lattice shifted by half a
pitch, with corners about half a cell off, and no part of the gate
catches it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Path
|
The image file, as the workdir's |
required |
Returns:
| Type | Description |
|---|---|
RegionFit
|
The regions in page order: by the top of their rectangles, then by the |
...
|
left. The same image gives the same result on every run. |
Raises:
| Type | Description |
|---|---|
OSError
|
If the image can't be read. |