napari-myelin-quantifier 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.
- napari_myelin_quantifier/__init__.py +8 -0
- napari_myelin_quantifier/_axon_quant.py +206 -0
- napari_myelin_quantifier/_reader.py +85 -0
- napari_myelin_quantifier/_sample_data.py +22 -0
- napari_myelin_quantifier/_version.py +34 -0
- napari_myelin_quantifier/_widget.py +152 -0
- napari_myelin_quantifier/_writer.py +66 -0
- napari_myelin_quantifier/napari.yaml +18 -0
- napari_myelin_quantifier-0.1.0.dist-info/METADATA +136 -0
- napari_myelin_quantifier-0.1.0.dist-info/RECORD +14 -0
- napari_myelin_quantifier-0.1.0.dist-info/WHEEL +5 -0
- napari_myelin_quantifier-0.1.0.dist-info/entry_points.txt +2 -0
- napari_myelin_quantifier-0.1.0.dist-info/licenses/LICENSE +22 -0
- napari_myelin_quantifier-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from scipy import ndimage as ndi
|
|
8
|
+
from skimage import morphology
|
|
9
|
+
from skimage.color import rgb2hsv
|
|
10
|
+
from skimage.measure import label, regionprops
|
|
11
|
+
from skimage.segmentation import clear_border
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class QuantParams:
|
|
16
|
+
px_size_um: float = 0.02
|
|
17
|
+
|
|
18
|
+
# Myelin extraction / cleanup
|
|
19
|
+
v_thresh: float = 0.15
|
|
20
|
+
min_myelin_obj: int = 30
|
|
21
|
+
close_radius: int = 3
|
|
22
|
+
open_radius: int = 1
|
|
23
|
+
|
|
24
|
+
# Axon interior filtering
|
|
25
|
+
min_axon_area: int = 150
|
|
26
|
+
min_solidity: float = 0.60
|
|
27
|
+
|
|
28
|
+
# Edge handling
|
|
29
|
+
remove_border_objects: bool = True
|
|
30
|
+
edge_margin_px: int = 8
|
|
31
|
+
|
|
32
|
+
# Optional crop for screenshots with UI borders
|
|
33
|
+
auto_crop: bool = True
|
|
34
|
+
crop_frac_thresh: float = 0.15 # fraction threshold used for content crop
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _find_content_crop(
|
|
38
|
+
img_rgb: np.ndarray, v_thresh: float = 0.15, frac_thresh: float = 0.15
|
|
39
|
+
):
|
|
40
|
+
"""Auto-crop away UI/borders by finding dense colored region."""
|
|
41
|
+
hsv = rgb2hsv(img_rgb / 255.0)
|
|
42
|
+
v = hsv[..., 2]
|
|
43
|
+
mask = v > v_thresh
|
|
44
|
+
|
|
45
|
+
col_frac = mask.mean(axis=0)
|
|
46
|
+
row_frac = mask.mean(axis=1)
|
|
47
|
+
|
|
48
|
+
xs = np.where(col_frac > frac_thresh)[0]
|
|
49
|
+
ys = np.where(row_frac > frac_thresh)[0]
|
|
50
|
+
|
|
51
|
+
if len(xs) < 10 or len(ys) < 10:
|
|
52
|
+
return img_rgb, (0, 0)
|
|
53
|
+
|
|
54
|
+
x0, x1 = xs.min(), xs.max() + 1
|
|
55
|
+
y0, y1 = ys.min(), ys.max() + 1
|
|
56
|
+
return img_rgb[y0:y1, x0:x1], (y0, x0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _touches_margin(bbox, H: int, W: int, margin: int) -> bool:
|
|
60
|
+
minr, minc, maxr, maxc = bbox
|
|
61
|
+
return (
|
|
62
|
+
(minr <= margin)
|
|
63
|
+
or (minc <= margin)
|
|
64
|
+
or (maxr >= H - margin)
|
|
65
|
+
or (maxc >= W - margin)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def quantify_myelinated_axons(
|
|
70
|
+
img_rgb: np.ndarray,
|
|
71
|
+
params: QuantParams,
|
|
72
|
+
) -> tuple[np.ndarray, np.ndarray, pd.DataFrame, tuple[int, int]]:
|
|
73
|
+
"""
|
|
74
|
+
Returns:
|
|
75
|
+
labels_full: 2D int32 label image in full image coordinates (axon_id 1..N; 0=bg)
|
|
76
|
+
overlay_full: 3D uint8 RGB overlay in full image coordinates (boundary drawn white)
|
|
77
|
+
df: measurements table; axon_id matches labels/text
|
|
78
|
+
(y_off, x_off): crop offset applied
|
|
79
|
+
"""
|
|
80
|
+
if img_rgb.ndim != 3 or img_rgb.shape[2] not in (3, 4):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Expected 2D RGB/RGBA image with shape (Y, X, 3/4). Got: {img_rgb.shape}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if img_rgb.shape[2] == 4:
|
|
86
|
+
img_rgb = img_rgb[..., :3]
|
|
87
|
+
|
|
88
|
+
img_rgb = img_rgb.astype(np.uint8)
|
|
89
|
+
|
|
90
|
+
# Optional crop (useful for screenshots like your example)
|
|
91
|
+
if params.auto_crop:
|
|
92
|
+
img_c, (y_off, x_off) = _find_content_crop(
|
|
93
|
+
img_rgb,
|
|
94
|
+
v_thresh=params.v_thresh,
|
|
95
|
+
frac_thresh=params.crop_frac_thresh,
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
img_c, (y_off, x_off) = img_rgb, (0, 0)
|
|
99
|
+
|
|
100
|
+
Hc, Wc, _ = img_c.shape
|
|
101
|
+
|
|
102
|
+
# -----------------------
|
|
103
|
+
# MYELIN MASK
|
|
104
|
+
# -----------------------
|
|
105
|
+
hsv = rgb2hsv(img_c / 255.0)
|
|
106
|
+
v = hsv[..., 2]
|
|
107
|
+
myelin = v > params.v_thresh
|
|
108
|
+
|
|
109
|
+
myelin = morphology.remove_small_objects(myelin, params.min_myelin_obj)
|
|
110
|
+
myelin = morphology.binary_closing(
|
|
111
|
+
myelin, morphology.disk(params.close_radius)
|
|
112
|
+
)
|
|
113
|
+
myelin = morphology.binary_opening(
|
|
114
|
+
myelin, morphology.disk(params.open_radius)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# -----------------------
|
|
118
|
+
# FILL HOLES (NO AREA CAP)
|
|
119
|
+
# -----------------------
|
|
120
|
+
filled = ndi.binary_fill_holes(myelin)
|
|
121
|
+
axon = filled & (~myelin)
|
|
122
|
+
|
|
123
|
+
axon = morphology.remove_small_objects(axon, params.min_axon_area)
|
|
124
|
+
|
|
125
|
+
if params.remove_border_objects:
|
|
126
|
+
axon = clear_border(axon)
|
|
127
|
+
|
|
128
|
+
# -----------------------
|
|
129
|
+
# LABEL + MEASURE
|
|
130
|
+
# -----------------------
|
|
131
|
+
lab_raw = label(axon)
|
|
132
|
+
props = regionprops(lab_raw)
|
|
133
|
+
|
|
134
|
+
rows = []
|
|
135
|
+
for p in props:
|
|
136
|
+
if params.edge_margin_px > 0 and _touches_margin(
|
|
137
|
+
p.bbox, axon.shape[0], axon.shape[1], params.edge_margin_px
|
|
138
|
+
):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
area_px = float(p.area)
|
|
142
|
+
area_um2 = area_px * (params.px_size_um**2)
|
|
143
|
+
eq_diam_um = float(p.equivalent_diameter) * params.px_size_um
|
|
144
|
+
|
|
145
|
+
rows.append(
|
|
146
|
+
{
|
|
147
|
+
"raw_label": int(p.label),
|
|
148
|
+
"area_px": area_px,
|
|
149
|
+
"area_um2": area_um2,
|
|
150
|
+
"eq_diam_um": eq_diam_um,
|
|
151
|
+
"eccentricity": float(p.eccentricity),
|
|
152
|
+
"solidity": float(p.solidity),
|
|
153
|
+
"cy_crop": float(p.centroid[0]),
|
|
154
|
+
"cx_crop": float(p.centroid[1]),
|
|
155
|
+
"cy_img": float(p.centroid[0] + y_off),
|
|
156
|
+
"cx_img": float(p.centroid[1] + x_off),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
df = pd.DataFrame(rows)
|
|
161
|
+
|
|
162
|
+
# Filter by shape (your “verified axons/myelin” gate)
|
|
163
|
+
if len(df) > 0:
|
|
164
|
+
df_f = df[
|
|
165
|
+
(df["area_px"] >= params.min_axon_area)
|
|
166
|
+
& (df["solidity"] >= params.min_solidity)
|
|
167
|
+
].copy()
|
|
168
|
+
df_f = df_f.sort_values(["cy_img", "cx_img"]).reset_index(drop=True)
|
|
169
|
+
df_f.insert(0, "axon_id", np.arange(1, len(df_f) + 1))
|
|
170
|
+
else:
|
|
171
|
+
df_f = df.copy()
|
|
172
|
+
df_f.insert(0, "axon_id", [])
|
|
173
|
+
|
|
174
|
+
# Relabel into axon_id space (1..N) inside crop coords
|
|
175
|
+
labels_crop = np.zeros((Hc, Wc), dtype=np.int32)
|
|
176
|
+
if len(df_f) > 0:
|
|
177
|
+
raw_to_new = {
|
|
178
|
+
int(r.raw_label): int(r.axon_id)
|
|
179
|
+
for r in df_f.itertuples(index=False)
|
|
180
|
+
}
|
|
181
|
+
# Map raw labels -> new ids
|
|
182
|
+
# Fast remap via LUT
|
|
183
|
+
max_raw = int(lab_raw.max())
|
|
184
|
+
lut = np.zeros(max_raw + 1, dtype=np.int32)
|
|
185
|
+
for k, v in raw_to_new.items():
|
|
186
|
+
if 0 <= k <= max_raw:
|
|
187
|
+
lut[k] = v
|
|
188
|
+
labels_crop = lut[lab_raw].astype(np.int32)
|
|
189
|
+
|
|
190
|
+
# Boundary overlay (crop coords)
|
|
191
|
+
axon_crop_mask = labels_crop > 0
|
|
192
|
+
boundary = morphology.dilation(
|
|
193
|
+
axon_crop_mask, morphology.disk(1)
|
|
194
|
+
) ^ morphology.erosion(axon_crop_mask, morphology.disk(1))
|
|
195
|
+
overlay_crop = img_c.copy()
|
|
196
|
+
overlay_crop[boundary] = [255, 255, 255]
|
|
197
|
+
|
|
198
|
+
# Paste back into full-size arrays
|
|
199
|
+
H, W, _ = img_rgb.shape
|
|
200
|
+
labels_full = np.zeros((H, W), dtype=np.int32)
|
|
201
|
+
overlay_full = img_rgb.copy()
|
|
202
|
+
|
|
203
|
+
labels_full[y_off : y_off + Hc, x_off : x_off + Wc] = labels_crop
|
|
204
|
+
overlay_full[y_off : y_off + Hc, x_off : x_off + Wc] = overlay_crop
|
|
205
|
+
|
|
206
|
+
return labels_full, overlay_full, df_f, (y_off, x_off)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is an example of a barebones numpy reader plugin for napari.
|
|
3
|
+
|
|
4
|
+
It implements the Reader specification, but your plugin may choose to
|
|
5
|
+
implement multiple readers or even other plugin contributions. see:
|
|
6
|
+
https://napari.org/stable/plugins/building_a_plugin/guides.html#readers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def napari_get_reader(path):
|
|
13
|
+
"""A basic implementation of a Reader contribution.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
path : str or list of str
|
|
18
|
+
Path to file, or list of paths.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
function or None
|
|
23
|
+
If the path is a recognized format, return a function that accepts the
|
|
24
|
+
same path or list of paths, and returns a list of layer data tuples.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(path, list):
|
|
27
|
+
# reader plugins may be handed single path, or a list of paths.
|
|
28
|
+
# if it is a list, it is assumed to be an image stack...
|
|
29
|
+
# so we are only going to look at the first file.
|
|
30
|
+
path = path[0]
|
|
31
|
+
|
|
32
|
+
# the get_reader function should make as many checks as possible
|
|
33
|
+
# (without loading the full file) to determine if it can read
|
|
34
|
+
# the path. Here, we check the dtype of the array by loading
|
|
35
|
+
# it with memmap, so that we don't actually load the full array into memory.
|
|
36
|
+
# We pretend that this reader can only read integer arrays.
|
|
37
|
+
try:
|
|
38
|
+
arr = np.load(path, mmap_mode="r")
|
|
39
|
+
if arr.dtype != np.int_:
|
|
40
|
+
return None
|
|
41
|
+
# napari_get_reader should never raise an exception, because napari
|
|
42
|
+
# raises its own specific errors depending on what plugins are
|
|
43
|
+
# available for the given path, so we catch
|
|
44
|
+
# the OSError that np.load might raise if the file is malformed
|
|
45
|
+
except OSError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
# otherwise we return the *function* that can read ``path``.
|
|
49
|
+
return reader_function
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reader_function(path):
|
|
53
|
+
"""Take a path or list of paths and return a list of LayerData tuples.
|
|
54
|
+
|
|
55
|
+
Readers are expected to return data as a list of tuples, where each tuple
|
|
56
|
+
is (data, [add_kwargs, [layer_type]]), "add_kwargs" and "layer_type" are
|
|
57
|
+
both optional.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
path : str or list of str
|
|
62
|
+
Path to file, or list of paths.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
layer_data : list of tuples
|
|
67
|
+
A list of LayerData tuples where each tuple in the list contains
|
|
68
|
+
(data, metadata, layer_type), where data is a numpy array, metadata is
|
|
69
|
+
a dict of keyword arguments for the corresponding viewer.add_* method
|
|
70
|
+
in napari, and layer_type is a lower-case string naming the type of
|
|
71
|
+
layer. Both "meta", and "layer_type" are optional. napari will
|
|
72
|
+
default to layer_type=="image" if not provided
|
|
73
|
+
"""
|
|
74
|
+
# handle both a string and a list of strings
|
|
75
|
+
paths = [path] if isinstance(path, str) else path
|
|
76
|
+
# load all files into array
|
|
77
|
+
arrays = [np.load(_path) for _path in paths]
|
|
78
|
+
# stack arrays into single array
|
|
79
|
+
data = np.squeeze(np.stack(arrays))
|
|
80
|
+
|
|
81
|
+
# optional kwargs for the corresponding viewer.add_* method
|
|
82
|
+
add_kwargs = {}
|
|
83
|
+
|
|
84
|
+
layer_type = "image" # optional, default is "image"
|
|
85
|
+
return [(data, add_kwargs, layer_type)]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is an example of a barebones sample data provider for napari.
|
|
3
|
+
|
|
4
|
+
It implements the "sample data" specification.
|
|
5
|
+
see: https://napari.org/stable/plugins/building_a_plugin/guides.html#sample-data
|
|
6
|
+
|
|
7
|
+
Replace code below according to your needs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import numpy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def make_sample_data():
|
|
16
|
+
"""Generates an image"""
|
|
17
|
+
# Return list of tuples
|
|
18
|
+
# [(data1, add_image_kwargs1), (data2, add_image_kwargs2)]
|
|
19
|
+
# Check the documentation for more information about the
|
|
20
|
+
# add_image_kwargs
|
|
21
|
+
# https://napari.org/stable/api/napari.Viewer.html#napari.Viewer.add_image
|
|
22
|
+
return [(numpy.random.rand(512, 512), {})]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import napari
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from magicgui import magic_factory
|
|
5
|
+
from napari.layers import Image # <-- real class (magicgui can resolve)
|
|
6
|
+
from qtpy.QtWidgets import QFileDialog
|
|
7
|
+
|
|
8
|
+
from ._axon_quant import QuantParams, quantify_myelinated_axons
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _as_uint8_rgb(data: np.ndarray) -> np.ndarray:
|
|
12
|
+
"""Convert napari image data into uint8 RGB (Y,X,3)."""
|
|
13
|
+
arr = np.asarray(data)
|
|
14
|
+
|
|
15
|
+
# Expect (Y, X, 3) or (Y, X, 4)
|
|
16
|
+
if arr.ndim == 3 and arr.shape[-1] in (3, 4):
|
|
17
|
+
if arr.dtype != np.uint8:
|
|
18
|
+
a = arr.astype(np.float32)
|
|
19
|
+
a = a - np.nanmin(a)
|
|
20
|
+
denom = (np.nanmax(a) - np.nanmin(a)) + 1e-9
|
|
21
|
+
a = a / denom
|
|
22
|
+
a = (a * 255.0).clip(0, 255).astype(np.uint8)
|
|
23
|
+
arr = a
|
|
24
|
+
return arr[..., :3]
|
|
25
|
+
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Expected RGB/RGBA image data. Got shape={arr.shape}, dtype={arr.dtype}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@magic_factory(
|
|
32
|
+
call_button="Run quantification",
|
|
33
|
+
# Top 3 critical knobs
|
|
34
|
+
v_thresh={
|
|
35
|
+
"label": "V-threshold (myelin brightness)",
|
|
36
|
+
"min": 0.0,
|
|
37
|
+
"max": 1.0,
|
|
38
|
+
"step": 0.01,
|
|
39
|
+
},
|
|
40
|
+
min_axon_area={
|
|
41
|
+
"label": "Min axon area (px)",
|
|
42
|
+
"min": 0,
|
|
43
|
+
"max": 20000,
|
|
44
|
+
"step": 10,
|
|
45
|
+
},
|
|
46
|
+
min_solidity={
|
|
47
|
+
"label": "Min solidity",
|
|
48
|
+
"min": 0.0,
|
|
49
|
+
"max": 1.0,
|
|
50
|
+
"step": 0.01,
|
|
51
|
+
},
|
|
52
|
+
# Secondary knobs
|
|
53
|
+
close_radius={"label": "Closing radius", "min": 0, "max": 20, "step": 1},
|
|
54
|
+
open_radius={"label": "Opening radius", "min": 0, "max": 20, "step": 1},
|
|
55
|
+
edge_margin_px={
|
|
56
|
+
"label": "Edge margin exclusion (px)",
|
|
57
|
+
"min": 0,
|
|
58
|
+
"max": 200,
|
|
59
|
+
"step": 1,
|
|
60
|
+
},
|
|
61
|
+
px_size_um={
|
|
62
|
+
"label": "Pixel size (µm/px)",
|
|
63
|
+
"min": 0.0,
|
|
64
|
+
"max": 10.0,
|
|
65
|
+
"step": 0.001,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
def myelin_quantifier_widget(
|
|
69
|
+
viewer: napari.Viewer,
|
|
70
|
+
image_layer: Image, # <-- key change
|
|
71
|
+
v_thresh: float = 0.15,
|
|
72
|
+
min_axon_area: int = 150,
|
|
73
|
+
min_solidity: float = 0.60,
|
|
74
|
+
close_radius: int = 3,
|
|
75
|
+
open_radius: int = 1,
|
|
76
|
+
edge_margin_px: int = 8,
|
|
77
|
+
px_size_um: float = 0.02,
|
|
78
|
+
remove_border_objects: bool = True,
|
|
79
|
+
auto_crop_screenshot: bool = True,
|
|
80
|
+
export_csv: bool = True,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Runs myelinated axon quantification on an RGB image layer.
|
|
84
|
+
|
|
85
|
+
Outputs:
|
|
86
|
+
- Labels layer: axon interiors labeled 1..N (axon_id)
|
|
87
|
+
- Image layer: boundary overlay for verification
|
|
88
|
+
- Points layer: ID text anchored at centroids
|
|
89
|
+
- Optional CSV export with axon_id + measurements
|
|
90
|
+
"""
|
|
91
|
+
if image_layer is None:
|
|
92
|
+
raise ValueError("Select an RGB image layer.")
|
|
93
|
+
|
|
94
|
+
img_rgb = _as_uint8_rgb(image_layer.data)
|
|
95
|
+
|
|
96
|
+
params = QuantParams(
|
|
97
|
+
px_size_um=px_size_um,
|
|
98
|
+
v_thresh=v_thresh,
|
|
99
|
+
min_axon_area=min_axon_area,
|
|
100
|
+
min_solidity=min_solidity,
|
|
101
|
+
close_radius=close_radius,
|
|
102
|
+
open_radius=open_radius,
|
|
103
|
+
remove_border_objects=remove_border_objects,
|
|
104
|
+
edge_margin_px=edge_margin_px,
|
|
105
|
+
auto_crop=auto_crop_screenshot,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
labels_full, overlay_full, df, _ = quantify_myelinated_axons(
|
|
109
|
+
img_rgb, params
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
base = image_layer.name
|
|
113
|
+
|
|
114
|
+
# 1) Labels layer
|
|
115
|
+
viewer.add_labels(labels_full, name=f"{base} | axon_ids")
|
|
116
|
+
|
|
117
|
+
# 2) Overlay image layer
|
|
118
|
+
viewer.add_image(overlay_full, name=f"{base} | axon_overlay", opacity=0.8)
|
|
119
|
+
|
|
120
|
+
# 3) Text IDs as Points layer
|
|
121
|
+
if len(df) > 0:
|
|
122
|
+
points = df[["cy_img", "cx_img"]].to_numpy(dtype=float) # (y,x)
|
|
123
|
+
features = pd.DataFrame(
|
|
124
|
+
{"axon_id": df["axon_id"].astype(str).to_numpy()}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
pts = viewer.add_points(
|
|
128
|
+
points,
|
|
129
|
+
name=f"{base} | axon_id_text",
|
|
130
|
+
size=0, # text only
|
|
131
|
+
features=features,
|
|
132
|
+
)
|
|
133
|
+
pts.text = {
|
|
134
|
+
"string": "{axon_id}",
|
|
135
|
+
"size": 10,
|
|
136
|
+
"color": "yellow",
|
|
137
|
+
"anchor": "center",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# 4) Export CSV
|
|
141
|
+
if export_csv:
|
|
142
|
+
default_name = f"{base}_axon_measurements.csv"
|
|
143
|
+
save_path, _ = QFileDialog.getSaveFileName(
|
|
144
|
+
None,
|
|
145
|
+
"Save axon measurements CSV",
|
|
146
|
+
default_name,
|
|
147
|
+
"CSV files (*.csv)",
|
|
148
|
+
)
|
|
149
|
+
if save_path:
|
|
150
|
+
df.to_csv(save_path, index=False)
|
|
151
|
+
|
|
152
|
+
viewer.status = f"Myelin Quantifier: detected {len(df)} axons (filtered)."
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is an example of a barebones writer plugin for napari.
|
|
3
|
+
|
|
4
|
+
It implements the Writer specification.
|
|
5
|
+
see: https://napari.org/stable/plugins/building_a_plugin/guides.html#writers
|
|
6
|
+
|
|
7
|
+
Replace code below according to your needs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
DataType = Union[Any, Sequence[Any]]
|
|
17
|
+
FullLayerData = tuple[DataType, dict, str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def write_single_image(path: str, data: Any, meta: dict) -> list[str]:
|
|
21
|
+
"""Writes a single image layer.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
path : str
|
|
26
|
+
A string path indicating where to save the image file.
|
|
27
|
+
data : The layer data
|
|
28
|
+
The `.data` attribute from the napari layer.
|
|
29
|
+
meta : dict
|
|
30
|
+
A dictionary containing all other attributes from the napari layer
|
|
31
|
+
(excluding the `.data` layer attribute).
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
[path] : A list containing the string path to the saved file.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# implement your writer logic here ...
|
|
39
|
+
|
|
40
|
+
# return path to any file(s) that were successfully written
|
|
41
|
+
return [path]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_multiple(path: str, data: list[FullLayerData]) -> list[str]:
|
|
45
|
+
"""Writes multiple layers of different types.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
path : str
|
|
50
|
+
A string path indicating where to save the data file(s).
|
|
51
|
+
data : A list of layer tuples.
|
|
52
|
+
Tuples contain three elements: (data, meta, layer_type)
|
|
53
|
+
`data` is the layer data
|
|
54
|
+
`meta` is a dictionary containing all other metadata attributes
|
|
55
|
+
from the napari layer (excluding the `.data` layer attribute).
|
|
56
|
+
`layer_type` is a string, eg: "image", "labels", "surface", etc.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
[path] : A list containing (potentially multiple) string paths to the saved file(s).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# implement your writer logic here ...
|
|
64
|
+
|
|
65
|
+
# return path to any file(s) that were successfully written
|
|
66
|
+
return [path]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: napari-myelin-quantifier
|
|
2
|
+
display_name: Myelin Quantifier
|
|
3
|
+
visibility: public
|
|
4
|
+
|
|
5
|
+
contributions:
|
|
6
|
+
commands:
|
|
7
|
+
- id: napari-myelin-quantifier.myelin_quantifier_widget
|
|
8
|
+
python_name: napari_myelin_quantifier._widget:myelin_quantifier_widget
|
|
9
|
+
title: Myelin Quantifier (Axon Counter)
|
|
10
|
+
|
|
11
|
+
widgets:
|
|
12
|
+
- command: napari-myelin-quantifier.myelin_quantifier_widget
|
|
13
|
+
display_name: Myelin Quantifier
|
|
14
|
+
|
|
15
|
+
menus:
|
|
16
|
+
napari/plugins:
|
|
17
|
+
- command: napari-myelin-quantifier.myelin_quantifier_widget
|
|
18
|
+
when: always
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: napari-myelin-quantifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quantify myelinated axons with label tracking
|
|
5
|
+
Author: Napari User
|
|
6
|
+
Author-email: wulinteo.usa2@gmail.com
|
|
7
|
+
License:
|
|
8
|
+
The MIT License (MIT)
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Napari User
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Bug Tracker, https://github.com/wulinteousa2-hash/napari-myelin-quantifier/issues
|
|
31
|
+
Project-URL: Documentation, https://github.com/wulinteousa2-hash/napari-myelin-quantifier#README.md
|
|
32
|
+
Project-URL: Source Code, https://github.com/wulinteousa2-hash/napari-myelin-quantifier
|
|
33
|
+
Project-URL: User Support, https://github.com/wulinteousa2-hash/napari-myelin-quantifier/issues
|
|
34
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
35
|
+
Classifier: Framework :: napari
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python
|
|
40
|
+
Classifier: Programming Language :: Python :: 3
|
|
41
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
46
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
47
|
+
Requires-Python: >=3.10
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
License-File: LICENSE
|
|
50
|
+
Requires-Dist: numpy
|
|
51
|
+
Requires-Dist: magicgui
|
|
52
|
+
Requires-Dist: qtpy
|
|
53
|
+
Requires-Dist: scikit-image
|
|
54
|
+
Provides-Extra: all
|
|
55
|
+
Requires-Dist: napari[all]; extra == "all"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# napari-myelin-quantifier
|
|
59
|
+
|
|
60
|
+
[](https://github.com/wulinteousa2-hash/napari-myelin-quantifier/raw/main/LICENSE)
|
|
61
|
+
[](https://pypi.org/project/napari-myelin-quantifier)
|
|
62
|
+
[](https://python.org)
|
|
63
|
+
[](https://github.com/wulinteousa2-hash/napari-myelin-quantifier/actions)
|
|
64
|
+
[](https://codecov.io/gh/wulinteousa2-hash/napari-myelin-quantifier)
|
|
65
|
+
[](https://napari-hub.org/plugins/napari-myelin-quantifier)
|
|
66
|
+
[](https://napari.org/stable/plugins/index.html)
|
|
67
|
+
[](https://github.com/copier-org/copier)
|
|
68
|
+
|
|
69
|
+
Quantify myelinated axons with label tracking
|
|
70
|
+
|
|
71
|
+
----------------------------------
|
|
72
|
+
|
|
73
|
+
This [napari] plugin was generated with [copier] using the [napari-plugin-template] (None).
|
|
74
|
+
|
|
75
|
+
<!--
|
|
76
|
+
Don't miss the full getting started guide to set up your new package:
|
|
77
|
+
https://github.com/napari/napari-plugin-template#getting-started
|
|
78
|
+
|
|
79
|
+
and review the napari docs for plugin developers:
|
|
80
|
+
https://napari.org/stable/plugins/index.html
|
|
81
|
+
-->
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
You can install `napari-myelin-quantifier` via [pip]:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
pip install napari-myelin-quantifier
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If napari is not already installed, you can install `napari-myelin-quantifier` with napari and Qt via:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
pip install "napari-myelin-quantifier[all]"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
To install latest development version :
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
pip install git+https://github.com/wulinteousa2-hash/napari-myelin-quantifier.git
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
Contributions are very welcome. Tests can be run with [tox], please ensure
|
|
109
|
+
the coverage at least stays the same before you submit a pull request.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
Distributed under the terms of the [MIT] license,
|
|
114
|
+
"napari-myelin-quantifier" is free and open source software
|
|
115
|
+
|
|
116
|
+
## Issues
|
|
117
|
+
|
|
118
|
+
If you encounter any problems, please [file an issue] along with a detailed description.
|
|
119
|
+
|
|
120
|
+
[napari]: https://github.com/napari/napari
|
|
121
|
+
[copier]: https://copier.readthedocs.io/en/stable/
|
|
122
|
+
[@napari]: https://github.com/napari
|
|
123
|
+
[MIT]: http://opensource.org/licenses/MIT
|
|
124
|
+
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
|
|
125
|
+
[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt
|
|
126
|
+
[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt
|
|
127
|
+
[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0
|
|
128
|
+
[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt
|
|
129
|
+
[napari-plugin-template]: https://github.com/napari/napari-plugin-template
|
|
130
|
+
|
|
131
|
+
[file an issue]: https://github.com/wulinteousa2-hash/napari-myelin-quantifier/issues
|
|
132
|
+
|
|
133
|
+
[napari]: https://github.com/napari/napari
|
|
134
|
+
[tox]: https://tox.readthedocs.io/en/latest/
|
|
135
|
+
[pip]: https://pypi.org/project/pip/
|
|
136
|
+
[PyPI]: https://pypi.org/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
napari_myelin_quantifier/__init__.py,sha256=PQoEuuclJDku-ttiJPycAIgmCybSXFHRAGiKoNDj7nw,189
|
|
2
|
+
napari_myelin_quantifier/_axon_quant.py,sha256=kL9bpR57MdpwNsQ9nMFaJVP4fKuvOb4xP-221BcTOlM,6430
|
|
3
|
+
napari_myelin_quantifier/_reader.py,sha256=bAlIoUWx8N0IYF1InuqDUO1kL5N7L9r9jtMLm3eFlcY,3127
|
|
4
|
+
napari_myelin_quantifier/_sample_data.py,sha256=uHTGOqeoHhn1g-P11jEIFi7DOFtud9cB9aDnIVDvEig,662
|
|
5
|
+
napari_myelin_quantifier/_version.py,sha256=bauHj-EPGxrNjRoP5OYKhJo32mQBOMvEa2cW00W0jHk,738
|
|
6
|
+
napari_myelin_quantifier/_widget.py,sha256=h84e_rmy8HmuAi3RbXIdYonGykcEl_yCN3TMdWoR5KY,4485
|
|
7
|
+
napari_myelin_quantifier/_writer.py,sha256=R8DYqFtpA2TinwioQYcspMDDwqrZqkssATt6SwjDrVs,1958
|
|
8
|
+
napari_myelin_quantifier/napari.yaml,sha256=B270m-3v0ffZinsgx08jy_Nu1IIS8CVPoCHmQMKUP6c,526
|
|
9
|
+
napari_myelin_quantifier-0.1.0.dist-info/licenses/LICENSE,sha256=d1ACW9b17QHlefOPYOe_0-5E8vCJe80eRDPS-U5wkA8,1079
|
|
10
|
+
napari_myelin_quantifier-0.1.0.dist-info/METADATA,sha256=iiOrHYmpaUwAEcHt6WoNSFFbdb7YV9u1iBa0C-AnSeQ,6203
|
|
11
|
+
napari_myelin_quantifier-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
napari_myelin_quantifier-0.1.0.dist-info/entry_points.txt,sha256=QcAh7UGBW1p1HqQ6fSnZKzAjld9srLFZQZBLdhf7Ga8,82
|
|
13
|
+
napari_myelin_quantifier-0.1.0.dist-info/top_level.txt,sha256=DfQ-A26y__Y-MqVsnToCv-BUMrAmc5y26dggT7SMruE,25
|
|
14
|
+
napari_myelin_quantifier-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Napari User
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
napari_myelin_quantifier
|