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
@@ -0,0 +1,112 @@
1
+
2
+ try:
3
+ import albumentations as alb
4
+ from albumentations.pytorch import ToTensorV2
5
+ has_albumentations = True
6
+ except ImportError:
7
+ has_albumentations = False
8
+
9
+ try:
10
+ import cv2
11
+ has_cv2 = True
12
+ except ImportError:
13
+ has_cv2 = False
14
+
15
+ import random
16
+
17
+ import numpy as np
18
+
19
+
20
+ class AlbWrapper:
21
+ def __init__(self, transforms):
22
+ self.transforms = transforms
23
+
24
+ def __call__(self, im):
25
+ return self.transforms(image=np.asarray(im))["image"]
26
+
27
+ def __repr__(self) -> str:
28
+ format_string = self.__class__.__name__ + "("
29
+ for t in self.transforms:
30
+ format_string += "\n"
31
+ format_string += f" {t}"
32
+ format_string += "\n)"
33
+ return format_string
34
+
35
+
36
+ if has_albumentations and has_cv2:
37
+
38
+ class ErosionAlb(alb.ImageOnlyTransform):
39
+ def __init__(self, scale, p=0.5):
40
+ super().__init__(p=p)
41
+ if type(scale) is tuple or type(scale) is list:
42
+ assert len(scale) == 2
43
+ self.scale = scale
44
+ else:
45
+ self.scale = (scale, scale)
46
+
47
+ def get_transform_init_args_names(self):
48
+ return ('scale',)
49
+
50
+ def apply(self, img, **params):
51
+ kernel = cv2.getStructuringElement(
52
+ cv2.MORPH_ELLIPSE, tuple(int(k) for k in random.choices(range(self.scale[0], self.scale[1] + 1), k=2))
53
+ )
54
+ img = cv2.erode(img, kernel, iterations=1)
55
+ return img
56
+
57
+
58
+ class DilationAlb(alb.ImageOnlyTransform):
59
+ def __init__(self, scale, p=0.5):
60
+ super().__init__(p=p)
61
+ if type(scale) is tuple or type(scale) is list:
62
+ assert len(scale) == 2
63
+ self.scale = scale
64
+ else:
65
+ self.scale = (scale, scale)
66
+
67
+ def get_transform_init_args_names(self):
68
+ return ('scale',)
69
+
70
+ def apply(self, img, **params):
71
+ kernel = cv2.getStructuringElement(
72
+ cv2.MORPH_ELLIPSE,
73
+ tuple(int(k) for k in random.choices(range(self.scale[0], self.scale[1] + 1), k=2))
74
+ )
75
+ img = cv2.dilate(img, kernel, iterations=1)
76
+ return img
77
+
78
+
79
+ class BitmapAlb(alb.ImageOnlyTransform):
80
+ def __init__(self, value=0, lower=200, p=0.5):
81
+ super().__init__(p=p)
82
+ self.lower = lower
83
+ self.value = value
84
+
85
+ def get_transform_init_args_names(self):
86
+ return ('value', 'lower')
87
+
88
+ def apply(self, img, **params):
89
+ img = img.copy()
90
+ img[img < self.lower] = self.value
91
+ return img
92
+
93
+
94
+ class CropMarginCv2:
95
+
96
+ def __init__(self):
97
+ pass
98
+
99
+ def __call__(self, img):
100
+ data = np.array(img.convert("L"))
101
+ data = data.astype(np.uint8)
102
+ max_val = data.max()
103
+ min_val = data.min()
104
+ if max_val == min_val:
105
+ return img
106
+ data = (data - min_val) / (max_val - min_val) * 255
107
+ gray = 255 * (data < 200).astype(np.uint8)
108
+
109
+ coords = cv2.findNonZero(gray) # Find all non-zero points (text)
110
+ a, b, w, h = cv2.boundingRect(coords) # Find minimum spanning bounding box
111
+ return img.crop((a, b, w + a, h + b))
112
+
@@ -0,0 +1,58 @@
1
+ from typing import Any, Dict, Optional, Union
2
+
3
+ from chugr.common import ImageInputCfg, ImageAugCfg
4
+ from .build_transforms_doc import build_transforms_doc_better, build_transforms_doc_nougat, build_transforms_doc_basic, \
5
+ build_transforms_doc_naflex
6
+ from .build_transforms_image import build_transforms_image_timm, build_transforms_image_basic
7
+
8
+ _transform_factories = {
9
+ "image_basic": build_transforms_image_basic,
10
+ "image_timm": build_transforms_image_timm,
11
+ "doc_basic": build_transforms_doc_basic,
12
+ "doc_nougat": build_transforms_doc_nougat,
13
+ "doc_better": build_transforms_doc_better,
14
+ "doc_naflex": build_transforms_doc_naflex,
15
+ }
16
+
17
+ def build_image_transforms(
18
+ input_cfg: ImageInputCfg,
19
+ is_training=True,
20
+ do_normalize=True,
21
+ do_convert=False,
22
+ composed=True,
23
+ aug_cfg: Optional[Union[Dict[str, Any], ImageAugCfg]] = None,
24
+ ):
25
+ common_args = dict(
26
+ input_cfg=input_cfg,
27
+ is_training=is_training,
28
+ do_normalize=do_normalize,
29
+ aug_cfg=aug_cfg,
30
+ composed=composed,
31
+ )
32
+
33
+ tt = input_cfg.transform_type
34
+ assert tt in _transform_factories, \
35
+ f"Unrecognized transform type: {tt}. Must be one of {list(_transform_factories.keys())}."
36
+ transforms = _transform_factories[tt](**common_args)
37
+
38
+ return transforms
39
+
40
+
41
+ def create_image_preprocessor(
42
+ input_cfg: ImageInputCfg,
43
+ is_training=True,
44
+ do_normalize=True,
45
+ do_convert=False,
46
+ aug_cfg: Optional[Union[Dict[str, Any], ImageAugCfg]] = None,
47
+ ):
48
+ transforms = build_image_transforms(
49
+ input_cfg=input_cfg,
50
+ is_training=is_training,
51
+ do_normalize=do_normalize,
52
+ do_convert=do_convert,
53
+ aug_cfg=aug_cfg,
54
+ composed=True,
55
+ )
56
+ # NOTE for now, a stack of composed transforms are the image pre-processor
57
+ return transforms
58
+
@@ -0,0 +1,137 @@
1
+ import random
2
+
3
+ import numpy as np
4
+ import torch
5
+ import torchvision.transforms.functional as F
6
+ from torchvision import transforms
7
+ from PIL import Image, ImageFilter
8
+
9
+
10
+ class AlignLongAxis:
11
+ def __init__(
12
+ self,
13
+ input_size,
14
+ interpolation=transforms.InterpolationMode.BICUBIC
15
+ ):
16
+ self.input_size = input_size
17
+ self.interpolation = interpolation
18
+
19
+ def __call__(self, img):
20
+ img_width, img_height = F.get_image_size(img)
21
+ if (
22
+ (self.input_size[0] > self.input_size[1] and img_width > img_height) or
23
+ (self.input_size[0] < self.input_size[1] and img_width < img_height)
24
+ ):
25
+ img = F.rotate(img, angle=-90, expand=True, interpolation=self.interpolation)
26
+ return img
27
+
28
+
29
+ class Bitmap:
30
+ def __init__(self, threshold=200):
31
+ self.lut = [0 if i < threshold else i for i in range(256)]
32
+
33
+ def __call__(self, img):
34
+ if img.mode == "RGB" and len(self.lut) == 256:
35
+ lut = self.lut + self.lut + self.lut
36
+ else:
37
+ lut = self.lut
38
+ return img.point(lut)
39
+
40
+
41
+ class Erosion:
42
+ def __init__(self, scale=3):
43
+ super().__init__()
44
+ if type(scale) is tuple or type(scale) is list:
45
+ assert len(scale) == 2
46
+ self.scale = scale
47
+ else:
48
+ self.scale = (scale, scale)
49
+
50
+ @staticmethod
51
+ def get_params(scale):
52
+ if type(scale) is tuple or type(scale) is list:
53
+ assert len(scale) == 2
54
+ scale = random.choice(scale)
55
+ return scale
56
+
57
+ def __call__(self, img):
58
+ kernel_size = self.get_params(self.scale)
59
+ if isinstance(img, torch.Tensor):
60
+ padding = kernel_size // 2
61
+ img = -torch.nn.functional.max_pool2d(-img, kernel_size=kernel_size, stride=1, padding=padding) # minpool
62
+ elif isinstance(img, Image.Image):
63
+ img = img.filter(ImageFilter.MinFilter(kernel_size))
64
+ return img
65
+
66
+
67
+ class Dilation:
68
+ def __init__(self, scale=3):
69
+ super().__init__()
70
+ self.scale = scale
71
+
72
+ @staticmethod
73
+ def get_params(scale):
74
+ if type(scale) is tuple or type(scale) is list:
75
+ assert len(scale) == 2
76
+ scale = random.choice(scale)
77
+ return scale
78
+
79
+ def __call__(self, img):
80
+ kernel_size = self.get_params(self.scale)
81
+ if isinstance(img, torch.Tensor):
82
+ padding = kernel_size // 2
83
+ img = torch.nn.functional.max_pool2d(img, kernel_size=kernel_size, stride=1, padding=padding)
84
+ elif isinstance(img, Image.Image):
85
+ img = img.filter(ImageFilter.MaxFilter(kernel_size))
86
+ return img
87
+
88
+
89
+ def python_find_non_zero(image: np.array):
90
+ """This is a reimplementation of a findNonZero function equivalent to cv2."""
91
+ non_zero_indices = np.column_stack(np.nonzero(image))
92
+ idxvec = non_zero_indices[:, [1, 0]]
93
+ idxvec = idxvec.reshape(-1, 1, 2)
94
+ return idxvec
95
+
96
+
97
+ def python_bounding_rect(coordinates):
98
+ """This is a reimplementation of a BoundingRect function equivalent to cv2."""
99
+ min_values = np.min(coordinates, axis=(0, 1)).astype(int)
100
+ max_values = np.max(coordinates, axis=(0, 1)).astype(int)
101
+ x_min, y_min = min_values[0], min_values[1]
102
+ width = max_values[0] - x_min + 1
103
+ height = max_values[1] - y_min + 1
104
+ return x_min, y_min, width, height
105
+
106
+
107
+ class CropMargin:
108
+ def __init__(self) -> None:
109
+ pass
110
+
111
+ def __call__(
112
+ self,
113
+ image,
114
+ gray_threshold: int = 200,
115
+ ) -> np.array:
116
+ # FIXME check tensor vs PIL and convert as needed, this is assuming PIL right now
117
+ assert not isinstance(image, torch.Tensor)
118
+ data = np.array(image.convert("L")).astype(np.uint8)
119
+ max_val = data.max()
120
+ min_val = data.min()
121
+ if max_val == min_val:
122
+ return image
123
+ data = (data - min_val) / (max_val - min_val) * 255
124
+ gray = data < gray_threshold
125
+ coords = python_find_non_zero(gray)
126
+ x_min, y_min, width, height = python_bounding_rect(coords)
127
+ image = F.crop(image, y_min, x_min, height, width)
128
+ return image
129
+
130
+
131
+ class ConvertColor:
132
+ def __init__(self, mode='RGB'):
133
+ self.mode = mode
134
+
135
+ def __call__(self, image):
136
+ assert isinstance(image, Image.Image)
137
+ return image.convert(self.mode)
chugr/loader.py ADDED
@@ -0,0 +1,249 @@
1
+ import warnings
2
+ from functools import partial
3
+ from typing import Callable, List, Optional
4
+
5
+ from chugr.common import DataCfg, DataTaskCfg, DistributedCfg, LoaderBundle, source_to_shard_spec, SourceSpec
6
+ from chugr.hfds import create_loader_hf
7
+ from chugr.task_pipeline import create_task_pipeline
8
+ from chugr.wds import create_loader_wds, get_error_handler
9
+
10
+
11
+ def create_loader(
12
+ data_cfg: DataCfg,
13
+ task_cfg: DataTaskCfg,
14
+ task_pipeline: Optional[List[Callable]] = None,
15
+ is_training: bool = False,
16
+ start_interval: int = 0,
17
+ seed: int = 0,
18
+ distributed: DistributedCfg = DistributedCfg(),
19
+ ) -> LoaderBundle:
20
+ """
21
+ Creates a dataloader for training or validation based on configuration settings.
22
+
23
+ Parameters:
24
+ data_cfg: Configuration object for the dataset.
25
+ task_cfg: Configuration object for the task specific processing.
26
+ task_pipeline: Task specific processing pipeline (takes priority over task_cfg).
27
+ is_training : Indicates if the loader is for training data (True) or validation data (False).
28
+ start_interval: The starting interval (epoch for full passes) for setting seed, etc. appropriately.
29
+ seed: Seed for random operations to ensure reproducibility.
30
+ distributed: Distributed device information.
31
+
32
+ Returns:
33
+ DataLoader: A PyTorch DataLoader instance configured according to the provided settings.
34
+
35
+ Note:
36
+ Currently supports "wds" and "hf_dataset" as dataset formats.
37
+ """
38
+ if data_cfg.format == "wds":
39
+ loader = create_loader_from_config_wds(
40
+ data_cfg=data_cfg,
41
+ task_cfg=task_cfg,
42
+ task_pipeline=task_pipeline,
43
+ is_training=is_training,
44
+ start_interval=start_interval,
45
+ seed=seed,
46
+ distributed=distributed,
47
+ )
48
+
49
+ elif data_cfg.format.startswith("hf"):
50
+ loader = create_loader_from_config_hf(
51
+ data_cfg=data_cfg,
52
+ task_cfg=task_cfg,
53
+ task_pipeline=task_pipeline,
54
+ is_training=is_training,
55
+ start_interval=start_interval,
56
+ seed=seed,
57
+ distributed=distributed,
58
+ )
59
+
60
+ else:
61
+ assert False, f"Unsupported dataset format ({data_cfg.format})."
62
+
63
+ return loader
64
+
65
+
66
+ def _validate_cfgs(
67
+ data_cfg: DataCfg,
68
+ task_cfg: Optional[DataTaskCfg],
69
+ is_training: bool = False,
70
+ ):
71
+ batch_size = data_cfg.batch_size
72
+ if batch_size is not None:
73
+ if task_cfg.decode_and_process_fn is None:
74
+ # images must be processed into tensors to batch, text may be left as strings (collated as lists)
75
+ assert task_cfg.image_process_fn is not None, \
76
+ 'task_cfg.image_process_fn must be set if batching enabled'
77
+ else:
78
+ assert task_cfg.decode_fn is None, \
79
+ 'task_cfg.decode_fn should not be set at the same time as task_cfg.decode_and_process_fn'
80
+
81
+
82
+ def create_loader_from_config_wds(
83
+ data_cfg: DataCfg,
84
+ task_cfg: Optional[DataTaskCfg],
85
+ task_pipeline: Optional[List[Callable]] = None,
86
+ is_training: bool = False,
87
+ start_interval: int = 0,
88
+ seed: int = 0,
89
+ collate_fn: Optional[Callable] = None,
90
+ distributed: DistributedCfg = DistributedCfg(),
91
+ ):
92
+ """
93
+
94
+ Args:
95
+ data_cfg:
96
+ task_cfg:
97
+ task_pipeline:
98
+ is_training:
99
+ start_interval:
100
+ seed:
101
+ collate_fn:
102
+ distributed:
103
+
104
+ Returns:
105
+
106
+ """
107
+ _validate_cfgs(data_cfg, task_cfg, is_training=is_training)
108
+
109
+ if task_pipeline is None:
110
+ assert task_cfg is not None
111
+ task_pipeline = create_task_pipeline(
112
+ task_cfg,
113
+ )
114
+
115
+ handler = get_error_handler(task_cfg.error_handler)
116
+
117
+ naflex_scheduler = None
118
+ naflex_stage_factory = None
119
+ if is_training and data_cfg.naflex_seq_lens:
120
+ from chugr.naflex import LengthBucketer, NaFlexBatcher
121
+
122
+ assert data_cfg.num_samples, 'NaFlex batching requires num_samples (per interval).'
123
+ naflex_scheduler, processors, image_key = _create_naflex_batching(
124
+ data_cfg, task_cfg, num_samples=data_cfg.num_samples, seed=seed, world_size=distributed.world_size)
125
+
126
+ def naflex_stage_factory(interval):
127
+ stages = []
128
+ if data_cfg.length_bucketing:
129
+ stages.append(LengthBucketer(seed=seed, interval=interval))
130
+ stages.append(NaFlexBatcher(
131
+ naflex_scheduler, processors, image_key=image_key, interval=interval, handler=handler))
132
+ return stages
133
+
134
+ return create_loader_wds(
135
+ shards=data_cfg.shard_spec,
136
+ task_pipeline=task_pipeline,
137
+ num_samples=data_cfg.num_samples,
138
+ is_training=is_training,
139
+ resampled=data_cfg.resampled,
140
+ multi_interval=True, # FIXME via config?
141
+ num_workers=data_cfg.num_workers,
142
+ batch_size=data_cfg.batch_size,
143
+ persistent_workers=data_cfg.persistent_workers,
144
+ collate_fn=collate_fn,
145
+ start_interval=start_interval,
146
+ seed=seed,
147
+ handler=handler,
148
+ distributed=distributed,
149
+ naflex_scheduler=naflex_scheduler,
150
+ naflex_stage_factory=naflex_stage_factory,
151
+ )
152
+
153
+
154
+ def _create_naflex_batching(data_cfg: DataCfg, task_cfg: DataTaskCfg, num_samples: int, seed: int, world_size: int):
155
+ """ NaFlex batch schedule (for `num_samples` per interval), per seq_len task processors, and image key. """
156
+ from chugr.naflex import NaFlexBatchScheduler
157
+ from chugr.task_pipeline import create_task_processor
158
+
159
+ assert task_cfg.image_process_factory is not None, \
160
+ 'NaFlex batching requires task_cfg.image_process_factory (seq_len -> image process fn).'
161
+ max_tokens = data_cfg.naflex_max_tokens or (data_cfg.batch_size or 1) * max(data_cfg.naflex_seq_lens)
162
+ scheduler = NaFlexBatchScheduler(
163
+ seq_lens=data_cfg.naflex_seq_lens,
164
+ max_tokens_per_batch=max_tokens,
165
+ num_samples=num_samples,
166
+ seq_len_probs=data_cfg.naflex_seq_len_probs,
167
+ per_row_text_tokens=data_cfg.naflex_text_tokens,
168
+ batch_divisor=data_cfg.naflex_batch_divisor,
169
+ seed=seed,
170
+ world_size=world_size,
171
+ )
172
+ processors = {
173
+ s: create_task_processor(task_cfg, image_process_fn=task_cfg.image_process_factory(s))
174
+ for s in scheduler.seq_lens
175
+ }
176
+ image_key = getattr(task_cfg, 'image_input_feat').output_name
177
+ return scheduler, processors, image_key
178
+
179
+
180
+ def create_loader_from_config_hf(
181
+ data_cfg: DataCfg,
182
+ task_cfg: DataTaskCfg,
183
+ task_pipeline: Optional[List[Callable]] = None,
184
+ is_training: bool = False,
185
+ start_interval: int = 0,
186
+ seed: int = 0,
187
+ distributed: DistributedCfg = DistributedCfg(),
188
+ ):
189
+ """
190
+
191
+ Args:
192
+ data_cfg:
193
+ task_cfg:
194
+ task_pipeline:
195
+ is_training:
196
+ start_interval:
197
+ seed:
198
+ distributed:
199
+
200
+ Returns:
201
+
202
+ """
203
+ assert not isinstance(data_cfg.source, (list, tuple)), "Multiple sources not supported for HF datasets."
204
+ assert isinstance(data_cfg.source, (str, SourceSpec)), \
205
+ "The specified source for HF dataset must be a string or SourceSpec."
206
+
207
+ _validate_cfgs(data_cfg, task_cfg, is_training=is_training)
208
+
209
+ if isinstance(data_cfg.source, SourceSpec):
210
+ source = data_cfg.source.url
211
+ split = data_cfg.source.split
212
+ assert split, "Split must be set in SourceSpec with HF datasets."
213
+ else:
214
+ source = data_cfg.source
215
+ split = data_cfg.split
216
+ assert split, "Split must be set in DataCfg when string source is used with HF datasets."
217
+
218
+ if task_pipeline is None:
219
+ assert task_cfg is not None
220
+ task_pipeline = create_task_pipeline(
221
+ task_cfg,
222
+ )
223
+
224
+ streaming = 'hfids' in data_cfg.format
225
+
226
+ naflex_factory = None
227
+ if is_training and data_cfg.naflex_seq_lens:
228
+ # schedule driven NaFlex batching, the schedule is built once the dataset length is known (map-style)
229
+ if data_cfg.length_bucketing:
230
+ warnings.warn('length_bucketing is not supported for HF datasets, ignored.')
231
+ naflex_factory = partial(
232
+ _create_naflex_batching, data_cfg, task_cfg, seed=seed, world_size=distributed.world_size)
233
+
234
+ return create_loader_hf(
235
+ source=source,
236
+ split=split,
237
+ task_pipeline=task_pipeline,
238
+ streaming=streaming,
239
+ is_training=is_training,
240
+ batch_size=data_cfg.batch_size,
241
+ data_dir=data_cfg.data_dir,
242
+ num_samples=data_cfg.num_samples,
243
+ num_workers=data_cfg.num_workers,
244
+ persistent_workers=data_cfg.persistent_workers,
245
+ seed=seed,
246
+ distributed=distributed,
247
+ naflex_factory=naflex_factory,
248
+ handler=get_error_handler(task_cfg.error_handler),
249
+ )