Passed
Pull Request — master (#536)
by Konstantin
01:40
created

ocrd_utils.image   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 579
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 52
eloc 252
dl 0
loc 579
rs 7.44
c 0
b 0
f 0

28 Functions

Rating   Name   Duplication   Size   Complexity  
A transpose_coordinates() 0 65 2
A adjust_canvas_to_rotation() 0 16 1
A points_from_x0y0x1y1() 0 13 1
A shift_coordinates() 0 17 1
A transform_coordinates() 0 16 2
A coordinates_of_segment() 0 34 1
A points_from_xywh() 0 11 1
A points_from_bbox() 0 4 1
A bbox_from_xywh() 0 7 1
A xywh_from_points() 0 5 1
A points_from_polygon() 0 3 1
A polygon_mask() 0 15 2
A xywh_from_polygon() 0 3 1
B crop_image() 0 28 6
A polygon_from_xywh() 0 3 1
A transpose_image() 0 39 1
A coordinates_for_segment() 0 32 1
A polygon_from_bbox() 0 3 1
A points_from_y0x0y1x1() 0 13 1
B rotate_image() 0 47 6
B bbox_from_polygon() 0 16 6
A adjust_canvas_to_transposition() 0 15 2
A image_from_polygon() 0 40 5
A xywh_from_bbox() 0 7 1
A polygon_from_points() 0 9 2
A polygon_from_x0y0x1y1() 0 7 1
A rotate_coordinates() 0 36 1
A bbox_from_points() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like ocrd_utils.image often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import sys
2
3
import numpy as np
4
from PIL import Image, ImageStat, ImageDraw, ImageChops
5
6
from .logging import getLogger
7
from .introspect import membername
8
9
__all__ = [
10
    'adjust_canvas_to_rotation',
11
    'adjust_canvas_to_transposition',
12
    'bbox_from_points',
13
    'bbox_from_polygon',
14
    'bbox_from_xywh',
15
    'coordinates_for_segment',
16
    'coordinates_of_segment',
17
    'image_from_polygon',
18
    'points_from_bbox',
19
    'points_from_polygon',
20
    'points_from_x0y0x1y1',
21
    'points_from_xywh',
22
    'points_from_y0x0y1x1',
23
    'polygon_from_bbox',
24
    'polygon_from_points',
25
    'polygon_from_x0y0x1y1',
26
    'polygon_from_xywh',
27
    'polygon_mask',
28
    'rotate_coordinates',
29
    'shift_coordinates',
30
    'transform_coordinates',
31
    'transpose_coordinates',
32
    'xywh_from_bbox',
33
    'xywh_from_points',
34
    'xywh_from_polygon',
35
]
36
37
def adjust_canvas_to_rotation(size, angle):
38
    """Calculate the enlarged image size after rotation.
39
    
40
    Given a numpy array ``size`` of an original canvas (width and height),
41
    and a rotation angle in degrees counter-clockwise ``angle``,
42
    calculate the new size which is necessary to encompass the full
43
    image after rotation.
44
    
45
    Return a numpy array of the enlarged width and height.
46
    """
47
    angle = np.deg2rad(angle)
48
    sin = np.abs(np.sin(angle))
49
    cos = np.abs(np.cos(angle))
50
    return np.dot(np.array([[cos, sin],
51
                            [sin, cos]]),
52
                  np.array(size))
53
54
def adjust_canvas_to_transposition(size, method):
55
    """Calculate the flipped image size after transposition.
56
    
57
    Given a numpy array ``size`` of an original canvas (width and height),
58
    and a transposition mode ``method`` (see ``transpose_image``),
59
    calculate the new size after transposition.
60
    
61
    Return a numpy array of the enlarged width and height.
62
    """
63
    if method in [Image.ROTATE_90,
64
                  Image.ROTATE_270,
65
                  Image.TRANSPOSE,
66
                  Image.TRANSVERSE]:
67
        size = size[::-1]
68
    return size
69
70
def bbox_from_points(points):
71
    """Construct a numeric list representing a bounding box from polygon coordinates in page representation."""
72
    xys = [[int(p) for p in pair.split(',')] for pair in points.split(' ')]
73
    return bbox_from_polygon(xys)
74
75
def bbox_from_polygon(polygon):
76
    """Construct a numeric list representing a bounding box from polygon coordinates in numeric list representation."""
77
    minx = sys.maxsize
78
    miny = sys.maxsize
79
    maxx = -sys.maxsize
80
    maxy = -sys.maxsize
81
    for xy in polygon:
82
        if xy[0] < minx:
83
            minx = xy[0]
84
        if xy[0] > maxx:
85
            maxx = xy[0]
86
        if xy[1] < miny:
87
            miny = xy[1]
88
        if xy[1] > maxy:
89
            maxy = xy[1]
90
    return minx, miny, maxx, maxy
91
92
def bbox_from_xywh(xywh):
93
    """Convert a bounding box from a numeric dict to a numeric list representation."""
94
    return (
95
        xywh['x'],
96
        xywh['y'],
97
        xywh['x'] + xywh['w'],
98
        xywh['y'] + xywh['h']
99
    )
100
101
def coordinates_of_segment(segment, parent_image, parent_coords):
102
    """Extract the coordinates of a PAGE segment element relative to its parent.
103
104
    Given...
105
106
    - ``segment``, a PAGE segment object in absolute coordinates
107
      (i.e. RegionType / TextLineType / WordType / GlyphType), and
108
    - ``parent_image``, the PIL.Image of its corresponding parent object
109
      (i.e. PageType / RegionType / TextLineType / WordType), (not used),
110
      along with
111
    - ``parent_coords``, its corresponding affine transformation,
112
113
    ...calculate the relative coordinates of the segment within the image.
114
115
    That is, apply the given transform to the points annotated in ``segment``.
116
    The transform encodes (recursively):
117
118
    1. Whenever ``parent_image`` or any of its parents was cropped,
119
       all points must be shifted by the offset
120
       (i.e. coordinate system gets translated by the upper left).
121
    2. Whenever ``parent_image`` or any of its parents was rotated,
122
       all points must be rotated around the center of that image
123
       (i.e. coordinate system gets translated by the center in
124
       opposite direction, rotated purely, and translated back;
125
       the latter involves an additional offset from the increase
126
       in canvas size necessary to accommodate all points).
127
128
    Return the rounded numpy array of the resulting polygon.
129
    """
130
    # get polygon:
131
    polygon = np.array(polygon_from_points(segment.get_Coords().points))
132
    # apply affine transform:
133
    polygon = transform_coordinates(polygon, parent_coords['transform'])
134
    return np.round(polygon).astype(np.int32)
135
136
def polygon_from_points(points):
137
    """
138
    Convert polygon coordinates in page representation to polygon coordinates in numeric list representation.
139
    """
140
    polygon = []
141
    for pair in points.split(" "):
142
        x_y = pair.split(",")
143
        polygon.append([float(x_y[0]), float(x_y[1])])
144
    return polygon
145
146
147
def coordinates_for_segment(polygon, parent_image, parent_coords):
148
    """Convert relative coordinates to absolute.
149
150
    Given...
151
152
    - ``polygon``, a numpy array of points relative to
153
    - ``parent_image``, a PIL.Image (not used), along with
154
    - ``parent_coords``, its corresponding affine transformation,
155
156
    ...calculate the absolute coordinates within the page.
157
    
158
    That is, apply the given transform inversely to ``polygon``
159
    The transform encodes (recursively):
160
161
    1. Whenever ``parent_image`` or any of its parents was cropped,
162
       all points must be shifted by the offset in opposite direction
163
       (i.e. coordinate system gets translated by the upper left).
164
    2. Whenever ``parent_image`` or any of its parents was rotated,
165
       all points must be rotated around the center of that image in
166
       opposite direction
167
       (i.e. coordinate system gets translated by the center in
168
       opposite direction, rotated purely, and translated back;
169
       the latter involves an additional offset from the increase
170
       in canvas size necessary to accommodate all points).
171
172
    Return the rounded numpy array of the resulting polygon.
173
    """
174
    polygon = np.array(polygon, dtype=np.float32) # avoid implicit type cast problems
175
    # apply inverse of affine transform:
176
    inv_transform = np.linalg.inv(parent_coords['transform'])
177
    polygon = transform_coordinates(polygon, inv_transform)
178
    return np.round(polygon).astype(np.int32)
179
180
def polygon_mask(image, coordinates):
181
    """"Create a mask image of a polygon.
182
183
    Given a PIL.Image ``image`` (merely for dimensions), and
184
    a numpy array ``polygon`` of relative coordinates into the image,
185
    create a new image of the same size with black background, and
186
    fill everything inside the polygon hull with white.
187
188
    Return the new PIL.Image.
189
    """
190
    mask = Image.new('L', image.size, 0)
191
    if isinstance(coordinates, np.ndarray):
192
        coordinates = list(map(tuple, coordinates))
193
    ImageDraw.Draw(mask).polygon(coordinates, outline=255, fill=255)
194
    return mask
195
196
def rotate_coordinates(transform, angle, orig=np.array([0, 0])):
197
    """Compose an affine coordinate transformation with a passive rotation.
198
199
    Given a numpy array ``transform`` of an existing transformation
200
    matrix in homogeneous (3d) coordinates, and a rotation angle in
201
    degrees counter-clockwise ``angle``, as well as a numpy array
202
    ``orig`` of the center of rotation, calculate the affine
203
    coordinate transform corresponding to the composition of both
204
    transformations. (This entails translation to the center, followed
205
    by pure rotation, and subsequent translation back. However, since
206
    rotation necessarily increases the bounding box, and thus image size,
207
    do not translate back the same amount, but to the enlarged offset.)
208
    
209
    Return a numpy array of the resulting affine transformation matrix.
210
    """
211
    LOG = getLogger('ocrd_utils.coords.rotate_coordinates')
212
    rad = np.deg2rad(angle)
213
    cos = np.cos(rad)
214
    sin = np.sin(rad)
215
    # get rotation matrix for passive rotation:
216
    rot = np.array([[+cos, sin, 0],
217
                    [-sin, cos, 0],
218
                    [0, 0, 1]])
219
    # shift to center of rotation
220
    transform = shift_coordinates(transform, -orig)
221
    # apply pure rotation
222
    LOG.debug('rotating coordinates by %.2f° around %s', angle, str(orig))
223
    transform = np.dot(rot, transform)
224
    # shift back
225
    transform = shift_coordinates(
226
        transform,
227
        #orig)
228
        # the image (bounding box) increases with rotation,
229
        # so we must translate back to the new upper left:
230
        adjust_canvas_to_rotation(orig, angle))
231
    return transform
232
233
def rotate_image(image, angle, fill='background', transparency=False):
234
    """"Rotate an image, enlarging and filling with background.
235
236
    Given a PIL.Image ``image`` and a rotation angle in degrees
237
    counter-clockwise ``angle``, rotate the image, increasing its
238
    size at the margins accordingly, and filling everything outside
239
    the original image according to ``fill``:
240
241
    - if ``background`` (the default),
242
      then use the median color of the image;
243
    - otherwise use the given color, e.g. ``'white'`` or (255,255,255).
244
245
    Moreover, if ``transparency`` is true, then add an alpha channel
246
    fully opaque (i.e. everything outside the original image will
247
    be transparent for those that can interpret alpha channels).
248
    (This is true for images which already have an alpha channel,
249
    regardless of the setting used.)
250
251
    Return a new PIL.Image.
252
    """
253
    LOG = getLogger('ocrd_utils.rotate_image')
254
    LOG.debug('rotating image by %.2f°', angle)
255
    if transparency and image.mode in ['RGB', 'L']:
256
        # ensure no information is lost by adding transparency channel
257
        # initialized to fully opaque (so cropping and rotation will
258
        # expose areas as transparent):
259
        image = image.copy()
260
        image.putalpha(255)
261
    if fill == 'background':
262
        background = ImageStat.Stat(image).median
263
        if image.mode in ['RGBA', 'LA']:
264
            background[-1] = 0 # fully transparent
265
        background = tuple(background)
266
    else:
267
        background = fill
268
    new_image = image.rotate(angle,
269
                             expand=True,
270
                             #resample=Image.BILINEAR,
271
                             fillcolor=background)
272
    if new_image.mode in ['LA']:
273
        # workaround for #1600 (bug in LA support which
274
        # causes areas fully transparent before rotation
275
        # to be filled with black here):
276
        image = new_image
277
        new_image = Image.new(image.mode, image.size, background)
278
        new_image.paste(image, mask=image.getchannel('A'))
279
    return new_image
280
281
282
def shift_coordinates(transform, offset):
283
    """Compose an affine coordinate transformation with a translation.
284
285
    Given a numpy array ``transform`` of an existing transformation
286
    matrix in homogeneous (3d) coordinates, and a numpy array
287
    ``offset`` of the translation vector, calculate the affine
288
    coordinate transform corresponding to the composition of both
289
    transformations.
290
    
291
    Return a numpy array of the resulting affine transformation matrix.
292
    """
293
    LOG = getLogger('ocrd_utils.coords.shift_coordinates')
294
    LOG.debug('shifting coordinates by %s', str(offset))
295
    shift = np.eye(3)
296
    shift[0, 2] = offset[0]
297
    shift[1, 2] = offset[1]
298
    return np.dot(shift, transform)
299
300
def transform_coordinates(polygon, transform=None):
301
    """Apply an affine transformation to a set of points.
302
    Augment the 2d numpy array of points ``polygon`` with a an extra
303
    column of ones (homogeneous coordinates), then multiply with
304
    the transformation matrix ``transform`` (or the identity matrix),
305
    and finally remove the extra column from the result.
306
    """
307
    if transform is None:
308
        transform = np.eye(3)
309
    polygon = np.insert(polygon, 2, 1, axis=1) # make 3d homogeneous coordinates
310
    polygon = np.dot(transform, polygon.T).T
311
    # ones = polygon[:,2]
312
    # assert np.all(np.array_equal(ones, np.clip(ones, 1 - 1e-2, 1 + 1e-2))), \
313
    #     'affine transform failed' # should never happen
314
    polygon = np.delete(polygon, 2, axis=1) # remove z coordinate again
315
    return polygon
316
317
def transpose_coordinates(transform, method, orig=np.array([0, 0])):
318
    """"Compose an affine coordinate transformation with a transposition (i.e. flip or rotate in 90° multiples).
319
320
    Given a numpy array ``transform`` of an existing transformation
321
    matrix in homogeneous (3d) coordinates, a transposition mode ``method``,
322
    as well as a numpy array ``orig`` of the center of the image,
323
    calculate the affine coordinate transform corresponding to the composition
324
    of both transformations, which is respectively:
325
326
    - ``PIL.Image.FLIP_LEFT_RIGHT``:
327
      entails translation to the center, followed by pure reflection
328
      about the y-axis, and subsequent translation back
329
    - ``PIL.Image.FLIP_TOP_BOTTOM``:
330
      entails translation to the center, followed by pure reflection
331
      about the x-axis, and subsequent translation back
332
    - ``PIL.Image.ROTATE_180``:
333
      entails translation to the center, followed by pure reflection
334
      about the origin, and subsequent translation back
335
    - ``PIL.Image.ROTATE_90``:
336
      entails translation to the center, followed by pure rotation
337
      by 90° counter-clockwise, and subsequent translation back
338
    - ``PIL.Image.ROTATE_270``:
339
      entails translation to the center, followed by pure rotation
340
      by 270° counter-clockwise, and subsequent translation back
341
    - ``PIL.Image.TRANSPOSE``:
342
      entails translation to the center, followed by pure rotation
343
      by 90° counter-clockwise and pure reflection about the x-axis,
344
      and subsequent translation back
345
    - ``PIL.Image.TRANSVERSE``:
346
      entails translation to the center, followed by pure rotation
347
      by 90° counter-clockwise and pure reflection about the y-axis,
348
      and subsequent translation back
349
350
    Return a numpy array of the resulting affine transformation matrix.
351
    """
352
    LOG = getLogger('ocrd_utils.coords.transpose_coordinates')
353
    LOG.debug('transposing coordinates with %s around %s', membername(Image, method), str(orig))
354
    # get rotation matrix for passive rotation/reflection:
355
    rot90 = np.array([[0, 1, 0],
356
                      [-1, 0, 0],
357
                      [0, 0, 1]])
358
    reflx = np.array([[1, 0, 0],
359
                      [0, -1, 0],
360
                      [0, 0, 1]])
361
    refly = np.array([[-1, 0, 0],
362
                      [0, 1, 0],
363
                      [0, 0, 1]])
364
    transform = shift_coordinates(transform, -orig)
365
    operations = {
366
        Image.FLIP_LEFT_RIGHT: [refly],
367
        Image.FLIP_TOP_BOTTOM: [reflx],
368
        Image.ROTATE_180: [reflx, refly],
369
        Image.ROTATE_90: [rot90],
370
        Image.ROTATE_270: [rot90, reflx, refly],
371
        Image.TRANSPOSE: [rot90, reflx],
372
        Image.TRANSVERSE: [rot90, refly]
373
    }.get(method) # no default
374
    for operation in operations:
375
        transform = np.dot(operation, transform)
376
    transform = shift_coordinates(
377
        transform,
378
        # the image (bounding box) may flip with transposition,
379
        # so we must translate back to the new upper left:
380
        adjust_canvas_to_transposition(orig, method))
381
    return transform
382
383
def transpose_image(image, method):
384
    """"Transpose (i.e. flip or rotate in 90° multiples) an image.
385
386
    Given a PIL.Image ``image`` and a transposition mode ``method``,
387
    apply the respective operation:
388
389
    - ``PIL.Image.FLIP_LEFT_RIGHT``:
390
      all pixels get mirrored at half the width of the image
391
    - ``PIL.Image.FLIP_TOP_BOTTOM``:
392
      all pixels get mirrored at half the height of the image
393
    - ``PIL.Image.ROTATE_180``:
394
      all pixels get mirrored at both, the width and half the height
395
      of the image,
396
      i.e. the image gets rotated by 180° counter-clockwise
397
    - ``PIL.Image.ROTATE_90``:
398
      rows become columns (but counted from the right) and
399
      columns become rows,
400
      i.e. the image gets rotated by 90° counter-clockwise;
401
      width becomes height and vice versa
402
    - ``PIL.Image.ROTATE_270``:
403
      rows become columns and
404
      columns become rows (but counted from the bottom),
405
      i.e. the image gets rotated by 270° counter-clockwise;
406
      width becomes height and vice versa
407
    - ``PIL.Image.TRANSPOSE``:
408
      rows become columns and vice versa,
409
      i.e. all pixels get mirrored at the main diagonal;
410
      width becomes height and vice versa
411
    - ``PIL.Image.TRANSVERSE``:
412
      rows become columns (but counted from the right) and
413
      columns become rows (but counted from the bottom),
414
      i.e. all pixels get mirrored at the opposite diagonal;
415
      width becomes height and vice versa
416
    
417
    Return a new PIL.Image.
418
    """
419
    LOG = getLogger('ocrd_utils.transpose_image')
420
    LOG.debug('transposing image with %s', membername(Image, method))
421
    return image.transpose(method)
422
423
def crop_image(image, box=None):
424
    """"Crop an image to a rectangle, filling with background.
425
426
    Given a PIL.Image ``image`` and a list ``box`` of the bounding
427
    rectangle relative to the image, crop at the box coordinates,
428
    filling everything outside ``image`` with the background.
429
    (This covers the case where ``box`` indexes are negative or
430
    larger than ``image`` width/height. PIL.Image.crop would fill
431
    with black.) Since ``image`` is not necessarily binarized yet,
432
    determine the background from the median color (instead of
433
    white).
434
435
    Return a new PIL.Image.
436
    """
437
    LOG = getLogger('ocrd_utils.crop_image')
438
    if not box:
439
        box = (0, 0, image.width, image.height)
440
    elif box[0] < 0 or box[1] < 0 or box[2] > image.width or box[3] > image.height:
441
        # (It should be invalid in PAGE-XML to extend beyond parents.)
442
        LOG.warning('crop coordinates (%s) exceed image (%dx%d)',
443
                    str(box), image.width, image.height)
444
    LOG.debug('cropping image to %s', str(box))
445
    xywh = xywh_from_bbox(*box)
446
    background = tuple(ImageStat.Stat(image).median)
447
    new_image = Image.new(image.mode, (xywh['w'], xywh['h']),
448
                          background) # or 'white'
449
    new_image.paste(image, (-xywh['x'], -xywh['y']))
450
    return new_image
451
452
def image_from_polygon(image, polygon, fill='background', transparency=False):
453
    """"Mask an image with a polygon.
454
455
    Given a PIL.Image ``image`` and a numpy array ``polygon``
456
    of relative coordinates into the image, fill everything
457
    outside the polygon hull to a color according to ``fill``:
458
459
    - if ``background`` (the default),
460
      then use the median color of the image;
461
    - otherwise use the given color, e.g. ``'white'`` or (255,255,255).
462
463
    Moreover, if ``transparency`` is true, then add an alpha channel
464
    from the polygon mask (i.e. everything outside the polygon will
465
    be transparent, for those consumers that can interpret alpha channels).
466
    Images which already have an alpha channel will have it shrunk
467
    from the polygon mask (i.e. everything outside the polygon will
468
    be transparent, in addition to existing transparent pixels).
469
    
470
    Return a new PIL.Image.
471
    """
472
    mask = polygon_mask(image, polygon)
473
    if fill == 'background':
474
        background = tuple(ImageStat.Stat(image).median)
475
    else:
476
        background = fill
477
    new_image = Image.new(image.mode, image.size, background)
478
    new_image.paste(image, mask=mask)
479
    # ensure no information is lost by a adding transparency channel
480
    # initialized to fully transparent outside the polygon mask
481
    # (so consumers do not have to rely on background estimation,
482
    #  which can fail on foreground-dominated segments, or white,
483
    #  which can be inconsistent on unbinarized images):
484
    if image.mode in ['RGBA', 'LA']:
485
        # ensure transparency maximizes (i.e. parent mask AND mask):
486
        mask = ImageChops.darker(mask, image.getchannel('A')) # min opaque
487
        new_image.putalpha(mask)
488
    elif transparency and image.mode in ['RGB', 'L']:
489
        # introduce transparency:
490
        new_image.putalpha(mask)
491
    return new_image
492
493
def points_from_bbox(minx, miny, maxx, maxy):
494
    """Construct polygon coordinates in page representation from a numeric list representing a bounding box."""
495
    return "%i,%i %i,%i %i,%i %i,%i" % (
496
        minx, miny, maxx, miny, maxx, maxy, minx, maxy)
497
498
def points_from_polygon(polygon):
499
    """Convert polygon coordinates from a numeric list representation to a page representation."""
500
    return " ".join("%i,%i" % (x, y) for x, y in polygon)
501
502
def points_from_xywh(box):
503
    """
504
    Construct polygon coordinates in page representation from numeric dict representing a bounding box.
505
    """
506
    x, y, w, h = box['x'], box['y'], box['w'], box['h']
507
    # tesseract uses a different region representation format
508
    return "%i,%i %i,%i %i,%i %i,%i" % (
509
        x, y,
510
        x + w, y,
511
        x + w, y + h,
512
        x, y + h
513
    )
514
def points_from_y0x0y1x1(yxyx):
515
    """
516
    Construct a polygon representation from a rectangle described as a list [y0, x0, y1, x1]
517
    """
518
    y0 = yxyx[0]
519
    x0 = yxyx[1]
520
    y1 = yxyx[2]
521
    x1 = yxyx[3]
522
    return "%s,%s %s,%s %s,%s %s,%s" % (
523
        x0, y0,
524
        x1, y0,
525
        x1, y1,
526
        x0, y1
527
    )
528
529
def points_from_x0y0x1y1(xyxy):
530
    """
531
    Construct a polygon representation from a rectangle described as a list [x0, y0, x1, y1]
532
    """
533
    x0 = xyxy[0]
534
    y0 = xyxy[1]
535
    x1 = xyxy[2]
536
    y1 = xyxy[3]
537
    return "%s,%s %s,%s %s,%s %s,%s" % (
538
        x0, y0,
539
        x1, y0,
540
        x1, y1,
541
        x0, y1
542
    )
543
544
def polygon_from_bbox(minx, miny, maxx, maxy):
545
    """Construct polygon coordinates in numeric list representation from a numeric list representing a bounding box."""
546
    return [[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]]
547
548
def polygon_from_x0y0x1y1(x0y0x1y1):
549
    """Construct polygon coordinates in numeric list representation from a string list representing a bounding box."""
550
    minx = int(x0y0x1y1[0])
551
    miny = int(x0y0x1y1[1])
552
    maxx = int(x0y0x1y1[2])
553
    maxy = int(x0y0x1y1[3])
554
    return [[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]]
555
556
def polygon_from_xywh(xywh):
557
    """Construct polygon coordinates in numeric list representation from numeric dict representing a bounding box."""
558
    return polygon_from_bbox(*bbox_from_xywh(xywh))
559
560
def xywh_from_bbox(minx, miny, maxx, maxy):
561
    """Convert a bounding box from a numeric list to a numeric dict representation."""
562
    return {
563
        'x': minx,
564
        'y': miny,
565
        'w': maxx - minx,
566
        'h': maxy - miny,
567
    }
568
569
def xywh_from_points(points):
570
    """
571
    Construct a numeric dict representing a bounding box from polygon coordinates in page representation.
572
    """
573
    return xywh_from_bbox(*bbox_from_points(points))
574
575
576
def xywh_from_polygon(polygon):
577
    """Construct a numeric dict representing a bounding box from polygon coordinates in numeric list representation."""
578
    return xywh_from_bbox(*bbox_from_polygon(polygon))
579