ialdev-dataman 0.1.0__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.
- iad/dataman/__init__.py +22 -0
- iad/dataman/datacast/__init__.py +8 -0
- iad/dataman/datacast/caster.py +523 -0
- iad/dataman/datacast/collect.py +1466 -0
- iad/dataman/datacast/labeled.py +80 -0
- iad/dataman/datacast/scan.py +178 -0
- iad/dataman/datacast/transforms.py +40 -0
- iad/dataman/datacast/transtools.py +608 -0
- iad/dataman/env.py +10 -0
- iad/dataman/factories.py +143 -0
- iad/dataman/models.py +425 -0
- iad/dataman/resman.py +1229 -0
- ialdev_dataman-0.1.0.dist-info/METADATA +33 -0
- ialdev_dataman-0.1.0.dist-info/RECORD +15 -0
- ialdev_dataman-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LabelRules:
|
|
10
|
+
"""Class encapsulates labeling rules in specific domain.
|
|
11
|
+
The rules are loaded from domains labeling definition yaml file,
|
|
12
|
+
which lists domain-specific labels categories and optionally
|
|
13
|
+
the corresponding categorical values.
|
|
14
|
+
|
|
15
|
+
Default domains definition is stored inside this package in 'labels.yml'
|
|
16
|
+
with full path accessible as `LabelRules._default_domains_yaml`.
|
|
17
|
+
|
|
18
|
+
Domain definition may include special _common_ domain (name in
|
|
19
|
+
`LabelRules._common_domain`) with other domains inheriting from it.
|
|
20
|
+
|
|
21
|
+
Available domains may be listed using _static_ `LabelRules.domains()` method.
|
|
22
|
+
"""
|
|
23
|
+
_common_domain = 'common'
|
|
24
|
+
|
|
25
|
+
def __init__(self, domain, path=None):
|
|
26
|
+
"""Create specific domain labeling rules from
|
|
27
|
+
given or default domains definition file.
|
|
28
|
+
|
|
29
|
+
:param domain: name of the domain to use
|
|
30
|
+
:param path: path to a custom domains file
|
|
31
|
+
"""
|
|
32
|
+
dms = self.load_domains(path)
|
|
33
|
+
if domain not in dms:
|
|
34
|
+
raise KeyError(f'Domain {domain} is not defined among {list(dms)}!')
|
|
35
|
+
|
|
36
|
+
self.labels = getattr(dms, self._common_domain, {}).copy() # type: dict
|
|
37
|
+
self.labels.update(dms[domain])
|
|
38
|
+
self.domain = domain
|
|
39
|
+
for cat in self.labels: # cast categorical as sets for fast searches
|
|
40
|
+
self.labels[cat] = set(self.labels[cat])
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def domains(path=None):
|
|
44
|
+
dms = LabelRules.load_domains(path)
|
|
45
|
+
dms.pop(LabelRules._common_domain, {})
|
|
46
|
+
return list(dms.keys())
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def load_domains(path=None):
|
|
50
|
+
"""Load domains conventions"""
|
|
51
|
+
from iad.core.param import TBox
|
|
52
|
+
from iad.dataman.env import EnvLoc
|
|
53
|
+
# Consider: is that the right approach ? What if RES_LOC missing and path is None?
|
|
54
|
+
path = path or (EnvLoc.RESOURCES / 'schemes').first_file('labels.yml')
|
|
55
|
+
dms = TBox.from_yaml(filename=str(path))
|
|
56
|
+
return dms
|
|
57
|
+
|
|
58
|
+
def __contains__(self, cat):
|
|
59
|
+
return cat in self.labels
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, cat):
|
|
62
|
+
if cat not in self:
|
|
63
|
+
raise KeyError(f'Unknown category {cat} in the domain {self.domain}')
|
|
64
|
+
return self.labels[cat]
|
|
65
|
+
|
|
66
|
+
def in_cat(self, cat: str, item) -> bool:
|
|
67
|
+
"""Checks if item is defined in the given label category.
|
|
68
|
+
:param cat: name of the category
|
|
69
|
+
:param item: item to be found in this category
|
|
70
|
+
|
|
71
|
+
:return True if label is categorical indicates if item
|
|
72
|
+
is among the defined categories
|
|
73
|
+
Otherwise returns None
|
|
74
|
+
"""
|
|
75
|
+
items = self[cat]
|
|
76
|
+
return item in items if items else True
|
|
77
|
+
|
|
78
|
+
def __repr__(self):
|
|
79
|
+
return f"Labeling Rules for {self.domain} domain\n" \
|
|
80
|
+
f"Labels categories: {list(self.labels)}"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from time import time
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
|
|
7
|
+
import pydantic.v1 as pydantic
|
|
8
|
+
import regex as re
|
|
9
|
+
from pydantic.v1 import PrivateAttr
|
|
10
|
+
|
|
11
|
+
from iad.core.param import YamlModel
|
|
12
|
+
from iad.core import logger
|
|
13
|
+
from iad.core.filesproc import normalize
|
|
14
|
+
from iad.core.paths import TransPath
|
|
15
|
+
|
|
16
|
+
_log = logger('datacast.scan')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GuideScan(YamlModel, hash_exclude=['_pather', '_match_skip_folders']):
|
|
20
|
+
"""
|
|
21
|
+
Constrain-guided scanner for folders tree.
|
|
22
|
+
|
|
23
|
+
Produces iterator over labels extracted from files matching specific pattern.
|
|
24
|
+
Scanning may is guided by additional search parameters:
|
|
25
|
+
::
|
|
26
|
+
pattern: regular expression to search
|
|
27
|
+
ignore_case: set case-sensitivity of the matching
|
|
28
|
+
method: math as: full path | root-relative part | any path at the end
|
|
29
|
+
|
|
30
|
+
skip_folders: regex describing `names` of folders to skip (default skips starting with . or _)
|
|
31
|
+
skip_after_match: Skip other file in the folder
|
|
32
|
+
skip_under_match: Skip folders under matched files
|
|
33
|
+
|
|
34
|
+
max_depth: Skip folders with larger depth under the root
|
|
35
|
+
max_folders:Skip folders containing many sub-folders
|
|
36
|
+
max_files: Skip folders containing many files
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
Field = pydantic.Field
|
|
40
|
+
|
|
41
|
+
# -------------------------------------
|
|
42
|
+
pattern: str
|
|
43
|
+
test_url: Optional[pydantic.AnyUrl]
|
|
44
|
+
ignore_case: bool = False
|
|
45
|
+
|
|
46
|
+
method: Literal['relative', 'full', 'end'] = Field('relative', description=(
|
|
47
|
+
'Match pattern with: full path | root-relative part | any path at the end'))
|
|
48
|
+
skip_folders: str = r"^[._].*"
|
|
49
|
+
skip_after_match: bool = Field(False, description="Skip other file in the folder")
|
|
50
|
+
skip_under_match: bool = Field(False, description="Skip folders under matched files")
|
|
51
|
+
|
|
52
|
+
max_depth: int = Field(4, description="Skip folders with larger depth under the root")
|
|
53
|
+
max_folders: int = Field(None, description="Skip folders containing many sub-folders")
|
|
54
|
+
max_files: int = Field(None, description="Skip folders containing many files")
|
|
55
|
+
|
|
56
|
+
samples: list[str] = Field(None, description="List of sample paths for validation")
|
|
57
|
+
# ---------------------------------------------------
|
|
58
|
+
_pather: PrivateAttr = PrivateAttr()
|
|
59
|
+
_match_skip_folders: PrivateAttr = PrivateAttr()
|
|
60
|
+
|
|
61
|
+
def verify_samples(self, fail=False):
|
|
62
|
+
"""Return list of sample failed parsing"""
|
|
63
|
+
total = self.samples and len(self.samples) or 0
|
|
64
|
+
failed = [s for s in self.samples if self._pather.regex.parse(s) is None] if total else []
|
|
65
|
+
if num := len(failed):
|
|
66
|
+
msg = (f"Pattern {self._pather.regex.regex.pattern}\n\tfailed parsing "
|
|
67
|
+
f"{num} of {total=} samples:\n\t{failed}")
|
|
68
|
+
if fail:
|
|
69
|
+
raise ValueError(msg)
|
|
70
|
+
else:
|
|
71
|
+
_log.error(msg)
|
|
72
|
+
return len(failed), total, failed
|
|
73
|
+
|
|
74
|
+
def __init__(self, **kwargs):
|
|
75
|
+
super().__init__(**kwargs)
|
|
76
|
+
flags = self.ignore_case and re.IGNORECASE or 0
|
|
77
|
+
self._pather = TransPath(self.pattern, flags=flags)
|
|
78
|
+
self.verify_samples(fail=True)
|
|
79
|
+
self._match_skip_folders = self.skip_folders and re.compile(self.skip_folders).fullmatch
|
|
80
|
+
|
|
81
|
+
def scanner(self, root, win_to_posix: bool):
|
|
82
|
+
"""
|
|
83
|
+
Return generator of labels parsed from matching files under the root
|
|
84
|
+
:param root: path to search under
|
|
85
|
+
:param win_to_posix: if `True` convert files paths into posix before matching and parsing.
|
|
86
|
+
Allowed only in 'relative' matching method.
|
|
87
|
+
:return: Generator of parsed labels for found matching files.
|
|
88
|
+
"""
|
|
89
|
+
root = normalize(root, out=str)
|
|
90
|
+
if win_to_posix and self.method not in (sup := ['relative', 'end']):
|
|
91
|
+
raise ValueError(f"win_to_posix is compatible only if method in {sup}")
|
|
92
|
+
crop = len(root) + 1 if self.method == 'relative' else 0
|
|
93
|
+
method = 'search' if self.method == 'end' else 'fullmatch'
|
|
94
|
+
parse = self._pather.regex.parse
|
|
95
|
+
|
|
96
|
+
def match(p: str):
|
|
97
|
+
"""Match closure tailored for specific root cropping"""
|
|
98
|
+
if win_to_posix:
|
|
99
|
+
p = p.replace('\\', '/')
|
|
100
|
+
if (m := parse(crop and p[crop:] or p, method=method)) is not None:
|
|
101
|
+
return m | {'path': p}
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
_log.debug(f"๐Scanning for {self._pather.form} in {root}")
|
|
105
|
+
return self._walk(root, match=match)
|
|
106
|
+
|
|
107
|
+
def _walk(self, root, *, match, level=0):
|
|
108
|
+
"""
|
|
109
|
+
Iterate over files and/or dirs under the given `root` folder.
|
|
110
|
+
:param root: the folder to start from
|
|
111
|
+
:param level: level of the caller
|
|
112
|
+
:returns: Iterator over dictionaries with labels extracted by parsing
|
|
113
|
+
"""
|
|
114
|
+
# to optimize the "for entries" loop as much as possible all the
|
|
115
|
+
# per entry access calculations are assigned to variables:
|
|
116
|
+
t0 = time()
|
|
117
|
+
ms = lambda: f"{(time() - t0) * 1000:.2f}ms"
|
|
118
|
+
|
|
119
|
+
do_log = _log.isEnabledFor(_log.DEBUG)
|
|
120
|
+
max_dirs, max_files = self.max_folders, self.max_files
|
|
121
|
+
skip_after_match, skip_under_match = self.skip_after_match, self.skip_under_match
|
|
122
|
+
|
|
123
|
+
any_skip = skip_under_match or skip_after_match
|
|
124
|
+
check_skip_folders = bool(self.skip_folders)
|
|
125
|
+
check_dirs = (level := level + 1) < self.max_depth # False stops entering sub-folders
|
|
126
|
+
|
|
127
|
+
files, folders = [], []
|
|
128
|
+
collect_files = bool(max_files)
|
|
129
|
+
check_files = True
|
|
130
|
+
n_dirs, n_files, n_matches = 0, 0, 0
|
|
131
|
+
|
|
132
|
+
def skip():
|
|
133
|
+
nonlocal check_files, check_dirs
|
|
134
|
+
if skip_after_match: check_files = False
|
|
135
|
+
if skip_under_match: check_dirs = False
|
|
136
|
+
if not (check_dirs or check_files):
|
|
137
|
+
do_log and _log.debug(f'Dropped ๐ง (after: {skip_after_match}, '
|
|
138
|
+
f'under {skip_after_match}) match in ๐ฟ{root}')
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
with os.scandir(root) as entries:
|
|
143
|
+
n = 0
|
|
144
|
+
for n, entry in enumerate(entries, 1):
|
|
145
|
+
if entry.is_dir():
|
|
146
|
+
n_dirs += 1
|
|
147
|
+
if max_dirs and n_dirs >= max_dirs:
|
|
148
|
+
do_log and _log.debug(f'Dropped ๐ง at {n_dirs=} ({ms()}) ๐ฟ{root}')
|
|
149
|
+
return
|
|
150
|
+
# check_dirs may be False after skip_under_match!
|
|
151
|
+
if check_dirs and not (check_skip_folders and self._match_skip_folders(entry.name)):
|
|
152
|
+
folders.append(entry.path)
|
|
153
|
+
elif check_files: # block on after match in no-collect mode
|
|
154
|
+
n_files += 1
|
|
155
|
+
if max_files and n_files >= max_files:
|
|
156
|
+
do_log and _log.debug(f'Dropped ๐ง at {n_files=} ({ms()}) ๐ฟ{root}')
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if collect_files:
|
|
160
|
+
files.append(entry.path)
|
|
161
|
+
elif m := match(entry.path):
|
|
162
|
+
yield m # process files on the fly
|
|
163
|
+
n_matches += 1
|
|
164
|
+
if any_skip and skip(): break
|
|
165
|
+
|
|
166
|
+
for path in files: # if collect_files is False, then files == []!
|
|
167
|
+
if m := match(path):
|
|
168
|
+
yield m
|
|
169
|
+
n_matches += 1
|
|
170
|
+
if any_skip and skip(): break
|
|
171
|
+
|
|
172
|
+
do_log and _log.debug(f'{level}โฉ Scanned{n:3d} '
|
|
173
|
+
f'({n_dirs}๐ฟ+{n_files}๐ !{n_matches}) in {ms()} in ๐ฟ{root}')
|
|
174
|
+
if check_dirs and folders:
|
|
175
|
+
t0 = time()
|
|
176
|
+
for folder in folders:
|
|
177
|
+
yield from self._walk(folder, match=match, level=level)
|
|
178
|
+
do_log and _log.debug(f'โฎฑ {len(folders)} sub-folders scanned in {ms()} in ๐ฟ{root}')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------
|
|
2
|
+
# Implementation if specific transforms
|
|
3
|
+
# ----------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Iterable, Union, Tuple
|
|
7
|
+
|
|
8
|
+
# imports below bring into the namespace functions to be available as transforms
|
|
9
|
+
# and exported accordingly by this module.
|
|
10
|
+
|
|
11
|
+
from numpy import squeeze, number
|
|
12
|
+
from iad.img.transforms import gamma, shot_noise, norm, take_ch, alpha_blend
|
|
13
|
+
from iad.img.tools import center_crop
|
|
14
|
+
|
|
15
|
+
__all__ = ['gamma', 'shot_noise', 'norm', 'center_crop',
|
|
16
|
+
'squeeze', 'regions', 'recode', 'take_ch', 'alpha_blend']
|
|
17
|
+
|
|
18
|
+
transform_module = globals()
|
|
19
|
+
|
|
20
|
+
Number = Union[float, int, number]
|
|
21
|
+
Pair = Tuple[Number, Number]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def recode(im, from_to: Union[Pair, Iterable[Pair]]):
|
|
25
|
+
"""replace values in the im according to the codes map"""
|
|
26
|
+
if not from_to:
|
|
27
|
+
return im
|
|
28
|
+
if not hasattr(from_to[0], '__len__'):
|
|
29
|
+
from_to = [from_to]
|
|
30
|
+
for src, trg in from_to:
|
|
31
|
+
im[im == src] = trg
|
|
32
|
+
return im
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def regions(im, **rgn_codes):
|
|
36
|
+
from iad.img.regions import Regions
|
|
37
|
+
"""Create Regions object from the encoded image data."""
|
|
38
|
+
return Regions(im, rgn_codes=rgn_codes)
|
|
39
|
+
|
|
40
|
+
|