Skip to content

Commit d1bd2d9

Browse files
authored
Merge pull request #2 from juglab/v0.2
Add python backend to `napari` files
2 parents d6e9849 + 41543a4 commit d1bd2d9

6 files changed

Lines changed: 2023 additions & 0 deletions

File tree

napari/detectNuclei/blob.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python3
2+
import numpy as np
3+
from numpy import zeros, ones, asarray, empty, nonzero, transpose, triu, seterr, arccos, sqrt
4+
from numpy.linalg import norm
5+
from math import pi
6+
from scipy.ndimage.filters import gaussian_laplace, minimum_filter
7+
from operator import contains
8+
from functools import partial
9+
from itertools import filterfalse
10+
from tqdm.contrib import tzip
11+
import tifffile
12+
from skimage import filters
13+
14+
def localMinima(data):
15+
peaks1 = data == minimum_filter(data, size=(3,)*data.ndim) # TODO peaks Scales x Z x Y x X boolean type
16+
return peaks1
17+
18+
def getPeaksSubset(log, peaks1, scales, threshold = None):
19+
if threshold is None:
20+
threshold = filters.threshold_otsu(log[peaks1])
21+
print("Value of Otsu Threshold is = {} *****".format(threshold))
22+
peaks = log < threshold
23+
peaks &= peaks1 # boolean array scales, Z, Y, X
24+
peaksList = transpose(nonzero(peaks))
25+
peaksList[:, 0] = scales[peaksList[:, 0]] # N x 4 table
26+
return peaks, peaksList, threshold
27+
28+
def blobLOG(data, progress_bar, scales=range(5, 9, 1), anisotropyFactor = 5.0):
29+
"""Find blobs. Returns [[scale, x, y, ...], ...]"""
30+
31+
data = asarray(data)
32+
scales = asarray(scales)
33+
34+
log = empty((len(scales),) + data.shape, dtype=data.dtype)
35+
count = 1
36+
for slog, scale in (tzip(log, scales)):
37+
slog[...] = scale ** 2 * gaussian_laplace(data, asarray([scale/anisotropyFactor, scale, scale]))
38+
progress_bar.setValue(100 * count // len(scales))
39+
count+=1
40+
peaks1 = localMinima(log)
41+
42+
peaks, peaksList, threshold = getPeaksSubset(log, peaks1, scales)
43+
return peaks, peaksList, log, peaks1, threshold
44+
45+
def sphereIntersection(r1, r2, d):
46+
# https://en.wikipedia.org/wiki/Spherical_cap#Application
47+
48+
valid = (d < (r1 + r2)) & (d > 0)
49+
return valid * (pi * (r1 + r2 - d) ** 2
50+
* (d ** 2 + 2 * d * (r1 + r2) - 3 * (r1 - r2) ** 2)
51+
/ (12 * d))
52+
53+
def circleIntersection(r1, r2, d):
54+
# http://mathworld.wolfram.com/Circle-CircleIntersection.html
55+
return (r1 ** 2 * arccos((d ** 2 + r1 ** 2 - r2 ** 2) / (2 * d * r1))
56+
+ r2 ** 2 * arccos((d ** 2 + r2 ** 2 - r1 ** 2) / (2 * d * r2))
57+
- sqrt((-d + r1 + r2) * (d + r1 - r2)
58+
* (d - r1 + r2) * (d + r1 + r2)) / 2)
59+
60+
def suppressIntersectingNuclei(peaks, peaksList, log, anisotropyFactor):
61+
peaksComplete = np.hstack((peaksList, log[peaks][:, np.newaxis]))
62+
peaksSorted = peaksComplete[peaksComplete[:, -1].argsort()]
63+
peaksSubset = []
64+
while(len(peaksSorted) > 0):
65+
booleanIOUTable = getIntersectionTruths(peaksSorted, anisotropyFactor)
66+
indices, = np.where(booleanIOUTable[0, :] == 1) # returns the x and y indices
67+
indices = indices[indices!=0] # ignore if it is pointing to itself!
68+
peaksSubset.append(peaksSorted[0, :4])
69+
peaksSorted = np.delete(peaksSorted, indices, 0)
70+
peaksSorted = np.delete(peaksSorted, 0, 0)
71+
return peaksSubset
72+
73+
def getIntersectionTruths(peaksSorted, anisotropyFactor):
74+
booleanIOUTable = np.zeros((1, peaksSorted.shape[0]))
75+
for j, row in enumerate(peaksSorted):
76+
d = np.linalg.norm([anisotropyFactor*(peaksSorted[0, 1] - peaksSorted[j, 1]), peaksSorted[0, 2] - peaksSorted[j, 2], peaksSorted[0, 3] - peaksSorted[j, 3]])
77+
radius_i = np.sqrt(3) * peaksSorted[0, 0]
78+
radius_j = np.sqrt(3) * peaksSorted[j, 0]
79+
volume_i = 4 / 3 * pi * radius_i ** 3
80+
volume_j = 4 / 3 * pi * radius_j ** 3
81+
if d != 0:
82+
intersection = sphereIntersection(radius_i, radius_j, d)
83+
else:
84+
intersection = np.minimum(volume_i, volume_j)
85+
booleanIOUTable[0, j] = intersection > 0.05 * np.minimum(volume_i, volume_j)
86+
return booleanIOUTable
87+
88+
def findNuclei(img, progress_bar, scales=range(1, 10), anisotropyFactor = 5.0, max_overlap=0.05):
89+
peaks, peaksList, log, peaks1, threshold = blobLOG(img, progress_bar, scales=scales, anisotropyFactor = anisotropyFactor) # Important to flip the sign!
90+
# peaks SZYX
91+
# peaks1 SZYX
92+
# peaksList N x 4
93+
#np.savetxt('/home/manan/Desktop/seeds_1', peaksList, delimiter=',')
94+
print("Minima saved!!")
95+
print("Peaks shape is {}, Peaks list shape is {}, peaks1 shape is {}".format(peaks.shape, peaksList.shape, peaks1.shape))
96+
print("Log peaks shape = {}".format(log[peaks].shape))
97+
peaksSubset = suppressIntersectingNuclei(peaks, peaksList, log, anisotropyFactor)
98+
return peaks, np.asarray(peaksSubset), log, peaks1, threshold
99+
100+
def peakEnclosed(peaks, shape, size=1):
101+
102+
shape = asarray(shape)
103+
return ((size <= peaks).all(axis=-1) & (size < (shape - peaks)).all(axis=-1))
104+
105+
def plot(args):
106+
from tifffile import imread
107+
from numpy import loadtxt, delete
108+
from pickle import load
109+
import matplotlib
110+
from mpl_toolkits.axes_grid.anchored_artists import AnchoredAuxTransformBox
111+
from matplotlib.text import Text
112+
from matplotlib.text import Line2D
113+
114+
if args.outfile is not None:
115+
matplotlib.use('Agg')
116+
import matplotlib.pyplot as plt
117+
118+
image = imread(str(args.image)).T
119+
scale = asarray(args.scale) if args.scale else ones(image.ndim, dtype='int')
120+
121+
if args.peaks.suffix == '.txt':
122+
peaks = loadtxt(str(args.peaks), ndmin=2)
123+
elif args.peaks.suffix == ".csv":
124+
peaks = loadtxt(str(args.peaks), ndmin=2, delimiter=',')
125+
elif args.peaks.suffix == ".pickle":
126+
with args.peaks.open("rb") as f:
127+
peaks = load(f)
128+
else:
129+
raise ValueError("Unrecognized file type: '{}', need '.pickle' or '.csv'"
130+
.format(args.peaks.suffix))
131+
peaks = peaks / scale
132+
133+
proj_axes = tuple(filterfalse(partial(contains, args.axes), range(image.ndim)))
134+
image = image.max(proj_axes)
135+
peaks = delete(peaks, proj_axes, axis=1)
136+
137+
fig, ax = plt.subplots(1, 1, figsize=args.size)
138+
ax.imshow(image.T, cmap='gray')
139+
ax.set_xticks([])
140+
ax.set_yticks([])
141+
ax.scatter(*peaks.T, edgecolor="C1", facecolor='none')
142+
143+
if args.scalebar is not None:
144+
pixel, units, length = args.scalebar
145+
pixel = float(pixel)
146+
length = int(length)
147+
148+
box = AnchoredAuxTransformBox(ax.transData, loc=4)
149+
box.patch.set_alpha(0.8)
150+
bar = Line2D([-length/pixel/2, length/pixel/2], [0.0, 0.0], color='black')
151+
box.drawing_area.add_artist(bar)
152+
label = Text(
153+
0.0, 0.0, "{} {}".format(length, units),
154+
horizontalalignment="center", verticalalignment="bottom"
155+
)
156+
box.drawing_area.add_artist(label)
157+
ax.add_artist(box)
158+
159+
if args.outfile is None:
160+
plt.show()
161+
else:
162+
fig.tight_layout()
163+
fig.savefig(str(args.outfile))
164+
165+
def find(args):
166+
from sys import stdout
167+
from tifffile import imread
168+
169+
image = imread(str(args.image)).astype('float32')
170+
171+
scale = asarray(args.scale) if args.scale else ones(image.ndim, dtype='int')
172+
blobs = findNuclei(image, range(*args.size), args.threshold)[:, 1:] # Remove scale
173+
blobs = blobs[peakEnclosed(blobs, shape=image.shape, size=args.edge)]
174+
blobs = blobs[:, ::-1] # Reverse to xyz order
175+
blobs = blobs * scale
176+
177+
if args.format == "pickle":
178+
from pickle import dump, HIGHEST_PROTOCOL
179+
from functools import partial
180+
dump = partial(dump, protocol=HIGHEST_PROTOCOL)
181+
182+
dump(blobs, stdout.buffer)
183+
else:
184+
import csv
185+
186+
if args.format == 'txt':
187+
delimiter = ' '
188+
elif args.format == 'csv':
189+
delimiter = ','
190+
writer = csv.writer(stdout, delimiter=delimiter)
191+
for blob in blobs:
192+
writer.writerow(blob)
193+
194+
# For setuptools entry_points
195+
def main(args=None):
196+
from argparse import ArgumentParser
197+
from pathlib import Path
198+
from sys import argv
199+
200+
parser = ArgumentParser(description="Find peaks in an nD image")
201+
subparsers = parser.add_subparsers()
202+
203+
find_parser = subparsers.add_parser("find")
204+
find_parser.add_argument("image", type=Path, help="The image to process")
205+
find_parser.add_argument("--size", type=int, nargs=2, default=(1, 1),
206+
help="The range of sizes (in px) to search.")
207+
find_parser.add_argument("--threshold", type=float, default=5,
208+
help="The minimum spot intensity")
209+
find_parser.add_argument("--format", choices={"csv", "txt", "pickle"}, default="csv",
210+
help="The output format (for stdout)")
211+
find_parser.add_argument("--edge", type=int, default=0,
212+
help="Minimum distance to edge allowed.")
213+
find_parser.set_defaults(func=find)
214+
215+
plot_parser = subparsers.add_parser("plot")
216+
plot_parser.add_argument("image", type=Path, help="The image to process")
217+
plot_parser.add_argument("peaks", type=Path, help="The peaks to plot")
218+
plot_parser.add_argument("outfile", nargs='?', type=Path, default=None,
219+
help="Where to save the plot (omit to display)")
220+
plot_parser.add_argument("--axes", type=int, nargs=2, default=(0, 1),
221+
help="The axes to plot")
222+
plot_parser.add_argument("--size", type=float, nargs=2, default=(5, 5),
223+
help="The size of the figure (in inches)")
224+
plot_parser.add_argument("--scalebar", type=str, nargs=3, default=None,
225+
help="The pixel-size, units and scalebar size")
226+
plot_parser.set_defaults(func=plot)
227+
228+
for p in (plot_parser, find_parser):
229+
p.add_argument("--scale", nargs="*", type=float,
230+
help="The scale for the points along each axis.")
231+
232+
args = parser.parse_args(argv[1:] if args is None else args)
233+
args.func(args)
234+
235+
if __name__ == "__main__":
236+
main()

0 commit comments

Comments
 (0)