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.
@@ -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
+