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.
Files changed (53) hide show
  1. chugr/__init__.py +46 -0
  2. chugr/app/test.py +105 -0
  3. chugr/common/__init__.py +9 -0
  4. chugr/common/cli.py +234 -0
  5. chugr/common/collate.py +104 -0
  6. chugr/common/config.py +335 -0
  7. chugr/common/random.py +76 -0
  8. chugr/common/serialize.py +150 -0
  9. chugr/common/task_config.py +24 -0
  10. chugr/common/types.py +122 -0
  11. chugr/common/urls.py +88 -0
  12. chugr/doc/__init__.py +17 -0
  13. chugr/doc/constants.py +15 -0
  14. chugr/doc/doc_processor.py +248 -0
  15. chugr/doc/doc_read_processor.py +249 -0
  16. chugr/doc/doc_vqa_processor.py +113 -0
  17. chugr/doc/synth_text.py +212 -0
  18. chugr/hfds/__init__.py +2 -0
  19. chugr/hfds/collate.py +83 -0
  20. chugr/hfds/loader.py +385 -0
  21. chugr/hfds/wrappers.py +108 -0
  22. chugr/image/__init__.py +4 -0
  23. chugr/image/build_transforms_doc.py +427 -0
  24. chugr/image/build_transforms_image.py +203 -0
  25. chugr/image/transforms_alb.py +112 -0
  26. chugr/image/transforms_factory.py +58 -0
  27. chugr/image/transforms_torch.py +137 -0
  28. chugr/loader.py +249 -0
  29. chugr/naflex.py +357 -0
  30. chugr/task_pipeline/__init__.py +7 -0
  31. chugr/task_pipeline/pipeline_doc_read.py +66 -0
  32. chugr/task_pipeline/pipeline_doc_vqa.py +124 -0
  33. chugr/task_pipeline/pipeline_factory.py +32 -0
  34. chugr/task_pipeline/pipeline_gtparse.py +93 -0
  35. chugr/task_pipeline/pipeline_image_text.py +90 -0
  36. chugr/task_pipeline/pipeline_manual.py +27 -0
  37. chugr/text/__init__.py +1 -0
  38. chugr/text/tokenization.py +222 -0
  39. chugr/version.py +1 -0
  40. chugr/wds/__init__.py +7 -0
  41. chugr/wds/dataset_info.py +65 -0
  42. chugr/wds/decode.py +319 -0
  43. chugr/wds/filters.py +142 -0
  44. chugr/wds/helpers.py +68 -0
  45. chugr/wds/loader.py +187 -0
  46. chugr/wds/pipeline.py +182 -0
  47. chugr/wds/shardlists.py +134 -0
  48. chugr/wds/tariterators.py +70 -0
  49. chugr-0.2.0.dev0.dist-info/METADATA +436 -0
  50. chugr-0.2.0.dev0.dist-info/RECORD +53 -0
  51. chugr-0.2.0.dev0.dist-info/WHEEL +4 -0
  52. chugr-0.2.0.dev0.dist-info/entry_points.txt +4 -0
  53. chugr-0.2.0.dev0.dist-info/licenses/LICENSE +201 -0
chugr/wds/decode.py ADDED
@@ -0,0 +1,319 @@
1
+ import io
2
+ import logging
3
+ import os
4
+ import random
5
+ import re
6
+ import warnings
7
+ from typing import Any, Callable, Dict, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+ import webdataset as wds
11
+ from PIL import Image
12
+
13
+ from .helpers import log_and_continue
14
+
15
+
16
+ # IMPORTANT fitz aka PyMuPDF is AGPL licensed w/ a commercial purchase option, manual intervention required to use it.
17
+ _USE_AGPL_PYMUPDF = int(os.environ.get('CHUGR_USE_AGPL_PYMUPDF', -1))
18
+ if _USE_AGPL_PYMUPDF < 1:
19
+ import importlib
20
+ if importlib.util.find_spec('fitz') is not None and _USE_AGPL_PYMUPDF == -1:
21
+ # warn if not explicitly disable by setting to 0
22
+ warnings.warn(
23
+ "The fitz/pymupdf library is installed but disabled as the environment variable CHUGR_USE_AGPL_PYMUPDF is"
24
+ " not set to 1. Please be aware of the licensing concerns in your use cases if you enable it.")
25
+ fitz = None
26
+ else:
27
+ try:
28
+ import fitz
29
+ except ImportError as e:
30
+ fitz = None
31
+
32
+ # defaults to pypdfium2 when fitz is not installed and/or not enabled
33
+ if fitz is None:
34
+ try:
35
+ import pypdfium2
36
+ except ImportError as e:
37
+ pypdfium2 = None
38
+ else:
39
+ pypdfium2 = None
40
+
41
+ _logger = logging.getLogger(__name__)
42
+
43
+
44
+ PDF_EXTENSIONS = {
45
+ 'pdf',
46
+ }
47
+
48
+
49
+ def decode_pdf_pages(
50
+ data: bytes,
51
+ image_mode: str = 'L',
52
+ page_indices: Optional[Tuple[int]] = None,
53
+ select_random: Optional[int] = None,
54
+ render_dpi: int = 144,
55
+ ):
56
+ rendered_pages = []
57
+
58
+ with io.BytesIO(data) as b:
59
+ # FIXME test and use an alternate pdf reader/render as default
60
+ if fitz is not None:
61
+ doc = fitz.Document(stream=b)
62
+ num_doc_pages = doc.page_count
63
+
64
+ if page_indices is not None:
65
+ page_indices = [p % num_doc_pages for p in page_indices] # support -ve indexing
66
+ else:
67
+ page_indices = range(num_doc_pages)
68
+
69
+ if select_random:
70
+ if select_random == 1:
71
+ page_indices = [random.choice(page_indices)]
72
+ else:
73
+ page_indices = random.sample(page_indices, select_random)
74
+ page_indices.sort()
75
+
76
+ for i in page_indices:
77
+ page = doc.load_page(i)
78
+ if image_mode == 'L':
79
+ fitz_cs = fitz.csGRAY
80
+ fitz_mode = 'L'
81
+ alpha = False
82
+ elif image_mode == 'RGB' or image_mode == 'BGR':
83
+ fitz_cs = fitz.csRGB
84
+ fitz_mode = 'RGB'
85
+ alpha = False
86
+ elif image_mode == 'RGBA' or image_mode == 'BGRA':
87
+ fitz_cs = fitz.csRGB
88
+ fitz_mode = 'RGBA'
89
+ alpha = True
90
+ else:
91
+ assert False
92
+ pixmap = page.get_pixmap(dpi=render_dpi, colorspace=fitz_cs, alpha=alpha)
93
+ page_image = Image.frombuffer(fitz_mode, (pixmap.width, pixmap.height), pixmap.samples)
94
+ if fitz_mode != page_image.mode:
95
+ page_image = page_image.convert(image_mode)
96
+
97
+ rendered_pages += [page_image]
98
+
99
+ elif pypdfium2 is not None:
100
+ grayscale = image_mode == "L"
101
+ reverse = 'RGB' in image_mode
102
+ doc = pypdfium2.PdfDocument(data)
103
+ num_doc_pages = len(doc)
104
+ page_indices = page_indices or range(num_doc_pages)
105
+ if select_random:
106
+ page_indices = [random.choice(page_indices)]
107
+ for i in page_indices:
108
+ page = doc[i]
109
+ page_image = page.render(
110
+ scale=render_dpi / 72,
111
+ grayscale=grayscale,
112
+ rev_byteorder=reverse, # RGB instead of BGR(X)
113
+ ).to_pil()
114
+ if image_mode != page_image.mode:
115
+ page_image = page_image.convert(image_mode)
116
+
117
+ rendered_pages += [page_image]
118
+ else:
119
+ assert False, "No PDF decoding library installed, please install one of pypdfium2 or fitz (PyMuPDF). " \
120
+ "NOTE: pypdifum2 is Apache 2.0 / BSD 3.0 licensed and fitz is AGPL."
121
+
122
+ return rendered_pages, num_doc_pages
123
+
124
+
125
+ def decode_image_pages(
126
+ data: bytes,
127
+ image_mode: str = 'L',
128
+ page_indices: Optional[Tuple[int]] = None,
129
+ select_random: Optional[int] = None,
130
+ ):
131
+ """ decode multi-page image (e.g. TIFF)"""
132
+ decoded_pages = []
133
+
134
+ if isinstance(data, Image.Image):
135
+ doc_image = data
136
+ else:
137
+ doc_image = Image.open(io.BytesIO(data))
138
+
139
+ num_image_pages = getattr(doc_image, 'n_frames', 1)
140
+
141
+ if page_indices is not None:
142
+ page_indices = [p % num_image_pages for p in page_indices] # support -ve indexing
143
+ else:
144
+ page_indices = range(num_image_pages)
145
+
146
+ if select_random:
147
+ if select_random == 1:
148
+ page_indices = [random.choice(page_indices)]
149
+ else:
150
+ page_indices = random.sample(page_indices, select_random)
151
+ page_indices.sort()
152
+
153
+ for i, page_index in enumerate(page_indices):
154
+ assert page_index < num_image_pages
155
+ if num_image_pages > 1:
156
+ doc_image.seek(page_index)
157
+ else:
158
+ assert page_index == 0, "not a multi-page image"
159
+ doc_image.load()
160
+
161
+ page_image = doc_image.convert(image_mode)
162
+ decoded_pages.append(page_image)
163
+
164
+ return decoded_pages, num_image_pages
165
+
166
+
167
+ class DecodeDoc:
168
+
169
+ def __init__(
170
+ self,
171
+ imagespec,
172
+ num_pages=1,
173
+ page_sampling='first',
174
+ ):
175
+ """Create a PDF handler.
176
+
177
+ Args:
178
+ imagespec: short string indicating the type of decoding
179
+ The `imagespec` specifies whether the image is decoded
180
+ to numpy/torch/pi, decoded to uint8/float, and decoded
181
+ to l/rgb/rgba:
182
+
183
+ - l8: numpy uint8 l
184
+ - rgb8: numpy uint8 rgb
185
+ - rgba8: numpy uint8 rgba
186
+ - l: numpy float l
187
+ - rgb: numpy float rgb
188
+ - rgba: numpy float rgba
189
+ - torchl8: torch uint8 l
190
+ - torchrgb8: torch uint8 rgb
191
+ - torchrgba8: torch uint8 rgba
192
+ - torchl: torch float l
193
+ - torchrgb: torch float rgb
194
+ - torch: torch float rgb
195
+ - torchrgba: torch float rgba
196
+ - pill: pil None l
197
+ - pil: pil None rgb
198
+ - pilrgb: pil None rgb
199
+ - pilrgba: pil None rgba
200
+
201
+ """
202
+ if imagespec not in list(wds.autodecode.imagespecs.keys()):
203
+ raise ValueError("Unknown imagespec: %s" % imagespec)
204
+ self.imagespec = imagespec.lower()
205
+ # FIXME need to work out padding / selection issues for multi-page support
206
+ assert num_pages == 1, "Only 1-page decoding supported at present"
207
+ self.num_pages = num_pages
208
+ assert page_sampling in {'random', 'first', 'last'} # TODO add 'all' w/ multi-page support
209
+ self.page_sampling = page_sampling
210
+
211
+ def __call__(self, key, data):
212
+ """
213
+ Args:
214
+ key: file name extension
215
+ data: data to be decoded
216
+ """
217
+ extension = re.sub(r".*[.]", "", key)
218
+ if extension not in {'pdf', 'tiff', 'tif'}:
219
+ return None
220
+
221
+ imagespec = self.imagespec
222
+ atype, etype, mode = wds.autodecode.imagespecs[imagespec]
223
+
224
+ select_random = False
225
+ if self.page_sampling == 'random':
226
+ page_indices = None
227
+ select_random = True
228
+ elif self.page_sampling == 'first':
229
+ page_indices = [0] # first page
230
+ elif self.page_sampling == 'last':
231
+ page_indices = [-1]
232
+ else:
233
+ assert False
234
+
235
+ if extension == 'pdf':
236
+ # pdf document
237
+ result, num_pages = decode_pdf_pages(
238
+ data,
239
+ image_mode=mode.upper(),
240
+ page_indices=page_indices,
241
+ select_random=select_random,
242
+ )
243
+ else:
244
+ # multi-page image doc (e.g. tiff)
245
+ result, num_pages = decode_image_pages(
246
+ data,
247
+ image_mode=mode.upper(),
248
+ page_indices=page_indices,
249
+ select_random=select_random,
250
+ )
251
+
252
+ if atype == "pil":
253
+ return result
254
+
255
+ result = np.asarray(result)
256
+
257
+ if etype == "float":
258
+ result = result.astype(np.float32) / 255.0
259
+
260
+ assert result.ndim in [2, 3], result.shape
261
+ assert mode in ["l", "rgb", "rgba"], mode
262
+
263
+ if mode == "l":
264
+ if result.ndim == 3:
265
+ result = np.mean(result[:, :, :3], axis=2)
266
+ elif mode == "rgb":
267
+ if result.ndim == 2:
268
+ result = np.repeat(result[:, :, np.newaxis], 3, axis=2)
269
+ elif result.shape[2] == 4:
270
+ result = result[:, :, :3]
271
+ elif mode == "rgba":
272
+ if result.ndim == 2:
273
+ result = np.repeat(result[:, :, np.newaxis], 4, axis=2)
274
+ result[:, :, 3] = 255
275
+ elif result.shape[2] == 3:
276
+ result = np.concatenate(
277
+ [result, 255 * np.ones(result.shape[:2])], axis=2
278
+ )
279
+
280
+ assert atype in ["numpy", "torch"], atype
281
+
282
+ if atype == "numpy":
283
+ return result
284
+ elif atype == "torch":
285
+ import torch
286
+
287
+ if result.ndim == 3:
288
+ return torch.from_numpy(result.transpose(2, 0, 1))
289
+ else:
290
+ return torch.from_numpy(result)
291
+
292
+ return None
293
+
294
+
295
+ def create_image_decoder(
296
+ decode_fn: Optional[Callable] = None,
297
+ image_mode: str = "RGB",
298
+ enable_doc: bool = False, # FIXME enable doc support by default once tested?
299
+ handler: Callable = log_and_continue,
300
+ ):
301
+ if decode_fn is None:
302
+ if image_mode == "L":
303
+ img_type = "pill"
304
+ elif image_mode == "RGB":
305
+ img_type = "pilrgb"
306
+ else:
307
+ assert False, f"Unsupported image_mode ({image_mode})"
308
+ if enable_doc:
309
+ # FIXME, generic img + pdf decode WIP
310
+ decode_fn = wds.decode(DecodeDoc(img_type), img_type, handler=handler)
311
+ else:
312
+ decode_fn = wds.decode(img_type, handler=handler)
313
+ elif isinstance(decode_fn, (list, tuple)):
314
+ decode_fn = wds.decode(*decode_fn, handler=handler)
315
+ else:
316
+ assert isinstance(decode_fn, Callable)
317
+ decode_fn = wds.map(decode_fn)
318
+
319
+ return decode_fn
chugr/wds/filters.py ADDED
@@ -0,0 +1,142 @@
1
+ import itertools
2
+ import random
3
+ from typing import Mapping, Sequence, Union
4
+
5
+ import webdataset as wds
6
+ from webdataset.filters import _shuffle
7
+
8
+ from chugr.common import SharedCount, get_pytorch_worker_seed
9
+
10
+
11
+ class detshuffle_v2(wds.PipelineStage):
12
+
13
+ def __init__(
14
+ self,
15
+ bufsize: int = 1000,
16
+ initial: int = 100,
17
+ seed: int = 0,
18
+ interval: Union[int, SharedCount] =-1,
19
+ unique_worker: bool = False,
20
+ ):
21
+ self.bufsize = bufsize
22
+ self.initial = initial
23
+ self.seed = seed
24
+ self.interval = interval
25
+ self.unique_worker = unique_worker
26
+
27
+ def run(self, src):
28
+ if isinstance(self.interval, SharedCount):
29
+ interval = self.interval.get_value()
30
+ else:
31
+ # NOTE: this is epoch tracking is problematic in a multiprocess (dataloader workers or train)
32
+ # situation as different workers may wrap at different times (or not at all).
33
+ self.interval += 1
34
+ interval = self.interval
35
+
36
+ rng = random.Random()
37
+ if self.unique_worker:
38
+ # Use the PyTorch worker's seed, *different* across all nodes/workers
39
+ # but also deterministic if they are set consistently
40
+ seed = get_pytorch_worker_seed(interval, initial_seed=self.seed)
41
+ else:
42
+ # This seed to be deterministic AND the *same* across all nodes/workers in each epoch/interval
43
+ seed = self.seed + interval
44
+ rng.seed(seed)
45
+
46
+ return _shuffle(src, self.bufsize, self.initial, rng)
47
+
48
+
49
+ def _map_v2(data, f, handler=wds.reraise_exception):
50
+ """ Map samples.
51
+
52
+ This function differs from wds.map, it only adds '__key__' back to sample if it exists.
53
+
54
+ """
55
+ for sample in data:
56
+ try:
57
+ result = f(sample)
58
+ except Exception as exn:
59
+ if handler(exn):
60
+ continue
61
+ else:
62
+ break
63
+ if result is None:
64
+ continue
65
+ if isinstance(sample, dict) and isinstance(result, dict) and "__key__" in sample:
66
+ result["__key__"] = sample.get("__key__")
67
+ yield result
68
+
69
+
70
+ map_v2 = wds.pipelinefilter(_map_v2)
71
+
72
+
73
+ def _expand_maybe(data, f, handler=wds.reraise_exception):
74
+ for sample in data:
75
+ if isinstance(sample, Mapping):
76
+ try:
77
+ result = f(sample)
78
+ except Exception as exn:
79
+ if handler(exn):
80
+ continue
81
+ else:
82
+ break
83
+ if result is None:
84
+ continue
85
+ if "__key__" in sample:
86
+ result["__key__"] = sample["__key__"]
87
+ yield result
88
+ else:
89
+ assert isinstance(sample, Sequence)
90
+ for subsample in sample:
91
+ assert isinstance(subsample, Mapping)
92
+ try:
93
+ result = f(subsample)
94
+ except Exception as exn:
95
+ if handler(exn):
96
+ continue
97
+ else:
98
+ break
99
+ if result is None:
100
+ continue
101
+ if "__key__" in subsample:
102
+ result["__key__"] = subsample["__key__"]
103
+ yield result
104
+
105
+
106
+ map_expand_maybe = wds.pipelinefilter(_expand_maybe)
107
+
108
+
109
+ def _expand_always(data, f, handler=wds.reraise_exception):
110
+ for sample in itertools.chain(*data):
111
+ assert isinstance(sample, Mapping)
112
+ try:
113
+ result = f(sample)
114
+ except Exception as exn:
115
+ if handler(exn):
116
+ continue
117
+ else:
118
+ break
119
+ if result is None:
120
+ continue
121
+ if "__key__" in sample:
122
+ result["__key__"] = sample["__key__"]
123
+ yield result
124
+
125
+
126
+ map_expand_always = wds.pipelinefilter(_expand_always)
127
+
128
+
129
+ def _flatten_nested(data, *args, replace_existing=True, remove_original=True):
130
+ """Convert dict samples to tuples."""
131
+ for sample in data:
132
+ for k in args:
133
+ nested_dict = sample.pop(k, {}) if remove_original else sample.get(k, {})
134
+ if replace_existing:
135
+ sample.update(nested_dict)
136
+ elif k in sample:
137
+ for sk, sv in nested_dict.items():
138
+ sample.setdefault(sk, sv)
139
+ yield sample
140
+
141
+
142
+ flatten_nested = wds.pipelinefilter(_flatten_nested)
chugr/wds/helpers.py ADDED
@@ -0,0 +1,68 @@
1
+ import logging
2
+ import os
3
+
4
+ from urllib.parse import urlparse
5
+
6
+ import braceexpand
7
+ import webdataset as wds
8
+
9
+
10
+ def urldir(url):
11
+ """Return the directory part of a url."""
12
+ parsed_url = urlparse(url)
13
+ path = parsed_url.path
14
+ directory = os.path.dirname(path)
15
+ return parsed_url._replace(path=directory).geturl()
16
+
17
+
18
+ def expand_urls(urls, weights=None):
19
+ if weights is None:
20
+ expanded_urls = wds.shardlists.expand_urls(urls)
21
+ return expanded_urls
22
+
23
+ if isinstance(urls, str):
24
+ urllist = urls.split("::")
25
+ weights = weights.split('::')
26
+ assert len(weights) == len(urllist), \
27
+ f"Expected the number of data components ({len(urllist)}) and weights({len(weights)}) to match."
28
+ weights = [float(weight) for weight in weights]
29
+ all_urls, all_weights = [], []
30
+ for url, weight in zip(urllist, weights):
31
+ expanded_url = list(braceexpand.braceexpand(url))
32
+ expanded_weights = [weight for _ in expanded_url]
33
+ all_urls.extend(expanded_url)
34
+ all_weights.extend(expanded_weights)
35
+ return all_urls, all_weights
36
+ else:
37
+ all_urls = list(urls)
38
+ return all_urls, weights
39
+
40
+
41
+ def log_and_continue(exn):
42
+ """Call in an exception handler to ignore any exception, issue a warning, and continue."""
43
+ logging.warning(f'Handling webdataset error ({repr(exn)}). Ignoring.')
44
+ return True
45
+
46
+
47
+ def dump_and_reraise(exn):
48
+ """Dump stack and stop."""
49
+ import traceback
50
+ exception_trace = ''.join(traceback.format_tb(exn.__traceback__))
51
+ logging.error(f'Handling webdataset {type(exn)}. Exception trace:\n {exception_trace}')
52
+ current_trace = ''.join(traceback.format_tb(exn.__traceback__))
53
+ logging.error(f'Current stack trace:\n {current_trace}')
54
+ raise exn
55
+
56
+
57
+ _error_handlers = {
58
+ 'log_and_continue': log_and_continue,
59
+ 'ignore_and_continue': wds.ignore_and_continue,
60
+ 'warn_and_continue': wds.warn_and_continue,
61
+ 'ignore_and_stop': wds.ignore_and_stop,
62
+ 'warn_and_stop': wds.warn_and_stop,
63
+ 'dump_and_reraise': dump_and_reraise,
64
+ 'reraise_exception': wds.reraise_exception,
65
+ }
66
+
67
+ def get_error_handler(name: str):
68
+ return _error_handlers.get(name, dump_and_reraise)