Passed
Push — master ( d4a853...e428fe )
by Konstantin
02:09
created

ocrd_utils.image.transpose_coordinates()   A

Complexity

Conditions 2

Size

Total Lines 65
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 65
rs 9.208
c 0
b 0
f 0
cc 2
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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