chugr 0.2.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chugr/__init__.py +46 -0
- chugr/app/test.py +105 -0
- chugr/common/__init__.py +9 -0
- chugr/common/cli.py +234 -0
- chugr/common/collate.py +104 -0
- chugr/common/config.py +335 -0
- chugr/common/random.py +76 -0
- chugr/common/serialize.py +150 -0
- chugr/common/task_config.py +24 -0
- chugr/common/types.py +122 -0
- chugr/common/urls.py +88 -0
- chugr/doc/__init__.py +17 -0
- chugr/doc/constants.py +15 -0
- chugr/doc/doc_processor.py +248 -0
- chugr/doc/doc_read_processor.py +249 -0
- chugr/doc/doc_vqa_processor.py +113 -0
- chugr/doc/synth_text.py +212 -0
- chugr/hfds/__init__.py +2 -0
- chugr/hfds/collate.py +83 -0
- chugr/hfds/loader.py +385 -0
- chugr/hfds/wrappers.py +108 -0
- chugr/image/__init__.py +4 -0
- chugr/image/build_transforms_doc.py +427 -0
- chugr/image/build_transforms_image.py +203 -0
- chugr/image/transforms_alb.py +112 -0
- chugr/image/transforms_factory.py +58 -0
- chugr/image/transforms_torch.py +137 -0
- chugr/loader.py +249 -0
- chugr/naflex.py +357 -0
- chugr/task_pipeline/__init__.py +7 -0
- chugr/task_pipeline/pipeline_doc_read.py +66 -0
- chugr/task_pipeline/pipeline_doc_vqa.py +124 -0
- chugr/task_pipeline/pipeline_factory.py +32 -0
- chugr/task_pipeline/pipeline_gtparse.py +93 -0
- chugr/task_pipeline/pipeline_image_text.py +90 -0
- chugr/task_pipeline/pipeline_manual.py +27 -0
- chugr/text/__init__.py +1 -0
- chugr/text/tokenization.py +222 -0
- chugr/version.py +1 -0
- chugr/wds/__init__.py +7 -0
- chugr/wds/dataset_info.py +65 -0
- chugr/wds/decode.py +319 -0
- chugr/wds/filters.py +142 -0
- chugr/wds/helpers.py +68 -0
- chugr/wds/loader.py +187 -0
- chugr/wds/pipeline.py +182 -0
- chugr/wds/shardlists.py +134 -0
- chugr/wds/tariterators.py +70 -0
- chugr-0.2.0.dev0.dist-info/METADATA +436 -0
- chugr-0.2.0.dev0.dist-info/RECORD +53 -0
- chugr-0.2.0.dev0.dist-info/WHEEL +4 -0
- chugr-0.2.0.dev0.dist-info/entry_points.txt +4 -0
- chugr-0.2.0.dev0.dist-info/licenses/LICENSE +201 -0
chugr/common/types.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from multiprocessing import Value
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
4
|
+
from numbers import Number
|
|
5
|
+
|
|
6
|
+
from torch.utils.data import DataLoader, DistributedSampler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SharedCount:
|
|
10
|
+
def __init__(self, count: int = 0):
|
|
11
|
+
self.count = Value('i', count)
|
|
12
|
+
|
|
13
|
+
def set_value(self, epoch):
|
|
14
|
+
self.count.value = epoch
|
|
15
|
+
|
|
16
|
+
def get_value(self):
|
|
17
|
+
return self.count.value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class LoaderBundle:
|
|
22
|
+
"""
|
|
23
|
+
Bundle a DataLoader with num_batch / num_sample limits, sampler or shared_interval counter exposed
|
|
24
|
+
to allow easy seed control per-interval.
|
|
25
|
+
"""
|
|
26
|
+
loader: DataLoader
|
|
27
|
+
num_batches: int = 0
|
|
28
|
+
num_samples: int = 0
|
|
29
|
+
sampler: DistributedSampler = None
|
|
30
|
+
shared_interval: SharedCount = None
|
|
31
|
+
mean_batch_size: Optional[float] = None # set for variable batch size (NaFlex schedule) loaders
|
|
32
|
+
|
|
33
|
+
def set_interval(self, interval):
|
|
34
|
+
if self.shared_interval is not None:
|
|
35
|
+
self.shared_interval.set_value(interval)
|
|
36
|
+
dataset = getattr(self.loader, 'dataset', None)
|
|
37
|
+
if hasattr(dataset, 'set_interval_count'):
|
|
38
|
+
# main process side interval state of the dataset (e.g. the HF streaming dataset epoch)
|
|
39
|
+
dataset.set_interval_count(interval)
|
|
40
|
+
if self.sampler is not None and hasattr(self.sampler, 'set_epoch'):
|
|
41
|
+
self.sampler.set_epoch(interval)
|
|
42
|
+
|
|
43
|
+
def __iter__(self):
|
|
44
|
+
return self.loader.__iter__()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SplitInfo:
|
|
49
|
+
filenames: Tuple[str]
|
|
50
|
+
num_samples: int
|
|
51
|
+
shard_lengths: Tuple[int] = ()
|
|
52
|
+
name: str = ''
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# @dataclass
|
|
56
|
+
# class ShardInfo:
|
|
57
|
+
# url: str
|
|
58
|
+
# weight: float = 1.0
|
|
59
|
+
# num_samples: Optional[int] = None
|
|
60
|
+
#
|
|
61
|
+
#
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class SourceSpec:
|
|
66
|
+
url: str
|
|
67
|
+
split: Optional[str] = None # dataset split
|
|
68
|
+
template: Optional[str] = None # template to transform url -> usage
|
|
69
|
+
sampling_weight: Optional[float] = None
|
|
70
|
+
num_samples: Optional[int] = None
|
|
71
|
+
|
|
72
|
+
# TODO resolve dataset info and track base url, shard info (sizes, etc)
|
|
73
|
+
# base_url: str = None
|
|
74
|
+
# info_url: str = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class SourceInfo(SourceSpec):
|
|
79
|
+
split_info: Dict[str, SplitInfo] = None
|
|
80
|
+
shard_info: Dict[str, Dict[str, Any]] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ShardSpec:
|
|
85
|
+
urls: List[str]
|
|
86
|
+
weights: Optional[Union[float, List[float]]] = None
|
|
87
|
+
sizes: Optional[List[int]] = None
|
|
88
|
+
|
|
89
|
+
def __post_init__(self):
|
|
90
|
+
num_shards = len(self.urls)
|
|
91
|
+
if self.weights is not None:
|
|
92
|
+
if isinstance(self.weights, Number):
|
|
93
|
+
self.weights = [self.weights] * num_shards
|
|
94
|
+
assert len(self.weights) == num_shards
|
|
95
|
+
if self.sizes is not None:
|
|
96
|
+
assert len(self.sizes) == num_shards
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class FeatureInfo:
|
|
101
|
+
""" Feature Information
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
output_name: output feature name, None if an intermediary feature
|
|
105
|
+
input_key: input dataset key(s), ';' delimited for multiple options
|
|
106
|
+
"""
|
|
107
|
+
output_name: Optional[str] = 'image'
|
|
108
|
+
input_key: Optional[str] = 'jpg;png'
|
|
109
|
+
#parent: Optional[str] = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class ImageFeatureInfo(FeatureInfo):
|
|
114
|
+
""" Image Feature Information
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
image_mode: Image colour mode (e.g. 'RGB', 'RGBA', 'L')
|
|
118
|
+
output_name: output feature name, None if an intermediary feature
|
|
119
|
+
input_key: input dataset key(s), ';' delimited for multiple options
|
|
120
|
+
parent: parent key to search for input_key, e.g. 'json'
|
|
121
|
+
"""
|
|
122
|
+
image_mode: str = 'RGB'
|
chugr/common/urls.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from numbers import Number
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import braceexpand
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def envlookup(m):
|
|
11
|
+
"""Look up match in the environment with prefix WDS_.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
m: a match object
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
str: the value of the environment variable WDS_<m.group(1)>
|
|
18
|
+
"""
|
|
19
|
+
key = m.group(1)
|
|
20
|
+
for prefix in ('WDS_', 'CHUG_'):
|
|
21
|
+
key = prefix + key
|
|
22
|
+
if key in os.environ:
|
|
23
|
+
return os.environ[key]
|
|
24
|
+
assert key in os.environ, f"missing WDS/CHUG environment variable for {key}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def envsubst(s):
|
|
28
|
+
"""Substitute ${var} with the value of the environment variable WDS_var.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
s (str): string to be substituted
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
str: the substituted string
|
|
35
|
+
"""
|
|
36
|
+
return re.sub(r"\$\{(\w+)\}", envlookup, s)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _subst_and_expand(url: str):
|
|
40
|
+
for i in range(10):
|
|
41
|
+
last = url
|
|
42
|
+
url = envsubst(url)
|
|
43
|
+
if url == last:
|
|
44
|
+
break
|
|
45
|
+
return braceexpand.braceexpand(url)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def expand_urls(urls, weights=None):
|
|
49
|
+
""" Expand urls (and optionally weights) if they are strings, otherwise return as lists.
|
|
50
|
+
"""
|
|
51
|
+
if weights is None:
|
|
52
|
+
if isinstance(urls, str):
|
|
53
|
+
url_list = urls.split("::")
|
|
54
|
+
result = []
|
|
55
|
+
for url in url_list:
|
|
56
|
+
result.extend(_subst_and_expand(url))
|
|
57
|
+
return result, None
|
|
58
|
+
else:
|
|
59
|
+
return list(urls), None
|
|
60
|
+
|
|
61
|
+
if isinstance(urls, str):
|
|
62
|
+
url_list = urls.split('::')
|
|
63
|
+
|
|
64
|
+
if isinstance(weights, str):
|
|
65
|
+
weights = weights.split('::')
|
|
66
|
+
elif isinstance(weights, Number):
|
|
67
|
+
weights = [weights] * len(url_list)
|
|
68
|
+
assert len(weights) == len(url_list), \
|
|
69
|
+
f"Expected the number of data components ({len(url_list)}) and weights({len(weights)}) to match."
|
|
70
|
+
weights = [float(weight) for weight in weights]
|
|
71
|
+
all_urls, all_weights = [], []
|
|
72
|
+
for url, weight in zip(url_list, weights):
|
|
73
|
+
expanded_url = list(_subst_and_expand(url))
|
|
74
|
+
expanded_weights = [weight] * len(expanded_url)
|
|
75
|
+
all_urls.extend(expanded_url)
|
|
76
|
+
all_weights.extend(expanded_weights)
|
|
77
|
+
else:
|
|
78
|
+
all_urls = list(urls)
|
|
79
|
+
if isinstance(weights, Number):
|
|
80
|
+
# if weights is a scalar, expand to url list
|
|
81
|
+
all_weights = [float(weights)] * len(all_urls)
|
|
82
|
+
else:
|
|
83
|
+
assert len(weights) == len(all_urls), \
|
|
84
|
+
f"Expected the number of data components ({len(all_urls)}) and weights({len(weights)}) to match."
|
|
85
|
+
all_weights = list(weights)
|
|
86
|
+
|
|
87
|
+
return all_urls, all_weights
|
|
88
|
+
|
chugr/doc/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .constants import (
|
|
2
|
+
DEFAULT_DOC_KEY,
|
|
3
|
+
DEFAULT_QUESTION_KEY,
|
|
4
|
+
DEFAULT_QUESTION_ID_KEY,
|
|
5
|
+
DEFAULT_ANSWER_KEY,
|
|
6
|
+
DEFAULT_DOC_KEY_TUPLE,
|
|
7
|
+
DEFAULT_QUESTION_KEY_TUPLE,
|
|
8
|
+
DEFAULT_ANSWER_KEY_TUPLE,
|
|
9
|
+
DEFAULT_DOC_FEAT,
|
|
10
|
+
DEFAULT_QUESTION_FEAT,
|
|
11
|
+
DEFAULT_QUESTION_ID_FEAT,
|
|
12
|
+
DEFAULT_ANSWER_FEAT
|
|
13
|
+
)
|
|
14
|
+
from .doc_processor import DocProcessor
|
|
15
|
+
from .doc_read_processor import DocReadProcessor
|
|
16
|
+
from .doc_vqa_processor import DocVqaProcessor
|
|
17
|
+
from .synth_text import SynthTextCfg, SynthTextRenderer
|
chugr/doc/constants.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from chugr import FeatureInfo, ImageFeatureInfo
|
|
2
|
+
|
|
3
|
+
DEFAULT_DOC_KEY = "pdf;tif;png;jpeg;jpg;webp;image"
|
|
4
|
+
DEFAULT_QUESTION_KEY = "question;query"
|
|
5
|
+
DEFAULT_QUESTION_ID_KEY = "question_id;query_id"
|
|
6
|
+
DEFAULT_ANSWER_KEY = "answer;answers"
|
|
7
|
+
|
|
8
|
+
DEFAULT_DOC_KEY_TUPLE = tuple(DEFAULT_DOC_KEY.split(';'))
|
|
9
|
+
DEFAULT_QUESTION_KEY_TUPLE = tuple(DEFAULT_QUESTION_KEY.split(';'))
|
|
10
|
+
DEFAULT_ANSWER_KEY_TUPLE = tuple(DEFAULT_ANSWER_KEY.split(';'))
|
|
11
|
+
|
|
12
|
+
DEFAULT_DOC_FEAT = ImageFeatureInfo('image_input', input_key=DEFAULT_DOC_KEY, image_mode='L')
|
|
13
|
+
DEFAULT_QUESTION_FEAT = FeatureInfo(None, input_key=DEFAULT_QUESTION_KEY)
|
|
14
|
+
DEFAULT_QUESTION_ID_FEAT = FeatureInfo(None, input_key=DEFAULT_QUESTION_ID_KEY)
|
|
15
|
+
DEFAULT_ANSWER_FEAT = FeatureInfo(None, input_key=DEFAULT_ANSWER_KEY)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from chugr.common import FeatureInfo, ImageFeatureInfo
|
|
8
|
+
from chugr.wds import decode_image_pages, decode_pdf_pages
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .constants import (
|
|
12
|
+
DEFAULT_DOC_FEAT,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_next_valid_page_index(
|
|
19
|
+
current_index: int,
|
|
20
|
+
num_pages: int,
|
|
21
|
+
page_annos: list,
|
|
22
|
+
retries: int = 10,
|
|
23
|
+
wanted: str = 'lines',
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Get the index of the next valid page which contains text. If it doesn't find any non-empty page
|
|
27
|
+
after 'retries' attempts, it raises a RuntimeError.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
current_index (int): Current page index.
|
|
31
|
+
num_pages (int): Total number of pages.
|
|
32
|
+
page_annos (list): List of page annotations.
|
|
33
|
+
retries (int): Number of maximum retries for a given document.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
int: The index of the next non-empty page.
|
|
37
|
+
"""
|
|
38
|
+
for _ in range(retries):
|
|
39
|
+
# Get the next index, wrap around to 0 if it exceeds num_pages (in case of random init)
|
|
40
|
+
current_index = (current_index + 1) % num_pages
|
|
41
|
+
anno_page = page_annos[current_index]
|
|
42
|
+
anno_page = anno_page.get(wanted, anno_page) # use 'lines' / 'words' level if exists
|
|
43
|
+
if anno_page["text"]:
|
|
44
|
+
return current_index
|
|
45
|
+
raise RuntimeError(f"No non-empty page found after {retries} attempts")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_value(keys, sample):
|
|
49
|
+
if isinstance(keys, (list, tuple)):
|
|
50
|
+
value = None
|
|
51
|
+
for k in keys:
|
|
52
|
+
if (value := sample.get(k, None)) is not None:
|
|
53
|
+
break
|
|
54
|
+
return value
|
|
55
|
+
else:
|
|
56
|
+
return sample.get(keys, None)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DocProcessor:
|
|
60
|
+
""" Process documents w/ OCR annotation for reading tasks.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
image_process_fn: Optional[Callable] = None,
|
|
66
|
+
text_process_fn: Optional[Callable] = None,
|
|
67
|
+
image_input_feat: ImageFeatureInfo = DEFAULT_DOC_FEAT,
|
|
68
|
+
text_input_feat: FeatureInfo = FeatureInfo('text_input', input_key='pages'),
|
|
69
|
+
text_target_feat: FeatureInfo = FeatureInfo('text_target', input_key=None),
|
|
70
|
+
page_sampling: str = 'random',
|
|
71
|
+
render_dpi: int = 150,
|
|
72
|
+
squeeze_pages: bool = True,
|
|
73
|
+
expand_pages: bool = False,
|
|
74
|
+
flatten_json: bool = True,
|
|
75
|
+
seed: int = 0,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
image_process_fn:
|
|
81
|
+
text_process_fn:
|
|
82
|
+
page_sampling:
|
|
83
|
+
render_dpi:
|
|
84
|
+
seed:
|
|
85
|
+
"""
|
|
86
|
+
self.image_process_fn = image_process_fn
|
|
87
|
+
self.text_process_fn = text_process_fn
|
|
88
|
+
|
|
89
|
+
self.image_input_feat = image_input_feat
|
|
90
|
+
self.image_input_name = image_input_feat.output_name
|
|
91
|
+
self.image_input_key = image_input_feat.input_key.split(';')
|
|
92
|
+
self.text_input_feat = text_input_feat
|
|
93
|
+
self.text_input_name = text_input_feat.output_name
|
|
94
|
+
self.text_input_key = text_input_feat.input_key.split(';') if text_input_feat.input_key else None
|
|
95
|
+
self.text_target_feat = text_target_feat
|
|
96
|
+
self.text_target_name = text_target_feat.output_name if text_target_feat is not None else None
|
|
97
|
+
|
|
98
|
+
self.page_sampling = page_sampling
|
|
99
|
+
self.render_dpi = render_dpi
|
|
100
|
+
self.squeeze_pages = squeeze_pages
|
|
101
|
+
self.expand_pages = expand_pages
|
|
102
|
+
self.flatten_json = flatten_json
|
|
103
|
+
self.seed = seed
|
|
104
|
+
self._generator = random.Random(seed)
|
|
105
|
+
self._generator_worker = None
|
|
106
|
+
# FIXME note, should move to torchvision v2 annotations at some point
|
|
107
|
+
# * they should all eventually have a generator arg for better handling random state
|
|
108
|
+
# * they have forms that accept bbox/points args to transform annotations in sync with image
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def generator(self) -> random.Random:
|
|
112
|
+
# (re)seed per dataloader worker so workers don't produce identical random sequences
|
|
113
|
+
from torch.utils.data import get_worker_info
|
|
114
|
+
worker_info = get_worker_info()
|
|
115
|
+
worker_id = worker_info.id if worker_info is not None else -1
|
|
116
|
+
if worker_id != self._generator_worker:
|
|
117
|
+
if worker_info is not None:
|
|
118
|
+
self._generator = random.Random(worker_info.seed + self.seed)
|
|
119
|
+
self._generator_worker = worker_id
|
|
120
|
+
return self._generator
|
|
121
|
+
|
|
122
|
+
def _preprocess_image_pages(self, decoded_pages, page_image_info=None):
|
|
123
|
+
if self.image_process_fn is None:
|
|
124
|
+
return decoded_pages
|
|
125
|
+
|
|
126
|
+
if page_image_info is not None:
|
|
127
|
+
# FIXME, WIP. If train objective involves masking or otherwise processing image
|
|
128
|
+
# with knowledge of annotations / text content, anno info should contain
|
|
129
|
+
# mask locations, etc. For such a task, we need to pass it to image preprocess
|
|
130
|
+
decoded_pages = [self.image_process_fn(dp, page_info=pi) for dp, pi in zip(decoded_pages, page_image_info)]
|
|
131
|
+
else:
|
|
132
|
+
decoded_pages = [self.image_process_fn(dp) for dp in decoded_pages]
|
|
133
|
+
|
|
134
|
+
return decoded_pages
|
|
135
|
+
|
|
136
|
+
def _decode_image_pages(
|
|
137
|
+
self,
|
|
138
|
+
sample,
|
|
139
|
+
ext,
|
|
140
|
+
page_indices,
|
|
141
|
+
num_anno_pages,
|
|
142
|
+
):
|
|
143
|
+
image_mode = self.image_input_feat.image_mode
|
|
144
|
+
|
|
145
|
+
decoded_pages, num_image_pages = decode_image_pages(
|
|
146
|
+
sample[ext],
|
|
147
|
+
image_mode=image_mode,
|
|
148
|
+
page_indices=page_indices,
|
|
149
|
+
)
|
|
150
|
+
if num_image_pages != num_anno_pages:
|
|
151
|
+
_logger.warning(
|
|
152
|
+
f'Mismatch between num image and num annotation pages {num_image_pages} != {num_anno_pages}'
|
|
153
|
+
f' for sample {sample["__url__"]}, {sample["__key__"]}.')
|
|
154
|
+
|
|
155
|
+
decoded_pages = self._preprocess_image_pages(decoded_pages)
|
|
156
|
+
|
|
157
|
+
return decoded_pages, num_image_pages
|
|
158
|
+
|
|
159
|
+
def _decode_pdf_pages(
|
|
160
|
+
self,
|
|
161
|
+
sample,
|
|
162
|
+
ext,
|
|
163
|
+
page_indices,
|
|
164
|
+
num_anno_pages,
|
|
165
|
+
):
|
|
166
|
+
image_mode = self.image_input_feat.image_mode
|
|
167
|
+
decoded_pages, num_image_pages = decode_pdf_pages(
|
|
168
|
+
sample[ext],
|
|
169
|
+
image_mode=image_mode,
|
|
170
|
+
page_indices=page_indices,
|
|
171
|
+
render_dpi=self.render_dpi,
|
|
172
|
+
)
|
|
173
|
+
if num_anno_pages is not None and num_image_pages != num_anno_pages:
|
|
174
|
+
_logger.warning(
|
|
175
|
+
f'Mismatch between num image and num annotation pages {num_image_pages} != {num_anno_pages}'
|
|
176
|
+
f' for sample {sample["__url__"]}, {sample["__key__"]}.')
|
|
177
|
+
|
|
178
|
+
decoded_pages = self._preprocess_image_pages(decoded_pages)
|
|
179
|
+
|
|
180
|
+
return decoded_pages, num_image_pages
|
|
181
|
+
|
|
182
|
+
@abc.abstractmethod
|
|
183
|
+
def _decode_anno(self, sample) -> Tuple[Dict, List[int], int]:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
def _expand_anno(self, anno, count: int):
|
|
187
|
+
expanded_annos = [
|
|
188
|
+
{k: v[i] if isinstance(v, (list, tuple)) else v for k, v in anno.items()}
|
|
189
|
+
for i in range(count)
|
|
190
|
+
]
|
|
191
|
+
return expanded_annos
|
|
192
|
+
|
|
193
|
+
def _squeeze_anno(self, anno):
|
|
194
|
+
anno = {k: v[0] if isinstance(v, (list, tuple)) else v for k, v in anno.items()}
|
|
195
|
+
return anno
|
|
196
|
+
|
|
197
|
+
def __call__(self, sample):
|
|
198
|
+
if 'json' in sample and isinstance(sample['json'], bytes):
|
|
199
|
+
# decode json if present and in undecoded state
|
|
200
|
+
sample['json'] = json.loads(sample['json'])
|
|
201
|
+
|
|
202
|
+
if self.flatten_json and 'json' in sample:
|
|
203
|
+
# flatten json into sample
|
|
204
|
+
sample.update(sample.pop('json'))
|
|
205
|
+
|
|
206
|
+
# FIXME separate decode & preprocess interfaces
|
|
207
|
+
|
|
208
|
+
# decode page annotations / text
|
|
209
|
+
page_anno, page_indices, num_anno_pages = self._decode_anno(sample)
|
|
210
|
+
|
|
211
|
+
# decode page images
|
|
212
|
+
page_images = []
|
|
213
|
+
for ext in self.image_input_key:
|
|
214
|
+
if ext in sample:
|
|
215
|
+
if ext == 'pdf':
|
|
216
|
+
images, num_image_pages = self._decode_pdf_pages(
|
|
217
|
+
sample,
|
|
218
|
+
ext,
|
|
219
|
+
page_indices,
|
|
220
|
+
num_anno_pages,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
images, num_image_pages = self._decode_image_pages(
|
|
224
|
+
sample,
|
|
225
|
+
ext,
|
|
226
|
+
page_indices,
|
|
227
|
+
num_anno_pages,
|
|
228
|
+
)
|
|
229
|
+
page_images.extend(images)
|
|
230
|
+
# process one document type per doc, should be ordered by priority
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
assert len(page_images), 'No page images present'
|
|
234
|
+
|
|
235
|
+
if self.expand_pages and len(page_images) > 1:
|
|
236
|
+
# expand pages and page annotations into multiple samples (return list of sample dicts)
|
|
237
|
+
page_anno = self._expand_anno(page_anno, len(page_images))
|
|
238
|
+
decoded = [{self.image_input_name: pi, **pa} for pi, pa in zip(page_images, page_anno)]
|
|
239
|
+
else:
|
|
240
|
+
if self.squeeze_pages and len(page_images) == 1:
|
|
241
|
+
# squeeze page & annotation lists into singular items
|
|
242
|
+
page_images = page_images[0]
|
|
243
|
+
page_anno = self._squeeze_anno(page_anno)
|
|
244
|
+
decoded = {self.image_input_name: page_images, **page_anno}
|
|
245
|
+
|
|
246
|
+
return decoded
|
|
247
|
+
|
|
248
|
+
|