Skip to content

Commit 676ff04

Browse files
authored
Merge pull request #69 from OCR-D/port-to-v3
adapt to ocrd v3
2 parents 064b7a8 + 3f250f8 commit 676ff04

20 files changed

Lines changed: 1806 additions & 1953 deletions

Dockerfile

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,41 @@
1-
FROM docker.io/ocrd/core:v2.67.2 AS base
2-
ARG VCS_REF
3-
ARG BUILD_DATE
1+
ARG DOCKER_BASE_IMAGE=docker.io/ocrd/core
2+
FROM $DOCKER_BASE_IMAGE
3+
ARG DOCKER_BASE_IMAGE=docker.io/ocrd/core
4+
ARG VCS_REF=unknown
5+
ARG BUILD_DATE=unknown
46
LABEL \
5-
maintainer="https://github.com/OCR-D/ocrd_segment/issues" \
7+
maintainer="https://ocr-d.de/en/contact" \
68
org.label-schema.vcs-ref=$VCS_REF \
79
org.label-schema.vcs-url="https://github.com/OCR-D/ocrd_segment" \
8-
org.label-schema.build-date=$BUILD_DATE
10+
org.label-schema.build-date=$BUILD_DATE \
11+
org.opencontainers.image.vendor="DFG-Funded Initiative for Optical Character Recognition Development" \
12+
org.opencontainers.image.title="ocrd_segment" \
13+
org.opencontainers.image.description="page segmentation and segmentation evaluation" \
14+
org.opencontainers.image.source="https://github.com/OCR-D/ocrd_segment" \
15+
org.opencontainers.image.documentation="https://github.com/OCR-D/ocrd_segment/blob/${VCS_REF}/README.md" \
16+
org.opencontainers.image.revision=$VCS_REF \
17+
org.opencontainers.image.created=$BUILD_DATE \
18+
org.opencontainers.image.base.name=$DOCKER_BASE_IMAGE
19+
20+
ENV DEBIAN_FRONTEND noninteractive
21+
ENV PYTHONIOENCODING utf8
22+
ENV LC_ALL C.UTF-8
23+
ENV LANG C.UTF-8
24+
25+
# avoid HOME/.local/share (hard to predict USER here)
26+
# so let XDG_DATA_HOME coincide with fixed system location
27+
# (can still be overridden by derived stages)
28+
ENV XDG_DATA_HOME /usr/local/share
29+
# avoid the need for an extra volume for persistent resource user db
30+
# (i.e. XDG_CONFIG_HOME/ocrd/resources.yml)
31+
ENV XDG_CONFIG_HOME /usr/local/share/ocrd-resources
932

1033
WORKDIR /build/ocrd_segment
11-
COPY setup.py .
12-
COPY ocrd_segment/ocrd-tool.json .
13-
COPY ocrd_segment ./ocrd_segment
14-
COPY requirements.txt .
15-
COPY README.md .
16-
RUN pip install .
17-
RUN rm -rf /build/ocrd_segment
34+
COPY . .
35+
# prepackage ocrd-tool.json as ocrd-all-tool.json
36+
RUN ocrd ocrd-tool ocrd_segment/ocrd-tool.json dump-tools > $(dirname $(ocrd bashlib filename))/ocrd-all-tool.json
37+
# install everything and reduce image size
38+
RUN pip install . && rm -rf /build/ocrd_segment
1839

1940
WORKDIR /data
20-
VOLUME ["/data"]
41+
VOLUME /data

Makefile

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
SHELL = /bin/bash
22
PYTHON ?= python
33
PIP ?= pip
4-
TAG ?= ocrd/segment
5-
6-
# BEGIN-EVAL makefile-parser --make-help Makefile
4+
DOCKER_TAG ?= 'ocrd/segment'
5+
DOCKER_BASE_IMAGE ?= docker.io/ocrd/core:v3.1.0
76

87
help:
98
@echo ""
109
@echo " Targets"
1110
@echo ""
12-
@echo " deps (install required Python packages)"
13-
@echo " install (install this Python package)"
14-
@echo " docker (build Docker image)"
11+
@echo " deps (install required Python packages)"
12+
@echo " install (install this Python package)"
13+
@echo " install-dev (install in editable mode)"
14+
@echo " build (build source and binary distribution)"
15+
@echo " docker (build Docker image)"
1516
@echo ""
1617

1718
# END-EVAL
@@ -32,10 +33,18 @@ deps:
3233
install: deps
3334
$(PIP) install .
3435

36+
install-dev: deps
37+
$(PIP) install -e .
38+
39+
build:
40+
$(PIP) install build
41+
$(PYTHON) -m build .
42+
3543
docker:
3644
docker build \
37-
-t $(TAG) \
45+
-t $(DOCKER_TAG) \
46+
--build-arg DOCKER_BASE_IMAGE=$(DOCKER_BASE_IMAGE) \
3847
--build-arg VCS_REF=$(git rev-parse --short HEAD) \
3948
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") .
4049

41-
.PHONY: help deps install docker # deps-test test
50+
.PHONY: help deps install install-dev build docker # deps-test test

ocrd_segment/config.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

ocrd_segment/evaluate.py

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sys
44
import os
5+
from typing import Optional
56
import json
67
from itertools import chain
78
import click
@@ -10,17 +11,15 @@
1011
from PIL import Image
1112
from shapely.geometry import Polygon
1213

13-
from ocrd import Processor
14+
from ocrd import Workspace, Processor
1415
from ocrd_utils import (
1516
getLogger,
1617
initLogging,
17-
assert_file_grp_cardinality,
1818
xywh_from_polygon,
1919
polygon_from_points,
2020
coordinates_of_segment,
2121
MIMETYPE_PAGE
2222
)
23-
from ocrd_modelfactory import page_from_file
2423
from ocrd_models.ocrd_page import parse as parse_page
2524

2625
from pycocotools.coco import COCO
@@ -31,56 +30,51 @@
3130
area as maskArea
3231
)
3332

34-
from .config import OCRD_TOOL
35-
36-
TOOL = 'ocrd-segment-evaluate'
37-
3833
class EvaluateSegmentation(Processor):
3934

40-
def __init__(self, *args, **kwargs):
41-
kwargs['ocrd_tool'] = OCRD_TOOL['tools'][TOOL]
42-
kwargs['version'] = OCRD_TOOL['version']
43-
super(EvaluateSegmentation, self).__init__(*args, **kwargs)
35+
@property
36+
def executable(self):
37+
return 'ocrd-segment-evaluate'
4438

45-
def process(self):
39+
def process_workspace(self, workspace: Workspace) -> None:
4640
"""Performs segmentation evaluation with pycocotools on the workspace.
47-
41+
4842
Open and deserialize PAGE files from the first and second input file group
4943
(the first as ground truth, the second as prediction).
5044
Then iterate over the element hierarchy down to ``level-of-operation``.
5145
Aggregate and convert all pages' segmentation (coordinates and classes)
5246
to COCO:
5347
48+
\b
5449
- On the region level, unless ``ignore-subtype``, differentiate segment
5550
classes by their `@type`, if applicable.
5651
- On the region level, unless ``for-categories`` is empty, select only
5752
segment classes in that (comma-separated) list.
5853
- If ``only-fg``, then use the foreground mask from the binarized
5954
image inside each segment for overlap calculations.
60-
55+
6156
Next, configure and run COCOEval for comparison of all pages. Show the matching
6257
pairs (GT segment ID, prediction segment ID, IoU) for every overlap on each page.
6358
Also, calculate per-class precision and recall (at the point of maximum recall).
6459
Finally, get the typical summary mean average precision / recall (but without
6560
restriction on the number of segments).
66-
61+
6762
Write a JSON report to the output file group.
6863
"""
69-
LOG = getLogger('processor.EvaluateSegmentation')
70-
71-
assert_file_grp_cardinality(self.output_file_grp, 1)
72-
assert_file_grp_cardinality(self.input_file_grp, 2, 'GT and evaluation data')
7364
# region or line level?
7465
level = self.parameter['level-of-operation']
7566
onlyfg = self.parameter['only-fg']
7667
typed = not self.parameter['ignore-subtype']
7768
selected = self.parameter['for-categories']
7869
if selected:
7970
selected = selected.split(',')
71+
self.workspace = workspace
72+
self.verify()
73+
# FIXME: add configurable error handling as in super().process_workspace()
8074
# get input file groups
8175
ifgs = self.input_file_grp.split(",")
8276
# get input file tuples
83-
ifts = self.zip_input_files(mimetype=MIMETYPE_PAGE)
77+
ifts = self.zip_input_files(mimetype=MIMETYPE_PAGE, require_first=False)
8478
# convert to 2 COCO datasets from all page pairs
8579
categories = ["bg"] # needed by cocoeval
8680
images = []
@@ -89,14 +83,18 @@ def process(self):
8983
for ift in ifts:
9084
file_gt, file_dt = ift
9185
if not file_gt:
92-
LOG.warning("skipping page %s missing from GT", file_gt.pageId)
86+
self.logger.warning("skipping page %s missing from GT", file_gt.pageId)
9387
continue
9488
if not file_dt:
95-
LOG.warning("skipping page %s missing from prediction", file_gt.pageId)
89+
self.logger.warning("skipping page %s missing from prediction", file_gt.pageId)
9690
continue
97-
LOG.info("processing page %s", file_gt.pageId)
98-
pcgts_gt = page_from_file(self.workspace.download_file(file_gt))
99-
pcgts_dt = page_from_file(self.workspace.download_file(file_dt))
91+
self.logger.info("processing page %s", file_gt.pageId)
92+
if self.download:
93+
file_gt = self.workspace.download_file(file_gt)
94+
file_dt = self.workspace.download_file(file_dt)
95+
with pushd_popd(self.workspace.directory):
96+
pcgts_gt = page_from_file(file_gt)
97+
pcgts_dt = page_from_file(file_dt)
10098
page_gt = pcgts_gt.get_Page()
10199
page_dt = pcgts_dt.get_Page()
102100
if onlyfg:
@@ -115,11 +113,13 @@ def process(self):
115113
_add_annotations(annotations_gt, page_gt, imgid, categories,
116114
level=level, typed=typed,
117115
coords=page_coords if onlyfg else None,
118-
mask=page_mask if onlyfg else None)
116+
mask=page_mask if onlyfg else None,
117+
log=self.logger)
119118
_add_annotations(annotations_dt, page_dt, imgid, categories,
120119
level=level, typed=typed,
121120
coords=page_coords if onlyfg else None,
122-
mask=page_mask if onlyfg else None)
121+
mask=page_mask if onlyfg else None,
122+
log=self.logger)
123123

124124
if level == 'line':
125125
categories.append('textline')
@@ -130,17 +130,17 @@ def process(self):
130130
_add_ids(annotations_gt, 1) # cocoeval expects annotation IDs starting at 1
131131
_add_ids(annotations_dt, 1) # cocoeval expects annotation IDs starting at 1
132132

133-
LOG.info(f"found {len(annotations_gt)} GT / {len(annotations_dt)} DT segments"
134-
f" in {len(categories) - 1} categories for {len(images)} images")
133+
self.logger.info(f"found {len(annotations_gt)} GT / {len(annotations_dt)} DT segments"
134+
f" in {len(categories) - 1} categories for {len(images)} images")
135135

136136
coco_gt = _create_coco(categories, images, annotations_gt)
137137
coco_dt = _create_coco(categories, images, annotations_dt)
138138

139-
stats = evaluate_coco(coco_gt, coco_dt, self.parameter, selected)
139+
stats = evaluate_coco(coco_gt, coco_dt, self.parameter, selected, log=self.logger)
140140

141141
# write regions to custom JSON for this page
142142
file_id = 'id' + self.output_file_grp + '_report'
143-
self.workspace.add_file(
143+
workspace.add_file(
144144
ID=file_id,
145145
file_grp=self.output_file_grp,
146146
pageId=None,
@@ -203,6 +203,7 @@ def standalone_cli(gt_page_filelst,
203203
\b
204204
Write a JSON report to the output file group.
205205
"""
206+
initLogging()
206207
assert (tabfile is None) == (gt_page_filelst is not None) == (dt_page_filelst is not None), \
207208
"pass file lists either as tab-separated single file or as separate files"
208209
if tabfile is None:
@@ -238,8 +239,7 @@ def standalone_cli(gt_page_filelst,
238239

239240
# standalone entry point
240241
def evaluate_files(gt_files, dt_files, img_files=None, level='region', typed=True, selected=None):
241-
initLogging()
242-
LOG = getLogger('processor.EvaluateSegmentation')
242+
log = getLogger('EvaluateSegmentation')
243243
categories = ["bg"] # needed by cocoeval
244244
images = []
245245
annotations_gt = []
@@ -249,7 +249,7 @@ def evaluate_files(gt_files, dt_files, img_files=None, level='region', typed=Tru
249249
pcgts_gt = parse_page(gt_file)
250250
pcgts_dt = parse_page(dt_file)
251251
page_id = pcgts_gt.pcGtsId or gt_file
252-
LOG.info("processing page %s", page_id)
252+
log.info("processing page %s", page_id)
253253
page_gt = pcgts_gt.get_Page()
254254
page_dt = pcgts_dt.get_Page()
255255
if img_file:
@@ -271,11 +271,13 @@ def evaluate_files(gt_files, dt_files, img_files=None, level='region', typed=Tru
271271
_add_annotations(annotations_gt, page_gt, imgid, categories,
272272
level=level, typed=typed,
273273
coords=page_coords if img_file else None,
274-
mask=page_mask if img_file else None)
274+
mask=page_mask if img_file else None,
275+
log=log)
275276
_add_annotations(annotations_dt, page_dt, imgid, categories,
276277
level=level, typed=typed,
277278
coords=page_coords if img_file else None,
278-
mask=page_mask if img_file else None)
279+
mask=page_mask if img_file else None,
280+
log=log)
279281

280282
if level == 'line':
281283
categories.append('textline')
@@ -286,7 +288,7 @@ def evaluate_files(gt_files, dt_files, img_files=None, level='region', typed=Tru
286288
_add_ids(annotations_gt, 1) # cocoeval expects annotation IDs starting at 1
287289
_add_ids(annotations_dt, 1) # cocoeval expects annotation IDs starting at 1
288290

289-
LOG.info(f"found {len(annotations_gt)} GT / {len(annotations_dt)} DT segments"
291+
log.info(f"found {len(annotations_gt)} GT / {len(annotations_dt)} DT segments"
290292
f" in {len(categories) - 1} categories for {len(images)} images")
291293

292294
coco_gt = _create_coco(categories, images, annotations_gt)
@@ -299,9 +301,10 @@ def evaluate_files(gt_files, dt_files, img_files=None, level='region', typed=Tru
299301
stats = evaluate_coco(coco_gt, coco_dt, parameters, selected)
300302
return stats
301303

302-
def evaluate_coco(coco_gt, coco_dt, parameters, catIds=None):
303-
LOG = getLogger('processor.EvaluateSegmentation')
304-
LOG.info("comparing segmentations")
304+
def evaluate_coco(coco_gt, coco_dt, parameters, catIds=None, log=None):
305+
if log is None:
306+
log = getLogger('EvaluateSegmentation')
307+
log.info("comparing segmentations")
305308
stats = dict(parameters)
306309
coco_eval = COCOeval(coco_gt, coco_dt, 'segm') # bbox
307310
if catIds:
@@ -553,7 +556,7 @@ def _create_coco(categories, images, annotations):
553556
return coco
554557

555558
def _add_annotations(annotations, page, imgid, categories,
556-
level='region', typed=True, coords=None, mask=None):
559+
level='region', typed=True, coords=None, mask=None, log=None):
557560
for region in page.get_AllRegions(classes=None if level == 'region' else ['Text']):
558561
if level == 'region':
559562
cat = region.__class__.__name__[:-4]
@@ -563,18 +566,19 @@ def _add_annotations(annotations, page, imgid, categories,
563566
categories.append(cat)
564567
catid = categories.index(cat)
565568
_add_annotation(annotations, region, imgid, catid,
566-
coords=coords, mask=mask)
569+
coords=coords, mask=mask, log=log)
567570
continue
568571
for line in region.get_TextLine():
569572
_add_annotation(annotations, line, imgid, 1,
570-
coords=coords, mask=mask)
573+
coords=coords, mask=mask, log=log)
571574

572-
def _add_annotation(annotations, segment, imgid, catid, coords=None, mask=None):
573-
LOG = getLogger('processor.EvaluateSegmentation')
575+
def _add_annotation(annotations, segment, imgid, catid, coords=None, mask=None, log=None):
576+
if log is None:
577+
log = getLogger('EvaluateSegmentation')
574578
score = segment.get_Coords().get_conf() or 1.0
575579
polygon = polygon_from_points(segment.get_Coords().points)
576580
if len(polygon) < 3:
577-
LOG.warning('ignoring segment "%s" with only %d points', segment.id, len(polygon))
581+
log.warning('ignoring segment "%s" with only %d points', segment.id, len(polygon))
578582
return
579583
xywh = xywh_from_polygon(polygon)
580584
if mask is None:

0 commit comments

Comments
 (0)