swcgeom 0.11.1__py3-none-any.whl → 0.13.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.

Potentially problematic release.


This version of swcgeom might be problematic. Click here for more details.

Files changed (56) hide show
  1. swcgeom/__init__.py +4 -4
  2. swcgeom/_version.py +14 -2
  3. swcgeom/analysis/__init__.py +7 -7
  4. swcgeom/analysis/branch_features.py +1 -1
  5. swcgeom/analysis/feature_extractor.py +25 -12
  6. swcgeom/analysis/node_features.py +1 -1
  7. swcgeom/analysis/path_features.py +1 -1
  8. swcgeom/analysis/sholl.py +11 -7
  9. swcgeom/analysis/trunk.py +5 -5
  10. swcgeom/analysis/visualization.py +2 -2
  11. swcgeom/analysis/volume.py +80 -0
  12. swcgeom/core/__init__.py +9 -9
  13. swcgeom/core/branch.py +8 -4
  14. swcgeom/core/branch_tree.py +4 -5
  15. swcgeom/core/node.py +5 -3
  16. swcgeom/core/path.py +6 -3
  17. swcgeom/core/population.py +2 -2
  18. swcgeom/core/segment.py +8 -4
  19. swcgeom/core/swc.py +24 -3
  20. swcgeom/core/swc_utils/__init__.py +6 -6
  21. swcgeom/core/swc_utils/assembler.py +2 -2
  22. swcgeom/core/swc_utils/base.py +30 -1
  23. swcgeom/core/swc_utils/checker.py +30 -6
  24. swcgeom/core/swc_utils/io.py +31 -30
  25. swcgeom/core/swc_utils/normalizer.py +1 -1
  26. swcgeom/core/swc_utils/subtree.py +1 -1
  27. swcgeom/core/tree.py +38 -14
  28. swcgeom/core/tree_utils.py +47 -41
  29. swcgeom/core/tree_utils_impl.py +39 -0
  30. swcgeom/images/__init__.py +2 -2
  31. swcgeom/images/folder.py +2 -2
  32. swcgeom/images/io.py +48 -9
  33. swcgeom/transforms/__init__.py +10 -8
  34. swcgeom/transforms/branch.py +3 -3
  35. swcgeom/transforms/geometry.py +11 -4
  36. swcgeom/transforms/image_stack.py +3 -3
  37. swcgeom/transforms/images.py +1 -1
  38. swcgeom/transforms/mst.py +68 -13
  39. swcgeom/transforms/path.py +48 -0
  40. swcgeom/transforms/population.py +2 -2
  41. swcgeom/transforms/tree.py +18 -9
  42. swcgeom/transforms/tree_assembler.py +7 -4
  43. swcgeom/utils/__init__.py +10 -7
  44. swcgeom/utils/dsu.py +42 -0
  45. swcgeom/utils/file.py +91 -0
  46. swcgeom/utils/geometry_object.py +299 -0
  47. swcgeom/utils/neuromorpho.py +33 -11
  48. swcgeom/utils/renderer.py +5 -4
  49. swcgeom/utils/transforms.py +26 -1
  50. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/METADATA +8 -8
  51. swcgeom-0.13.0.dist-info/RECORD +61 -0
  52. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/WHEEL +1 -1
  53. swcgeom-0.11.1.dist-info/RECORD +0 -55
  54. /swcgeom/utils/{numpy.py → numpy_helper.py} +0 -0
  55. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/LICENSE +0 -0
  56. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/top_level.txt +0 -0
@@ -4,25 +4,25 @@ import re
4
4
  import warnings
5
5
  from typing import Callable, Iterable, List, Literal, Optional, Tuple
6
6
 
7
- import chardet
8
7
  import numpy as np
9
8
  import numpy.typing as npt
10
9
  import pandas as pd
11
10
 
12
- from .base import SWCNames, get_names
13
- from .checker import is_single_root
14
- from .normalizer import (
11
+ from swcgeom.core.swc_utils.base import SWCNames, get_names
12
+ from swcgeom.core.swc_utils.checker import is_single_root
13
+ from swcgeom.core.swc_utils.normalizer import (
15
14
  link_roots_to_nearest_,
16
15
  mark_roots_as_somas_,
17
16
  reset_index_,
18
17
  sort_nodes_,
19
18
  )
19
+ from swcgeom.utils import FileReader, PathOrIO
20
20
 
21
21
  __all__ = ["read_swc", "to_swc"]
22
22
 
23
23
 
24
24
  def read_swc(
25
- swc_file: str,
25
+ swc_file: PathOrIO,
26
26
  extra_cols: Optional[Iterable[str]] = None,
27
27
  fix_roots: Literal["somas", "nearest", False] = False,
28
28
  sort_nodes: bool = False,
@@ -35,7 +35,7 @@ def read_swc(
35
35
 
36
36
  Parameters
37
37
  ----------
38
- swc_file : str
38
+ swc_file : PathOrIO
39
39
  Path of swc file, the id should be consecutively incremented.
40
40
  extra_cols : Iterable[str], optional
41
41
  Read more cols in swc file.
@@ -47,7 +47,7 @@ def read_swc(
47
47
  reset_index : bool, default `True`
48
48
  Reset node index to start with zero, DO NOT set to false if
49
49
  you are not sure what will happend.
50
- encoding : str, default `utf-8`
50
+ encoding : str | 'detect', default `utf-8`
51
51
  The name of the encoding used to decode the file. If is
52
52
  `detect`, we will try to detect the character encoding.
53
53
  names : SWCNames, optional
@@ -59,19 +59,6 @@ def read_swc(
59
59
  """
60
60
 
61
61
  names = get_names(names)
62
-
63
- if encoding == "detect":
64
- with open(swc_file, "rb") as f:
65
- data = f.read()
66
-
67
- result = chardet.detect(data)
68
- encoding = result["encoding"] or "utf-8"
69
- if result["confidence"] < 0.9:
70
- warnings.warn(
71
- f"parse as `{encoding}` with low confidence "
72
- f"{result['confidence']} in `{swc_file}`"
73
- )
74
-
75
62
  df, comments = parse_swc(
76
63
  swc_file, names=names, extra_cols=extra_cols, encoding=encoding
77
64
  )
@@ -145,14 +132,23 @@ RE_FLOAT = r"([+-]?(?:\d+(?:[.]\d*)?(?:[eE][+-]?\d+)?|[.]\d+(?:[eE][+-]?\d+)?))"
145
132
 
146
133
 
147
134
  def parse_swc(
148
- swc_file: str,
135
+ fname: PathOrIO,
149
136
  *,
150
137
  names: SWCNames,
151
138
  extra_cols: Iterable[str] | None = None,
152
- encoding: str = "utf-8",
139
+ encoding: Literal["detect"] | str = "utf-8",
153
140
  ) -> Tuple[pd.DataFrame, List[str]]:
154
141
  """Parse swc file.
155
142
 
143
+ Parameters
144
+ ----------
145
+ fname : PathOrIO
146
+ names : SWCNames
147
+ extra_cols : list of str, optional
148
+ encoding : str | 'detect', default `utf-8`
149
+ The name of the encoding used to decode the file. If is
150
+ `detect`, we will try to detect the character encoding.
151
+
156
152
  Returns
157
153
  -------
158
154
  df : ~pandas.DataFrame
@@ -183,17 +179,19 @@ def parse_swc(
183
179
  # neuromorpho.org. More fields at the end is allowed, such as
184
180
  # reading eswc as swc, but with a warning.
185
181
  re_swc = re.compile(rf"^\s*{re_swc_cols_str}\s*([\s+-.0-9]*)$")
182
+
186
183
  last_group = 7 + len(extras) + 1
184
+ ignored_comment = f"# {' '.join(names.cols())}"
185
+ flag = True
187
186
 
188
187
  comments = []
189
- try:
190
- with open(swc_file, "r", encoding=encoding) as f:
191
- flag = True
188
+ with FileReader(fname, encoding=encoding) as f:
189
+ try:
192
190
  for i, line in enumerate(f):
193
191
  if (match := re_swc.search(line)) is not None:
194
192
  if flag and match.group(last_group):
195
193
  warnings.warn(
196
- f"some fields are ignored in row {i} of `{swc_file}`"
194
+ f"some fields are ignored in row {i+1} of `{fname}`"
197
195
  )
198
196
  flag = False
199
197
 
@@ -201,11 +199,14 @@ def parse_swc(
201
199
  vals[i].append(trans(match.group(i + 1)))
202
200
  elif match := RE_COMMENT.match(line):
203
201
  comment = line[len(match.group(0)) :].removesuffix("\n")
204
- comments.append(comment)
202
+ if not comment.startswith(ignored_comment):
203
+ comments.append(comment)
205
204
  elif not line.isspace():
206
- raise ValueError(f"invalid row {i} in `{swc_file}`")
207
- except UnicodeDecodeError as e:
208
- raise ValueError(f"encoding error in `{swc_file}`") from e
205
+ raise ValueError(f"invalid row {i+1} in `{fname}`")
206
+ except UnicodeDecodeError as e:
207
+ raise ValueError(
208
+ f"decode failed, try to enable auto detect `encoding='detect'`"
209
+ ) from e
209
210
 
210
211
  df = pd.DataFrame.from_dict(dict(zip(keys, vals)))
211
212
  return df, comments
@@ -9,7 +9,7 @@ import numpy as np
9
9
  import numpy.typing as npt
10
10
  import pandas as pd
11
11
 
12
- from .base import SWCNames, Topology, get_dsu, get_names
12
+ from swcgeom.core.swc_utils.base import SWCNames, Topology, get_dsu, get_names
13
13
 
14
14
  __all__ = [
15
15
  "mark_roots_as_somas",
@@ -11,7 +11,7 @@ from typing import Tuple, cast
11
11
  import numpy as np
12
12
  import numpy.typing as npt
13
13
 
14
- from .base import Topology, traverse
14
+ from swcgeom.core.swc_utils.base import Topology, traverse
15
15
 
16
16
  __all__ = ["REMOVAL", "to_sub_topology", "propagate_removal"]
17
17
 
swcgeom/core/tree.py CHANGED
@@ -22,13 +22,14 @@ import numpy.typing as npt
22
22
  import pandas as pd
23
23
  from typing_extensions import Self
24
24
 
25
- from ..utils import padding1d
26
- from .branch import Branch
27
- from .node import Node
28
- from .path import Path
29
- from .segment import Segment, Segments
30
- from .swc import DictSWC, eswc_cols
31
- from .swc_utils import SWCNames, get_names, read_swc, traverse
25
+ from swcgeom.core.branch import Branch
26
+ from swcgeom.core.node import Node
27
+ from swcgeom.core.path import Path
28
+ from swcgeom.core.segment import Segment, Segments
29
+ from swcgeom.core.swc import DictSWC, eswc_cols
30
+ from swcgeom.core.swc_utils import SWCNames, get_names, read_swc, traverse
31
+ from swcgeom.core.tree_utils_impl import get_subtree_impl
32
+ from swcgeom.utils import PathOrIO, padding1d
32
33
 
33
34
  __all__ = ["Tree"]
34
35
 
@@ -68,13 +69,21 @@ class Tree(DictSWC):
68
69
 
69
70
  return Tree.Branch(self.attach, [n.id for n in ns])
70
71
 
71
- def is_soma(self) -> bool:
72
- return self.id == 0
73
-
74
72
  def radial_distance(self) -> float:
75
73
  """The end-to-end straight-line distance to soma."""
76
74
  return self.distance(self.attach.soma())
77
75
 
76
+ def subtree(self) -> "Tree":
77
+ """Get subtree from node."""
78
+ n_nodes, ndata, source, names = get_subtree_impl(self.attach, self.id)
79
+ return Tree(n_nodes, **ndata, source=source, names=names)
80
+
81
+ def is_root(self) -> bool:
82
+ return self.parent() is None
83
+
84
+ def is_soma(self) -> bool: # TODO: support multi soma, e.g. 3 points
85
+ return self.type == self.attach.types.soma and self.is_root()
86
+
78
87
  # fmt: off
79
88
  @overload
80
89
  def traverse(self, *, enter: Callable[[Node, T | None], T], mode: Literal["dfs"] = ...) -> None: ...
@@ -181,8 +190,13 @@ class Tree(DictSWC):
181
190
  def node(self, idx: int | np.integer) -> Node:
182
191
  return self.Node(self, idx)
183
192
 
184
- def soma(self) -> Node:
185
- return self.node(0)
193
+ def soma(self, type_check: bool = True) -> Node:
194
+ """Get soma of neuron."""
195
+ # TODO: find soma, see also: https://neuromorpho.org/myfaq.jsp
196
+ n = self.node(0)
197
+ if type_check and n.type != self.types.soma:
198
+ raise ValueError(f"no soma found in: {self.source}")
199
+ return n
186
200
 
187
201
  def get_bifurcations(self) -> List[Node]:
188
202
  """Get all node of bifurcations."""
@@ -246,6 +260,16 @@ class Tree(DictSWC):
246
260
  paths = self.traverse(enter=assign_path, leave=collect_path)
247
261
  return [self.Path(self, idx) for idx in paths]
248
262
 
263
+ def get_neurites(self, type_check: bool = True) -> Iterable[Self]:
264
+ """Get neurites from soma."""
265
+ return (n.subtree() for n in self.soma(type_check).children())
266
+
267
+ def get_dendrites(self, type_check: bool = True) -> Iterable[Self]:
268
+ """Get dendrites."""
269
+ types = [self.types.apical_dendrite, self.types.basal_dendrite]
270
+ children = self.soma(type_check).children()
271
+ return (n.subtree() for n in children if n.type in types)
272
+
249
273
  # fmt: off
250
274
  @overload
251
275
  def traverse(self, *,
@@ -309,7 +333,7 @@ class Tree(DictSWC):
309
333
  return tree
310
334
 
311
335
  @classmethod
312
- def from_swc(cls, swc_file: str, **kwargs) -> Self:
336
+ def from_swc(cls, swc_file: PathOrIO, **kwargs) -> Self:
313
337
  """Read neuron tree from swc file.
314
338
 
315
339
  See Also
@@ -322,7 +346,7 @@ class Tree(DictSWC):
322
346
  except Exception as e: # pylint: disable=broad-except
323
347
  raise ValueError(f"fails to read swc: {swc_file}") from e
324
348
 
325
- source = os.path.abspath(swc_file)
349
+ source = os.path.abspath(swc_file) if isinstance(swc_file, str) else ""
326
350
  return cls.from_data_frame(df, source=source, comments=comments)
327
351
 
328
352
  @classmethod
@@ -5,8 +5,8 @@ from typing import Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, ove
5
5
 
6
6
  import numpy as np
7
7
 
8
- from .swc import SWCLike
9
- from .swc_utils import (
8
+ from swcgeom.core.swc import SWCLike
9
+ from swcgeom.core.swc_utils import (
10
10
  REMOVAL,
11
11
  SWCNames,
12
12
  Topology,
@@ -15,9 +15,9 @@ from .swc_utils import (
15
15
  propagate_removal,
16
16
  sort_nodes_impl,
17
17
  to_sub_topology,
18
- traverse,
19
18
  )
20
- from .tree import Tree
19
+ from swcgeom.core.tree import Tree
20
+ from swcgeom.core.tree_utils_impl import get_subtree_impl, to_subtree_impl
21
21
 
22
22
  __all__ = [
23
23
  "sort_tree",
@@ -120,8 +120,7 @@ def to_sub_tree(swc_like: SWCLike, sub: Topology) -> Tuple[Tree, Dict[int, int]]
120
120
  ndata = {k: swc_like.get_ndata(k)[id_map_arr].copy() for k in swc_like.keys()}
121
121
  ndata.update(id=new_id, pid=new_pid)
122
122
 
123
- subtree = Tree(n_nodes, **ndata, names=swc_like.names)
124
- subtree.source = swc_like.source
123
+ subtree = Tree(n_nodes, **ndata, source=swc_like.source, names=swc_like.names)
125
124
 
126
125
  id_map = {}
127
126
  for i, idx in enumerate(id_map_arr):
@@ -143,7 +142,8 @@ def to_subtree(swc_like: SWCLike, removals: Iterable[int]) -> Tree:
143
142
  new_ids[i] = REMOVAL
144
143
 
145
144
  sub = propagate_removal((new_ids, swc_like.pid()))
146
- return _to_subtree(swc_like, sub)
145
+ n_nodes, ndata, source, names = to_subtree_impl(swc_like, sub)
146
+ return Tree(n_nodes, **ndata, source=source, names=names)
147
147
 
148
148
 
149
149
  def get_subtree(swc_like: SWCLike, n: int) -> Tree:
@@ -155,14 +155,8 @@ def get_subtree(swc_like: SWCLike, n: int) -> Tree:
155
155
  n : int
156
156
  Id of the root of the subtree.
157
157
  """
158
- ids = []
159
- topo = (swc_like.id(), swc_like.pid())
160
- traverse(topo, enter=lambda n, _: ids.append(n), root=n)
161
-
162
- sub_ids = np.array(ids, dtype=np.int32)
163
- sub_pid = swc_like.pid()[sub_ids]
164
- sub_pid[0] = -1
165
- return _to_subtree(swc_like, (sub_ids, sub_pid))
158
+ n_nodes, ndata, source, names = get_subtree_impl(swc_like, n)
159
+ return Tree(n_nodes, **ndata, source=source, names=names)
166
160
 
167
161
 
168
162
  def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
@@ -179,9 +173,11 @@ def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
179
173
  """
180
174
  tree = tree.copy()
181
175
  path = [tree.node(new_root)]
182
- while (p := path[-1]).parent() is not None:
176
+ while (p := path[-1].parent()) is not None:
183
177
  path.append(p)
184
178
 
179
+ path[0].pid = -1
180
+ path[0].type, path[-1].type = path[-1].type, path[0].type
185
181
  for n, p in zip(path[1:], path[:-1]):
186
182
  n.pid = p.id
187
183
 
@@ -194,11 +190,12 @@ def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
194
190
  def cat_tree( # pylint: disable=too-many-arguments
195
191
  tree1: Tree,
196
192
  tree2: Tree,
197
- node1: int,
193
+ node1: int = 0,
198
194
  node2: int = 0,
199
195
  *,
200
- no_move: bool = False,
196
+ translate: bool = True,
201
197
  names: Optional[SWCNames] = None,
198
+ no_move: Optional[bool] = None, # legacy
202
199
  ) -> Tree:
203
200
  """Concatenates the second tree onto the first one.
204
201
 
@@ -206,31 +203,44 @@ def cat_tree( # pylint: disable=too-many-arguments
206
203
  ---------
207
204
  tree1 : Tree
208
205
  tree2 : Tree
209
- node1 : int
206
+ node1 : int, default `0`
210
207
  The node id of the tree to be connected.
211
208
  node2 : int, default `0`
212
209
  The node id of the connection point.
213
- no_move : bool, default `False`
214
- If true, link connection point without move.
210
+ translate : bool, default `True`
211
+ Wheather to translate node_2 to node_1. If False, add link
212
+ between node_1 and node_2 without translate.
215
213
  """
214
+ if no_move is not None:
215
+ warnings.warn(
216
+ "`no_move` has been, it is replaced by `translate` in "
217
+ "v0.12.0, and this will be removed in next version",
218
+ DeprecationWarning,
219
+ )
220
+ translate = not no_move
221
+
216
222
  names = get_names(names)
217
223
  tree, tree2 = tree1.copy(), tree2.copy()
218
- if not tree2.node(node2).is_soma():
224
+ if not tree2.node(node2).is_root():
219
225
  tree2 = redirect_tree(tree2, node2, sort=False)
220
226
 
221
227
  c = tree.node(node1)
222
- if not no_move:
228
+ if translate:
223
229
  tree2.ndata[names.x] -= tree2.node(node2).x - c.x
224
230
  tree2.ndata[names.y] -= tree2.node(node2).y - c.y
225
231
  tree2.ndata[names.z] -= tree2.node(node2).z - c.z
226
232
 
227
- tree2.ndata[names.id] += tree.number_of_nodes()
228
- tree2.ndata[names.pid] += tree.number_of_nodes()
233
+ ns = tree.number_of_nodes()
229
234
  if np.linalg.norm(tree2.node(node2).xyz() - c.xyz()) < EPS:
230
- for n in tree2.node(node2).children():
231
- n.pid = node1
235
+ remove = [node2 + ns]
236
+ link_to_root = [n.id + ns for n in tree2.node(node2).children()]
232
237
  else:
233
- tree2.node(node2).pid = node1
238
+ remove = None
239
+ link_to_root = [node2 + ns]
240
+
241
+ # APIs of tree2 are no longer available since we modify the topology
242
+ tree2.ndata[names.id] += ns
243
+ tree2.ndata[names.pid] += ns
234
244
 
235
245
  for k, v in tree.ndata.items(): # only keep keys in tree1
236
246
  if k in tree2.ndata:
@@ -238,7 +248,15 @@ def cat_tree( # pylint: disable=too-many-arguments
238
248
  else:
239
249
  tree.ndata[k] = np.pad(v, (0, tree2.number_of_nodes()))
240
250
 
241
- return _sort_tree(tree)
251
+ for n in link_to_root:
252
+ tree.node(n).pid = node1
253
+
254
+ if remove is not None: # TODO: This should be easy to implement during sort
255
+ for k, v in tree.ndata.items():
256
+ tree.ndata[k] = np.delete(v, remove)
257
+
258
+ _sort_tree(tree)
259
+ return tree
242
260
 
243
261
 
244
262
  def _sort_tree(tree: Tree) -> Tree:
@@ -247,15 +265,3 @@ def _sort_tree(tree: Tree) -> Tree:
247
265
  tree.ndata = {k: tree.ndata[k][id_map] for k in tree.ndata}
248
266
  tree.ndata.update(id=new_ids, pid=new_pids)
249
267
  return tree
250
-
251
-
252
- def _to_subtree(swc_like: SWCLike, sub: Topology) -> Tree:
253
- (new_id, new_pid), id_map = to_sub_topology(sub)
254
-
255
- n_nodes = new_id.shape[0]
256
- ndata = {k: swc_like.get_ndata(k)[id_map].copy() for k in swc_like.keys()}
257
- ndata.update(id=new_id, pid=new_pid)
258
-
259
- subtree = Tree(n_nodes, **ndata, names=swc_like.names)
260
- subtree.source = swc_like.source
261
- return subtree
@@ -0,0 +1,39 @@
1
+ """SWC util wrapper for tree, split to avoid circle imports.
2
+
3
+ Notes
4
+ -----
5
+ Do not import `Tree` and keep this file minimized.
6
+ """
7
+
8
+ from typing import Any, Dict, Tuple
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ from swcgeom.core.swc import SWCLike, SWCNames
14
+ from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
15
+
16
+ __all__ = ["get_subtree_impl", "to_subtree_impl"]
17
+
18
+ TreeArgs = Tuple[int, Dict[str, npt.NDArray[Any]], str, SWCNames]
19
+
20
+
21
+ def get_subtree_impl(swc_like: SWCLike, n: int) -> TreeArgs:
22
+ ids = []
23
+ topo = (swc_like.id(), swc_like.pid())
24
+ traverse(topo, enter=lambda n, _: ids.append(n), root=n)
25
+
26
+ sub_ids = np.array(ids, dtype=np.int32)
27
+ sub_pid = swc_like.pid()[sub_ids]
28
+ sub_pid[0] = -1
29
+ return to_subtree_impl(swc_like, (sub_ids, sub_pid))
30
+
31
+
32
+ def to_subtree_impl(swc_like: SWCLike, sub: Topology) -> TreeArgs:
33
+ (new_id, new_pid), id_map = to_sub_topology(sub)
34
+
35
+ n_nodes = new_id.shape[0]
36
+ ndata = {k: swc_like.get_ndata(k)[id_map].copy() for k in swc_like.keys()}
37
+ ndata.update(id=new_id, pid=new_pid)
38
+
39
+ return n_nodes, ndata, swc_like.source, swc_like.names
@@ -1,4 +1,4 @@
1
1
  """Image Stack Related."""
2
2
 
3
- from .folder import *
4
- from .io import *
3
+ from swcgeom.images.folder import *
4
+ from swcgeom.images.io import *
swcgeom/images/folder.py CHANGED
@@ -9,8 +9,8 @@ import numpy as np
9
9
  import numpy.typing as npt
10
10
  from typing_extensions import Self
11
11
 
12
- from ..transforms import Identity, Transform
13
- from .io import read_imgs
12
+ from swcgeom.images.io import read_imgs
13
+ from swcgeom.transforms import Identity, Transform
14
14
 
15
15
  __all__ = [
16
16
  "ImageStackFolder",
swcgeom/images/io.py CHANGED
@@ -6,12 +6,23 @@ import re
6
6
  import warnings
7
7
  from abc import ABC, abstractmethod
8
8
  from functools import cache, lru_cache
9
- from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple, overload
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Iterable,
13
+ List,
14
+ Literal,
15
+ Optional,
16
+ Tuple,
17
+ cast,
18
+ overload,
19
+ )
10
20
 
11
21
  import nrrd
12
22
  import numpy as np
13
23
  import numpy.typing as npt
14
24
  import tifffile
25
+ from v3dpy.loaders import PBD, Raw
15
26
 
16
27
  __all__ = ["read_imgs", "save_tiff", "read_images"]
17
28
 
@@ -20,10 +31,10 @@ RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
20
31
  RE_TERAFLY_NAME = re.compile(r"^\d+(_\d+)?(_\d+)?")
21
32
 
22
33
  UINT_MAX = {
23
- np.dtype(np.uint8): (2**8) - 1,
24
- np.dtype(np.uint16): (2**16) - 1,
25
- np.dtype(np.uint32): (2**32) - 1,
26
- np.dtype(np.uint64): (2**64) - 1,
34
+ np.dtype(np.uint8): (2**8) - 1, # type: ignore
35
+ np.dtype(np.uint16): (2**16) - 1, # type: ignore
36
+ np.dtype(np.uint32): (2**32) - 1, # type: ignore
37
+ np.dtype(np.uint64): (2**64) - 1, # type: ignore
27
38
  }
28
39
 
29
40
  AXES_ORDER = {
@@ -92,6 +103,10 @@ def read_imgs(fname: str, **kwargs) -> ImageStack:
92
103
  return TiffImageStack(fname, **kwargs)
93
104
  if ext in [".nrrd"]:
94
105
  return NrrdImageStack(fname, **kwargs)
106
+ if ext in [".v3dpbd"]:
107
+ return V3dpbdImageStack(fname, **kwargs)
108
+ if ext in [".v3draw"]:
109
+ return V3drawImageStack(fname, **kwargs)
95
110
  if ext in [".npy", ".npz"]:
96
111
  return NDArrayImageStack(np.load(fname), **kwargs)
97
112
  if TeraflyImageStack.is_root(fname):
@@ -226,7 +241,7 @@ class NDArrayImageStack(ImageStack):
226
241
 
227
242
  @property
228
243
  def shape(self) -> Tuple[int, int, int, int]:
229
- return self.imgs.shape
244
+ return cast(Tuple[int, int, int, int], self.imgs.shape)
230
245
 
231
246
 
232
247
  class TiffImageStack(NDArrayImageStack):
@@ -268,6 +283,29 @@ class NrrdImageStack(NDArrayImageStack):
268
283
  self.header = header
269
284
 
270
285
 
286
+ class V3dImageStack(NDArrayImageStack):
287
+ """v3d image stack."""
288
+
289
+ def __init__(self, fname: str, loader: Raw | PBD, **kwargs) -> None:
290
+ r = loader()
291
+ imgs = r.load(fname)
292
+ super().__init__(imgs, **kwargs)
293
+
294
+
295
+ class V3drawImageStack(V3dImageStack):
296
+ """v3draw image stack."""
297
+
298
+ def __init__(self, fname: str, **kwargs) -> None:
299
+ super().__init__(fname, loader=Raw, **kwargs)
300
+
301
+
302
+ class V3dpbdImageStack(V3dImageStack):
303
+ """v3dpbd image stack."""
304
+
305
+ def __init__(self, fname: str, **kwargs) -> None:
306
+ super().__init__(fname, loader=PBD, **kwargs)
307
+
308
+
271
309
  class TeraflyImageStack(ImageStack):
272
310
  """TeraFly image stack.
273
311
 
@@ -383,7 +421,8 @@ class TeraflyImageStack(ImageStack):
383
421
 
384
422
  @property
385
423
  def shape(self) -> Tuple[int, int, int, int]:
386
- return tuple(self.res[-1])
424
+ res_max = self.res[-1]
425
+ return res_max[0], res_max[1], res_max[2], 1
387
426
 
388
427
  @classmethod
389
428
  def get_resolutions(cls, root: str) -> Tuple[List[Vec3i], List[str], List[Vec3i]]:
@@ -476,13 +515,13 @@ class TeraflyImageStack(ImageStack):
476
515
  if shape[1] > lens[1]:
477
516
  starts_y = starts + [0, lens[1], 0]
478
517
  ends_y = np.array([starts[0], ends[1], ends[2]])
479
- ends_y += [min(shape[0], lens[0]), 0, 0]
518
+ ends_y += [min(shape[0], lens[0]), 0, 0] # type: ignore
480
519
  self._get_range(starts_y, ends_y, res_level, out[:, lens[1] :, :])
481
520
 
482
521
  if shape[2] > lens[2]:
483
522
  starts_z = starts + [0, 0, lens[2]]
484
523
  ends_z = np.array([starts[0], starts[1], ends[2]])
485
- ends_z += [min(shape[0], lens[0]), min(shape[1], lens[1]), 0]
524
+ ends_z += [min(shape[0], lens[0]), min(shape[1], lens[1]), 0] # type: ignore
486
525
  self._get_range(starts_z, ends_z, res_level, out[:, :, lens[2] :])
487
526
 
488
527
  def _find_correspond_imgs(self, p, res_level):
@@ -1,10 +1,12 @@
1
1
  """A series of transformations to compose codes."""
2
2
 
3
- from .base import *
4
- from .branch import *
5
- from .geometry import *
6
- from .image_stack import *
7
- from .images import *
8
- from .mst import *
9
- from .population import *
10
- from .tree import *
3
+ from swcgeom.transforms.base import *
4
+ from swcgeom.transforms.branch import *
5
+ from swcgeom.transforms.geometry import *
6
+ from swcgeom.transforms.image_stack import *
7
+ from swcgeom.transforms.images import *
8
+ from swcgeom.transforms.mst import *
9
+ from swcgeom.transforms.path import *
10
+ from swcgeom.transforms.population import *
11
+ from swcgeom.transforms.tree import *
12
+ from swcgeom.transforms.tree_assembler import *
@@ -7,8 +7,9 @@ import numpy as np
7
7
  import numpy.typing as npt
8
8
  from scipy import signal
9
9
 
10
- from ..core import Branch, DictSWC
11
- from ..utils import (
10
+ from swcgeom.core import Branch, DictSWC
11
+ from swcgeom.transforms.base import Transform
12
+ from swcgeom.utils import (
12
13
  angle,
13
14
  rotate3d_x,
14
15
  rotate3d_y,
@@ -17,7 +18,6 @@ from ..utils import (
17
18
  to_homogeneous,
18
19
  translate3d,
19
20
  )
20
- from .base import Transform
21
21
 
22
22
  __all__ = ["BranchLinearResampler", "BranchConvSmoother", "BranchStandardizer"]
23
23
 
@@ -6,10 +6,17 @@ from typing import Generic, Literal, Optional, TypeVar
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
8
 
9
- from ..core import DictSWC
10
- from ..core.swc_utils import SWCNames
11
- from ..utils import rotate3d, rotate3d_x, rotate3d_y, rotate3d_z, scale3d, translate3d
12
- from .base import Transform
9
+ from swcgeom.core import DictSWC
10
+ from swcgeom.core.swc_utils import SWCNames
11
+ from swcgeom.transforms.base import Transform
12
+ from swcgeom.utils import (
13
+ rotate3d,
14
+ rotate3d_x,
15
+ rotate3d_y,
16
+ rotate3d_z,
17
+ scale3d,
18
+ translate3d,
19
+ )
13
20
 
14
21
  __all__ = [
15
22
  "Normalizer",
@@ -20,9 +20,9 @@ import numpy as np
20
20
  import numpy.typing as npt
21
21
  import tifffile
22
22
 
23
- from ..core import Population, Tree
24
- from ..utils import SDF, SDFCompose, SDFRoundCone
25
- from .base import Transform
23
+ from swcgeom.core import Population, Tree
24
+ from swcgeom.transforms.base import Transform
25
+ from swcgeom.utils import SDF, SDFCompose, SDFRoundCone
26
26
 
27
27
  __all__ = ["ToImageStack"]
28
28