cellfinder 1.3.3__py3-none-any.whl → 1.4.0a0__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.
- cellfinder/core/classify/classify.py +1 -1
- cellfinder/core/detect/detect.py +118 -183
- cellfinder/core/detect/filters/plane/classical_filter.py +339 -37
- cellfinder/core/detect/filters/plane/plane_filter.py +137 -55
- cellfinder/core/detect/filters/plane/tile_walker.py +126 -60
- cellfinder/core/detect/filters/setup_filters.py +422 -65
- cellfinder/core/detect/filters/volume/ball_filter.py +313 -315
- cellfinder/core/detect/filters/volume/structure_detection.py +73 -35
- cellfinder/core/detect/filters/volume/structure_splitting.py +160 -96
- cellfinder/core/detect/filters/volume/volume_filter.py +444 -123
- cellfinder/core/main.py +6 -2
- cellfinder/core/tools/IO.py +45 -0
- cellfinder/core/tools/threading.py +380 -0
- cellfinder/core/tools/tools.py +128 -6
- {cellfinder-1.3.3.dist-info → cellfinder-1.4.0a0.dist-info}/METADATA +3 -2
- {cellfinder-1.3.3.dist-info → cellfinder-1.4.0a0.dist-info}/RECORD +20 -18
- {cellfinder-1.3.3.dist-info → cellfinder-1.4.0a0.dist-info}/WHEEL +1 -1
- {cellfinder-1.3.3.dist-info → cellfinder-1.4.0a0.dist-info}/LICENSE +0 -0
- {cellfinder-1.3.3.dist-info → cellfinder-1.4.0a0.dist-info}/entry_points.txt +0 -0
- {cellfinder-1.3.3.dist-info → cellfinder-1.4.0a0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pooch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fetch_pooch_directory(
|
|
5
|
+
registry: pooch.Pooch,
|
|
6
|
+
directory_name: str,
|
|
7
|
+
processor=None,
|
|
8
|
+
downloader=None,
|
|
9
|
+
progressbar=False,
|
|
10
|
+
):
|
|
11
|
+
"""
|
|
12
|
+
Fetches files from the Pooch registry that belong to a specific directory.
|
|
13
|
+
Parameters:
|
|
14
|
+
registry (pooch.Pooch): The Pooch registry object.
|
|
15
|
+
directory_name (str):
|
|
16
|
+
The remote relative path of the directory to fetch files from.
|
|
17
|
+
processor (callable, optional):
|
|
18
|
+
A function to process the fetched files. Defaults to None.
|
|
19
|
+
downloader (callable, optional):
|
|
20
|
+
A function to download the files. Defaults to None.
|
|
21
|
+
progressbar (bool, optional):
|
|
22
|
+
Whether to display a progress bar during the fetch.
|
|
23
|
+
Defaults to False.
|
|
24
|
+
Returns:
|
|
25
|
+
str: The local absolute path to the fetched directory.
|
|
26
|
+
"""
|
|
27
|
+
names = []
|
|
28
|
+
for name in registry.registry_files:
|
|
29
|
+
if name.startswith(f"{directory_name}/"):
|
|
30
|
+
names.append(name)
|
|
31
|
+
|
|
32
|
+
if not names:
|
|
33
|
+
raise FileExistsError(
|
|
34
|
+
f"Unable to find files in directory {directory_name}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
for name in names:
|
|
38
|
+
registry.fetch(
|
|
39
|
+
name,
|
|
40
|
+
processor=processor,
|
|
41
|
+
downloader=downloader,
|
|
42
|
+
progressbar=progressbar,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return str(registry.abspath / directory_name)
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides classes that can run a function in another thread or process and
|
|
3
|
+
allow passing data to and from the threads/processes. It also passes on any
|
|
4
|
+
exceptions that occur in the secondary thread/sub-process in the main thread
|
|
5
|
+
or when it exits.
|
|
6
|
+
|
|
7
|
+
If using a sub-process and pytorch Tensors are sent from/to the main
|
|
8
|
+
process, pytorch will memory map the tensor so the same data is shared and
|
|
9
|
+
edits in the main process will be visible in the sub-process. However, there
|
|
10
|
+
are limitations (such so not re-sharing a tensor shared with us). See
|
|
11
|
+
https://pytorch.org/docs/stable/multiprocessing.html#sharing-cuda-tensors for
|
|
12
|
+
details.
|
|
13
|
+
|
|
14
|
+
Typical example::
|
|
15
|
+
|
|
16
|
+
from cellfinder.core.tools.threading import ThreadWithException, \\
|
|
17
|
+
EOFSignal, ProcessWithException
|
|
18
|
+
import torch
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def worker(thread: ThreadWithException, power: float):
|
|
22
|
+
while True:
|
|
23
|
+
# if the main thread wants us to exit, it'll wake us up
|
|
24
|
+
msg = thread.get_msg_from_mainthread()
|
|
25
|
+
# we were asked to exit
|
|
26
|
+
if msg == EOFSignal:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
tensor_id, tensor, add = msg
|
|
30
|
+
# tensors are memory mapped for subprocess (and obv threads) so we
|
|
31
|
+
# can do the work inplace and result will be visible in main thread
|
|
32
|
+
tensor += add
|
|
33
|
+
tensor.pow_(power)
|
|
34
|
+
|
|
35
|
+
# we should not share a tensor shared with us as per pytorch docs,
|
|
36
|
+
# just send the id back
|
|
37
|
+
thread.send_msg_to_mainthread(tensor_id)
|
|
38
|
+
|
|
39
|
+
# we can also handle errors here, which will be re-raised in the main
|
|
40
|
+
# process
|
|
41
|
+
if tensor_id == 7:
|
|
42
|
+
raise ValueError("I fell asleep")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
data = torch.rand((5, 10))
|
|
47
|
+
thread = ThreadWithException(
|
|
48
|
+
target=worker, args=(2.5,), pass_self=True
|
|
49
|
+
)
|
|
50
|
+
# use thread or sub-process
|
|
51
|
+
# thread = ProcessWithException(
|
|
52
|
+
# target=worker, args=(2.5,), pass_self=True
|
|
53
|
+
# )
|
|
54
|
+
thread.start()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
for i in range(10):
|
|
58
|
+
thread.send_msg_to_thread((i, data, i / 2))
|
|
59
|
+
# if the thread raises an exception, get_msg_from_thread will
|
|
60
|
+
# re-raise it here
|
|
61
|
+
msg = thread.get_msg_from_thread()
|
|
62
|
+
if msg == EOFSignal:
|
|
63
|
+
# thread exited for whatever reason (not exception)
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
print(f"Thread processed tensor {i}")
|
|
67
|
+
finally:
|
|
68
|
+
# whatever happens, make sure thread is told to finish so it
|
|
69
|
+
# doesn't get stuck
|
|
70
|
+
thread.notify_to_end_thread()
|
|
71
|
+
thread.join()
|
|
72
|
+
|
|
73
|
+
When run, this prints::
|
|
74
|
+
|
|
75
|
+
Thread processed tensor 0
|
|
76
|
+
Thread processed tensor 1
|
|
77
|
+
Thread processed tensor 2
|
|
78
|
+
Thread processed tensor 3
|
|
79
|
+
Thread processed tensor 4
|
|
80
|
+
Thread processed tensor 5
|
|
81
|
+
Thread processed tensor 6
|
|
82
|
+
Thread processed tensor 7
|
|
83
|
+
Traceback (most recent call last):
|
|
84
|
+
File "threading.py", line 139, in user_func_runner
|
|
85
|
+
self.user_func(self, *self.args)
|
|
86
|
+
File "play.py", line 24, in worker
|
|
87
|
+
raise ValueError("I fell asleep")
|
|
88
|
+
ValueError: I fell asleep
|
|
89
|
+
|
|
90
|
+
The above exception was the direct cause of the following exception:
|
|
91
|
+
|
|
92
|
+
Traceback (most recent call last):
|
|
93
|
+
File "play.py", line 38, in <module>
|
|
94
|
+
msg = thread.get_msg_from_thread()
|
|
95
|
+
File "threading.py", line 203, in get_msg_from_thread
|
|
96
|
+
raise ExecutionFailure(
|
|
97
|
+
ExecutionFailure: Reporting failure from other thread/process
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
from queue import Queue
|
|
101
|
+
from threading import Thread
|
|
102
|
+
from typing import Any, Callable, Optional
|
|
103
|
+
|
|
104
|
+
import torch.multiprocessing as mp
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EOFSignal:
|
|
108
|
+
"""
|
|
109
|
+
This class object (not instance) is returned by the thread/process as an
|
|
110
|
+
indicator that someone exited or that you should exit.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ExecutionFailure(Exception):
|
|
117
|
+
"""
|
|
118
|
+
Exception class raised in the main thread when the function running in the
|
|
119
|
+
thread/process raises an exception.
|
|
120
|
+
|
|
121
|
+
Get the original exception using the `__cause__` property of this
|
|
122
|
+
exception.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ExceptionWithQueueMixIn:
|
|
129
|
+
"""
|
|
130
|
+
A mix-in that can be used with a secondary thread or sub-process to
|
|
131
|
+
facilitate communication with them and the passing back of exceptions and
|
|
132
|
+
signals such as when the thread/sub-process exits or when the main
|
|
133
|
+
thread/process want the sub-process/thread to end.
|
|
134
|
+
|
|
135
|
+
Communication happens bi-directionally via queues. These queues are not
|
|
136
|
+
limited in buffer size, so if there's potential for the queue to be backed
|
|
137
|
+
up because the main thread or sub-thread/process is unable to read and
|
|
138
|
+
process messages quickly enough, you must implement some kind of
|
|
139
|
+
back-pressure to prevent the queue from growing unlimitedly in size.
|
|
140
|
+
E.g. using a limited number of tokens that must be held to send a message
|
|
141
|
+
to prevent a sender from thrashing the queue. And these tokens can be
|
|
142
|
+
passed between main thread to sub-thread/process etc on each message and
|
|
143
|
+
when the message is done processing.
|
|
144
|
+
|
|
145
|
+
The main process uses `get_msg_from_thread` to raise exceptions in the
|
|
146
|
+
main thread that occurred in the sub-thread/process. So that must be
|
|
147
|
+
called in the main thread if you want to know if the sub-thread/process
|
|
148
|
+
exited.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
to_thread_queue: Queue
|
|
152
|
+
"""
|
|
153
|
+
Internal queue used to send messages to the thread from the main thread.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
from_thread_queue: Queue
|
|
157
|
+
"""
|
|
158
|
+
Internal queue used to send messages from the thread to the main thread.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
args: tuple = ()
|
|
162
|
+
"""
|
|
163
|
+
The args provided by the caller that will be passed to the function running
|
|
164
|
+
in the thread/process when it starts.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# user_func_runner must end with an eof msg to the main thread. This tracks
|
|
168
|
+
# whether the main thread saw the eof. If it didn't, we know there are more
|
|
169
|
+
# msgs in the queue waiting to be read
|
|
170
|
+
_saw_eof: bool = False
|
|
171
|
+
|
|
172
|
+
def __init__(self, target: Callable, pass_self: bool = False):
|
|
173
|
+
self.user_func = target
|
|
174
|
+
self.pass_self = pass_self
|
|
175
|
+
|
|
176
|
+
def user_func_runner(self) -> None:
|
|
177
|
+
"""
|
|
178
|
+
The internal function that runs the target function provided by the
|
|
179
|
+
user.
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
if self.user_func is not None:
|
|
183
|
+
if self.pass_self:
|
|
184
|
+
self.user_func(self, *self.args)
|
|
185
|
+
else:
|
|
186
|
+
self.user_func(*self.args)
|
|
187
|
+
except BaseException as e:
|
|
188
|
+
self.from_thread_queue.put(
|
|
189
|
+
("exception", e), block=True, timeout=None
|
|
190
|
+
)
|
|
191
|
+
finally:
|
|
192
|
+
self.from_thread_queue.put(("eof", None), block=True, timeout=None)
|
|
193
|
+
|
|
194
|
+
def send_msg_to_mainthread(self, value: Any) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Sends the `value` to the main thread, from the sub-thread/process.
|
|
197
|
+
|
|
198
|
+
The value must be pickleable if it's running in a sub-process. The main
|
|
199
|
+
thread then can read it using `get_msg_from_thread`.
|
|
200
|
+
|
|
201
|
+
The queue is not limited in size, so if there's potential for the
|
|
202
|
+
queue to be backed up because the main thread is unable to read quickly
|
|
203
|
+
enough, you must implement some kind of back-pressure to prevent
|
|
204
|
+
the queue from growing unlimitedly in size.
|
|
205
|
+
"""
|
|
206
|
+
self.from_thread_queue.put(("user", value), block=True, timeout=None)
|
|
207
|
+
|
|
208
|
+
def clear_remaining(self) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Celled in the main-thread as part of cleanup when we expect the
|
|
211
|
+
secondary thread to have exited (e.g. we sent it a message telling it
|
|
212
|
+
to).
|
|
213
|
+
|
|
214
|
+
It will drop any waiting messages sent by the secondary thread, but
|
|
215
|
+
more importantly, it will handle exceptions raised in the secondary
|
|
216
|
+
thread before it exited, that may not have yet been processed in the
|
|
217
|
+
main thread (e.g. we stopped listening to messages from the secondary
|
|
218
|
+
thread before we got an eof from it).
|
|
219
|
+
"""
|
|
220
|
+
while not self._saw_eof:
|
|
221
|
+
self.get_msg_from_thread()
|
|
222
|
+
|
|
223
|
+
def get_msg_from_thread(self, timeout: Optional[float] = None) -> Any:
|
|
224
|
+
"""
|
|
225
|
+
Gets a message from the sub-thread/process sent to the main thread.
|
|
226
|
+
|
|
227
|
+
This blocks forever until the message is sent by the sub-thread/process
|
|
228
|
+
and received by us. If `timeout` is not None, that's how long we block
|
|
229
|
+
here, in seconds, before raising an `Empty` Exception if no message was
|
|
230
|
+
received by then.
|
|
231
|
+
|
|
232
|
+
If the return value is the `EOFSignal` object, it means the
|
|
233
|
+
sub-thread/process has or is about to exit.
|
|
234
|
+
|
|
235
|
+
If the sub-thread/process has raised an exception, that exception is
|
|
236
|
+
caught and re-raised in the main thread when this method is called.
|
|
237
|
+
The exception raised is an `ExecutionFailure` and its `__cause__`
|
|
238
|
+
property is the original exception raised in the sub-thread/process.
|
|
239
|
+
|
|
240
|
+
A typical pattern is::
|
|
241
|
+
|
|
242
|
+
>>> try:
|
|
243
|
+
... msg = thread.get_msg_from_thread()
|
|
244
|
+
... if msg == EOFSignal:
|
|
245
|
+
... # thread exited
|
|
246
|
+
... pass
|
|
247
|
+
... else:
|
|
248
|
+
... # do something with the msg
|
|
249
|
+
... pass
|
|
250
|
+
... except ExecutionFailure as e:
|
|
251
|
+
... print(f"got exception {type(e.__cause__)}")
|
|
252
|
+
... print(f"with message {e.__cause__.args[0]}")
|
|
253
|
+
"""
|
|
254
|
+
msg, value = self.from_thread_queue.get(block=True, timeout=timeout)
|
|
255
|
+
if msg == "eof":
|
|
256
|
+
self._saw_eof = True
|
|
257
|
+
return EOFSignal
|
|
258
|
+
if msg == "exception":
|
|
259
|
+
raise ExecutionFailure(
|
|
260
|
+
"Reporting failure from other thread/process"
|
|
261
|
+
) from value
|
|
262
|
+
|
|
263
|
+
return value
|
|
264
|
+
|
|
265
|
+
def send_msg_to_thread(self, value: Any) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Sends the `value` to the sub-thread/process, from the main thread.
|
|
268
|
+
|
|
269
|
+
The value must be pickleable if it's sent to a sub-process. The thread
|
|
270
|
+
then can read it using `get_msg_from_mainthread`.
|
|
271
|
+
|
|
272
|
+
The queue is not limited in size, so if there's potential for the
|
|
273
|
+
queue to be backed up because the thread is unable to read quickly
|
|
274
|
+
enough, you must implement some kind of back-pressure to prevent
|
|
275
|
+
the queue from growing unlimitedly in size.
|
|
276
|
+
"""
|
|
277
|
+
self.to_thread_queue.put(("user", value), block=True, timeout=None)
|
|
278
|
+
|
|
279
|
+
def notify_to_end_thread(self) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Sends a message to the sub-process/thread that the main process wants
|
|
282
|
+
them to end. The sub-process/thread sees it by receiving an `EOFSignal`
|
|
283
|
+
message from `get_msg_from_mainthread` and it should exit asap.
|
|
284
|
+
"""
|
|
285
|
+
self.to_thread_queue.put(("eof", None), block=True, timeout=None)
|
|
286
|
+
|
|
287
|
+
def get_msg_from_mainthread(self, timeout: Optional[float] = None) -> Any:
|
|
288
|
+
"""
|
|
289
|
+
Gets a message from the main thread sent to the sub-thread/process.
|
|
290
|
+
|
|
291
|
+
This blocks forever until the message is sent by the main thread
|
|
292
|
+
and received by us. If `timeout` is not None, that's how long we block
|
|
293
|
+
here, in seconds, before raising an `Empty` Exception if no message was
|
|
294
|
+
received by then.
|
|
295
|
+
|
|
296
|
+
If the return value is the `EOFSignal` object, it means the
|
|
297
|
+
main thread has sent us an EOF message using `notify_to_end_thread`
|
|
298
|
+
because it wants us to exit.
|
|
299
|
+
|
|
300
|
+
A typical pattern is::
|
|
301
|
+
|
|
302
|
+
>>> msg = thread.get_msg_from_mainthread()
|
|
303
|
+
... if msg == EOFSignal:
|
|
304
|
+
... # we should exit asap
|
|
305
|
+
... return
|
|
306
|
+
... # do something with the msg
|
|
307
|
+
"""
|
|
308
|
+
msg, value = self.to_thread_queue.get(block=True, timeout=timeout)
|
|
309
|
+
if msg == "eof":
|
|
310
|
+
return EOFSignal
|
|
311
|
+
|
|
312
|
+
return value
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ThreadWithException(ExceptionWithQueueMixIn):
|
|
316
|
+
"""
|
|
317
|
+
Runs a target function in a secondary thread.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
thread: Thread = None
|
|
321
|
+
"""The thread running the function."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, target, args=(), **kwargs):
|
|
324
|
+
super().__init__(target=target, **kwargs)
|
|
325
|
+
self.to_thread_queue = Queue(maxsize=0)
|
|
326
|
+
self.from_thread_queue = Queue(maxsize=0)
|
|
327
|
+
self.args = args
|
|
328
|
+
self.thread = Thread(target=self.user_func_runner)
|
|
329
|
+
|
|
330
|
+
def start(self) -> None:
|
|
331
|
+
"""Starts the thread that runs the target function."""
|
|
332
|
+
self.thread.start()
|
|
333
|
+
|
|
334
|
+
def join(self, timeout: Optional[float] = None) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Waits and blocks until the thread exits. If timeout is given,
|
|
337
|
+
it's the duration to wait, in seconds, before returning.
|
|
338
|
+
|
|
339
|
+
To know if it exited, you need to check `is_alive` of the `thread`.
|
|
340
|
+
"""
|
|
341
|
+
self.thread.join(timeout=timeout)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class ProcessWithException(ExceptionWithQueueMixIn):
|
|
345
|
+
"""
|
|
346
|
+
Runs a target function in a sub-process.
|
|
347
|
+
|
|
348
|
+
Any data sent between the processes must be pickleable.
|
|
349
|
+
|
|
350
|
+
We run the function using `torch.multiprocessing`. Any tensors sent between
|
|
351
|
+
the main process and sub-process is memory mapped so that it doesn't copy
|
|
352
|
+
the tensor. So any edits in the main process/sub-process is seen in the
|
|
353
|
+
other as well. See https://pytorch.org/docs/stable/multiprocessing.html
|
|
354
|
+
for more details on this.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
process: mp.Process = None
|
|
358
|
+
"""The sub-process running the function."""
|
|
359
|
+
|
|
360
|
+
def __init__(self, target, args=(), **kwargs):
|
|
361
|
+
super().__init__(target=target, **kwargs)
|
|
362
|
+
ctx = mp.get_context("spawn")
|
|
363
|
+
self.to_thread_queue = ctx.Queue(maxsize=0)
|
|
364
|
+
self.from_thread_queue = ctx.Queue(maxsize=0)
|
|
365
|
+
self.process = ctx.Process(target=self.user_func_runner)
|
|
366
|
+
|
|
367
|
+
self.args = args
|
|
368
|
+
|
|
369
|
+
def start(self) -> None:
|
|
370
|
+
"""Starts the sub-process that runs the target function."""
|
|
371
|
+
self.process.start()
|
|
372
|
+
|
|
373
|
+
def join(self, timeout: Optional[float] = None) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Waits and blocks until the process exits. If timeout is given,
|
|
376
|
+
it's the duration to wait, in seconds, before returning.
|
|
377
|
+
|
|
378
|
+
To know if it exited, you need to check `is_alive` of the `process`.
|
|
379
|
+
"""
|
|
380
|
+
self.process.join(timeout=timeout)
|
cellfinder/core/tools/tools.py
CHANGED
|
@@ -1,19 +1,141 @@
|
|
|
1
|
+
from functools import wraps
|
|
1
2
|
from random import getrandbits, uniform
|
|
2
|
-
from typing import Optional
|
|
3
|
+
from typing import Callable, Optional, Type
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
6
|
+
import torch
|
|
5
7
|
from natsort import natsorted
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def
|
|
10
|
+
def inference_wrapper(func):
|
|
9
11
|
"""
|
|
10
|
-
|
|
12
|
+
Decorator that makes the decorated function/method run with
|
|
13
|
+
`torch.inference_mode` set to True.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@wraps(func)
|
|
17
|
+
def inner_function(*args, **kwargs):
|
|
18
|
+
with torch.inference_mode(True):
|
|
19
|
+
return func(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
return inner_function
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_max_possible_int_value(dtype: Type[np.number]) -> int:
|
|
25
|
+
"""
|
|
26
|
+
Returns the maximum allowed integer for a numpy array of given type.
|
|
27
|
+
|
|
28
|
+
If dtype is of integer type, it's the maximum value. If it's a floating
|
|
29
|
+
type, it's the maximum integer that can be accurately represented.
|
|
30
|
+
E.g. for float32, only integers up to 2**24 can be represented (due to
|
|
31
|
+
the number of bits representing the mantissa (significand).
|
|
11
32
|
"""
|
|
12
|
-
dtype = obj_in.dtype
|
|
13
33
|
if np.issubdtype(dtype, np.integer):
|
|
14
34
|
return np.iinfo(dtype).max
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
if np.issubdtype(dtype, np.floating):
|
|
36
|
+
mant = np.finfo(dtype).nmant
|
|
37
|
+
return 2**mant
|
|
38
|
+
raise ValueError("datatype must be of integer or floating data type")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_min_possible_int_value(dtype: Type[np.number]) -> int:
|
|
42
|
+
"""
|
|
43
|
+
Returns the minimum allowed integer for a numpy array of given type.
|
|
44
|
+
|
|
45
|
+
If dtype is of integer type, it's the minimum value. If it's a floating
|
|
46
|
+
type, it's the minimum integer that can be accurately represented.
|
|
47
|
+
E.g. for float32, only integers up to -2**24 can be represented (due to
|
|
48
|
+
the number of bits representing the mantissa (significand).
|
|
49
|
+
"""
|
|
50
|
+
if np.issubdtype(dtype, np.integer):
|
|
51
|
+
return np.iinfo(dtype).min
|
|
52
|
+
if np.issubdtype(dtype, np.floating):
|
|
53
|
+
mant = np.finfo(dtype).nmant
|
|
54
|
+
# the sign bit is separate so we have the full mantissa for value
|
|
55
|
+
return -(2**mant)
|
|
56
|
+
raise ValueError("datatype must be of integer or floating data type")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_data_converter(
|
|
60
|
+
src_dtype: Type[np.number], dest_dtype: Type[np.floating]
|
|
61
|
+
) -> Callable[[np.ndarray], np.ndarray]:
|
|
62
|
+
"""
|
|
63
|
+
Returns a function that can be called to convert one data-type to another,
|
|
64
|
+
scaling the data down as needed.
|
|
65
|
+
|
|
66
|
+
If the maximum value supported by the input data-type is smaller than that
|
|
67
|
+
supported by destination data-type, the data will be scaled by the ratio
|
|
68
|
+
of maximum integer representable by the `output / input` data-types.
|
|
69
|
+
If the max is equal or less, it's simply converted to the target type.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
src_dtype : np.dtype
|
|
74
|
+
The data-type of the input data.
|
|
75
|
+
dest_dtype : np.dtype
|
|
76
|
+
The data-type of the returned data. Currently, it must be a floating
|
|
77
|
+
type and `np.float32` or `np.float64`.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
callable: function
|
|
82
|
+
A function that takes a single input data parameter and returns
|
|
83
|
+
the converted data.
|
|
84
|
+
"""
|
|
85
|
+
if not np.issubdtype(dest_dtype, np.float32) and not np.issubdtype(
|
|
86
|
+
dest_dtype, np.float64
|
|
87
|
+
):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Destination dtype must be a float32 or float64, "
|
|
90
|
+
f"but it is {dest_dtype}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
in_min = get_min_possible_int_value(src_dtype)
|
|
94
|
+
in_max = get_max_possible_int_value(src_dtype)
|
|
95
|
+
out_min = get_min_possible_int_value(dest_dtype)
|
|
96
|
+
out_max = get_max_possible_int_value(dest_dtype)
|
|
97
|
+
in_abs_max = max(in_max, abs(in_min))
|
|
98
|
+
out_abs_max = max(out_max, abs(out_min))
|
|
99
|
+
|
|
100
|
+
def unchanged(data: np.ndarray) -> np.ndarray:
|
|
101
|
+
return np.asarray(data)
|
|
102
|
+
|
|
103
|
+
def float_to_float_scale_down(data: np.ndarray) -> np.ndarray:
|
|
104
|
+
return ((np.asarray(data) / in_abs_max) * out_abs_max).astype(
|
|
105
|
+
dest_dtype
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def int_to_float_scale_down(data: np.ndarray) -> np.ndarray:
|
|
109
|
+
# data must fit in float64
|
|
110
|
+
data = np.asarray(data).astype(np.float64)
|
|
111
|
+
return ((data / in_abs_max) * out_abs_max).astype(dest_dtype)
|
|
112
|
+
|
|
113
|
+
def to_float_unscaled(data: np.ndarray) -> np.ndarray:
|
|
114
|
+
return np.asarray(data).astype(dest_dtype)
|
|
115
|
+
|
|
116
|
+
if src_dtype == dest_dtype:
|
|
117
|
+
return unchanged
|
|
118
|
+
|
|
119
|
+
# out can hold the largest in values - just convert to float
|
|
120
|
+
if out_min <= in_min < in_max <= out_max:
|
|
121
|
+
return to_float_unscaled
|
|
122
|
+
|
|
123
|
+
# need to scale down before converting to float
|
|
124
|
+
if np.issubdtype(src_dtype, np.integer):
|
|
125
|
+
# if going to float32 and it didn't fit input must fit in 64-bit float
|
|
126
|
+
# so we can temp store it there to scale. If going to float64, same.
|
|
127
|
+
if in_max > get_max_possible_int_value(
|
|
128
|
+
np.float64
|
|
129
|
+
) or in_min < get_min_possible_int_value(np.float64):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"The input datatype {src_dtype} cannot fit in a "
|
|
132
|
+
f"64-bit float"
|
|
133
|
+
)
|
|
134
|
+
return int_to_float_scale_down
|
|
135
|
+
|
|
136
|
+
# for float input, however big it is, we can always scale it down in the
|
|
137
|
+
# input data type before changing type
|
|
138
|
+
return float_to_float_scale_down
|
|
17
139
|
|
|
18
140
|
|
|
19
141
|
def union(a, b):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cellfinder
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0a0
|
|
4
4
|
Summary: Automated 3D cell detection in large microscopy images
|
|
5
5
|
Author-email: "Adam Tyson, Christian Niedworok, Charly Rousseau" <code@adamltyson.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -33,7 +33,7 @@ Requires-Dist: numpy
|
|
|
33
33
|
Requires-Dist: scikit-image
|
|
34
34
|
Requires-Dist: scikit-learn
|
|
35
35
|
Requires-Dist: keras==3.5.0
|
|
36
|
-
Requires-Dist: torch
|
|
36
|
+
Requires-Dist: torch!=2.4,>=2.1.0
|
|
37
37
|
Requires-Dist: tifffile
|
|
38
38
|
Requires-Dist: tqdm
|
|
39
39
|
Provides-Extra: dev
|
|
@@ -46,6 +46,7 @@ Requires-Dist: pytest-qt; extra == "dev"
|
|
|
46
46
|
Requires-Dist: pytest-timeout; extra == "dev"
|
|
47
47
|
Requires-Dist: pytest; extra == "dev"
|
|
48
48
|
Requires-Dist: tox; extra == "dev"
|
|
49
|
+
Requires-Dist: pooch>=1; extra == "dev"
|
|
49
50
|
Provides-Extra: napari
|
|
50
51
|
Requires-Dist: brainglobe-napari-io; extra == "napari"
|
|
51
52
|
Requires-Dist: magicgui; extra == "napari"
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
cellfinder/__init__.py,sha256=S5oQ3EORuyQTMYC4uUuzGKZ23J3Ya6q-1DOBib1KfiA,1166
|
|
2
2
|
cellfinder/cli_migration_warning.py,sha256=gPtNrtnXvWpl5q0k_EGAQZg0DwcpCmuBTgpg56n5NfA,1578
|
|
3
3
|
cellfinder/core/__init__.py,sha256=pRFuQsl78HEK0S6gvhJw70QLbjjSBzP-GFO0AtVaGtk,62
|
|
4
|
-
cellfinder/core/main.py,sha256=
|
|
4
|
+
cellfinder/core/main.py,sha256=QiLo8qtK6hRT_cu-xSP9k93EE-dZjmb2xxocsY-hTwU,3836
|
|
5
5
|
cellfinder/core/types.py,sha256=lTqWE4v0SMM0qLAZJdyAzqV1nLgDtobEpglNJcXt160,106
|
|
6
6
|
cellfinder/core/classify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
cellfinder/core/classify/augment.py,sha256=8dMbM7KhimM6NMgdMC53oHoCfYj1CIB-h3Yk8CZAxPw,6321
|
|
8
|
-
cellfinder/core/classify/classify.py,sha256=
|
|
8
|
+
cellfinder/core/classify/classify.py,sha256=UN5j4-5RlsKNt9wd1A8udCjW4t-D4MySEyQ9BIkCckI,3529
|
|
9
9
|
cellfinder/core/classify/cube_generator.py,sha256=jC5aogTVy213PHouViSR9CgKkuOks3yk6csQC5kRoOE,17125
|
|
10
10
|
cellfinder/core/classify/resnet.py,sha256=vGa85y_NcQnOXwAt5EtatLx5mrO8IoShCcNKtJ5-EFg,10034
|
|
11
11
|
cellfinder/core/classify/tools.py,sha256=s5PEKAsZVbVuoferZ_nqMUM0f2bV_8WEKsdKe3SXEuE,2560
|
|
12
12
|
cellfinder/core/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
cellfinder/core/config/cellfinder.conf,sha256=5i8axif7ekMutKDiVnZRs-LiJrgVQljg_beltidqtNk,56
|
|
14
14
|
cellfinder/core/detect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
cellfinder/core/detect/detect.py,sha256=
|
|
15
|
+
cellfinder/core/detect/detect.py,sha256=cd6D3XOzP5HEA_Hb3Ik4AVmNzu0im4sQxV46cCY6izU,8056
|
|
16
16
|
cellfinder/core/detect/filters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
cellfinder/core/detect/filters/setup_filters.py,sha256=
|
|
17
|
+
cellfinder/core/detect/filters/setup_filters.py,sha256=gvWhdGVdGrK_Ab-t0kKPnXaf43qLi78ed_-vNPI0nE4,14554
|
|
18
18
|
cellfinder/core/detect/filters/plane/__init__.py,sha256=lybcPbVDnurEQeq2FiLq0zR8p_ztarQOlajhdh1Q2-4,40
|
|
19
|
-
cellfinder/core/detect/filters/plane/classical_filter.py,sha256=
|
|
20
|
-
cellfinder/core/detect/filters/plane/plane_filter.py,sha256=
|
|
21
|
-
cellfinder/core/detect/filters/plane/tile_walker.py,sha256=
|
|
19
|
+
cellfinder/core/detect/filters/plane/classical_filter.py,sha256=X5k266tbl9EHRVY5dls53B5IZlmP7U0UB9BsZ1ey_pc,13250
|
|
20
|
+
cellfinder/core/detect/filters/plane/plane_filter.py,sha256=6FbAjxgt39_RdGDXC_pQ9ndtZtknuHJeTWtbR8cReEA,6102
|
|
21
|
+
cellfinder/core/detect/filters/plane/tile_walker.py,sha256=IiQibvWKnYlgl9h414fRklV7C2xZ0vXNmJ9t89DhYuI,4863
|
|
22
22
|
cellfinder/core/detect/filters/volume/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
cellfinder/core/detect/filters/volume/ball_filter.py,sha256=
|
|
24
|
-
cellfinder/core/detect/filters/volume/structure_detection.py,sha256=
|
|
25
|
-
cellfinder/core/detect/filters/volume/structure_splitting.py,sha256=
|
|
26
|
-
cellfinder/core/detect/filters/volume/volume_filter.py,sha256=
|
|
23
|
+
cellfinder/core/detect/filters/volume/ball_filter.py,sha256=qIDo206f558B46tpxrOGXasP8VE6pZxju-duvhPRuNA,14440
|
|
24
|
+
cellfinder/core/detect/filters/volume/structure_detection.py,sha256=AIHq-5u5VFpKBBLEsE1Py-MlndbL8T0zXu0Bq2CI16Y,12916
|
|
25
|
+
cellfinder/core/detect/filters/volume/structure_splitting.py,sha256=0LGE8cQ6EJIsKqWWmd087Q3At8m-PUDmmPjCZKV1TdY,10138
|
|
26
|
+
cellfinder/core/detect/filters/volume/volume_filter.py,sha256=o9_4IsITedhqkhfwRsIhE53_zLNcFjgQzXbMMddH_q4,20544
|
|
27
27
|
cellfinder/core/download/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
28
|
cellfinder/core/download/cli.py,sha256=X9L9ZIkWqs58hX8G8q_0AKN4gk8BhydGxI9nLpdHQQE,1764
|
|
29
29
|
cellfinder/core/download/download.py,sha256=YQP4NMM9Xt0rECRJEV5z6RCZNi5uCFhohFnDreH0qHA,3277
|
|
30
|
+
cellfinder/core/tools/IO.py,sha256=jK1R_6YahQ_MbPlwg1IM-Fp-din-hLBNq71Jxn3q2WQ,1356
|
|
30
31
|
cellfinder/core/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
32
|
cellfinder/core/tools/array_operations.py,sha256=LDbqWA_N2YtxeKS877QYdJRL2FCMC1R1ExJtdb6-vEA,3371
|
|
32
33
|
cellfinder/core/tools/geometry.py,sha256=xlEQQmVQ9jcXqRzUqU554P8VxkaWkc5J_YhjkkKlI0Q,1124
|
|
@@ -34,8 +35,9 @@ cellfinder/core/tools/image_processing.py,sha256=z27bGjf3Iv3G4Nt1OYzpEnIYQNc4nNo
|
|
|
34
35
|
cellfinder/core/tools/prep.py,sha256=YU4VDyjEsOXn3S3gymEfPUzka-WYiHmR4Q4XdRebhDI,2175
|
|
35
36
|
cellfinder/core/tools/source_files.py,sha256=vvwsIMe1ULKvXg_x22L75iqvCyMjEbUjJsDFQk3GYzY,856
|
|
36
37
|
cellfinder/core/tools/system.py,sha256=WvEzPr7v-ohLDnzf4n13TMcN5OYIAXOEkaSmrHzdnwc,2438
|
|
38
|
+
cellfinder/core/tools/threading.py,sha256=EmDLF22ZvXJnsWg7g6sv4cZgNweC0YrVw2o7QXjrO9s,14094
|
|
37
39
|
cellfinder/core/tools/tiff.py,sha256=NzIz6wq2GzxmcIhawFMwZADe-uQO2rIG46H7xkpGKLs,2899
|
|
38
|
-
cellfinder/core/tools/tools.py,sha256=
|
|
40
|
+
cellfinder/core/tools/tools.py,sha256=opMGC5GBBsId0dmL8V0KQrI4U70w_D_KtGQYpZNeHYQ,9390
|
|
39
41
|
cellfinder/core/train/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
42
|
cellfinder/core/train/train_yml.py,sha256=9QXv2wk24G8hYskMnBij7OngEELUWySK2fH4NFbYWw4,13260
|
|
41
43
|
cellfinder/napari/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -51,9 +53,9 @@ cellfinder/napari/detect/thread_worker.py,sha256=PWM3OE-FpK-dpdhaE_Gi-2lD3u8sL-S
|
|
|
51
53
|
cellfinder/napari/train/__init__.py,sha256=xo4CK-DvSecInGEc2ohcTgQYlH3iylFnGvKTCoq2WkI,35
|
|
52
54
|
cellfinder/napari/train/train.py,sha256=zJY7zKcLqDTDtD76thmbwViEU4tTFCmXZze-zHsTpoo,5941
|
|
53
55
|
cellfinder/napari/train/train_containers.py,sha256=1wZ_GPe7B5XsLYs5XIx4m8GMw5KeVhg6SchhPtXu4V8,4386
|
|
54
|
-
cellfinder-1.
|
|
55
|
-
cellfinder-1.
|
|
56
|
-
cellfinder-1.
|
|
57
|
-
cellfinder-1.
|
|
58
|
-
cellfinder-1.
|
|
59
|
-
cellfinder-1.
|
|
56
|
+
cellfinder-1.4.0a0.dist-info/LICENSE,sha256=Tw8iMytIDXLSmcIUsbQmRWojstl9yOWsPCx6ZT6dZLY,1564
|
|
57
|
+
cellfinder-1.4.0a0.dist-info/METADATA,sha256=X5NbLqZZpyJC2_JDAEo03MHDAjgzYN7SJsh_x2vkS48,7122
|
|
58
|
+
cellfinder-1.4.0a0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
59
|
+
cellfinder-1.4.0a0.dist-info/entry_points.txt,sha256=cKKjU8GPiN-TRelG2sT2JCKAcB9XDCjP6g9atE9pSoY,247
|
|
60
|
+
cellfinder-1.4.0a0.dist-info/top_level.txt,sha256=jyTQzX-tDjbsMr6s-E71Oy0IKQzmHTXSk4ZhpG5EDSE,11
|
|
61
|
+
cellfinder-1.4.0a0.dist-info/RECORD,,
|
|
File without changes
|