22
33import sys
44import os
5+ from typing import Optional
56import json
67from itertools import chain
78import click
1011from PIL import Image
1112from shapely .geometry import Polygon
1213
13- from ocrd import Processor
14+ from ocrd import Workspace , Processor
1415from 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
2423from ocrd_models .ocrd_page import parse as parse_page
2524
2625from pycocotools .coco import COCO
3130 area as maskArea
3231)
3332
34- from .config import OCRD_TOOL
35-
36- TOOL = 'ocrd-segment-evaluate'
37-
3833class 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
240241def 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
555558def _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