forked from marimo-team/marimo-gh-pages-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBioimage_Analysis_Intutions.py
More file actions
362 lines (285 loc) · 13.3 KB
/
Bioimage_Analysis_Intutions.py
File metadata and controls
362 lines (285 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
import marimo
__generated_with = "0.14.12"
app = marimo.App(width="medium", html_head_file="head.html")
with app.setup:
# Initialization code that runs before all other cells
import marimo as mo
__generated_with = "0.13.11"
with mo.status.spinner(title="Installing and importing packages") as _spinner:
import os
import numpy
import skimage
import wigglystuff
_spinner.update()
@app.class_definition(hide_code=True)
class ProcessedImage:
def __init__(self,im,gt=False):
self.raw = im
self.im = im
self.gt = gt
self.disp_image = False
self.cache = {}
self.named_cache = {}
def return_raw(self):
return self.raw
def im(self):
return self.im
def blur(self,diam=False):
if not diam:
diam = 10
return skimage.filters.gaussian(self.im,sigma=diam)
def roll_ball(self,diam=False):
if not diam:
diam = 100
return self.im-(skimage.restoration.rolling_ball(self.im,radius=diam/2))
def power_transform(self,exp_to_use=False):
import numpy
if not exp_to_use:
exp_to_use=1
if exp_to_use <1:
im = numpy.where(self.im==0,0.00000001,self.im)
else:
im = self.im
return im**(exp_to_use)
def invert(self):
return skimage.util.invert(self.im)
def sato(self,sigma=False):
if not sigma:
sigmas = range(3,8,1)
else:
sigmas = range(int(sigma)-2,int(sigma)+3,1)
return skimage.filters.sato(self.im,sigmas=sigmas,black_ridges=False)
def texture_transform(self,sigma=False):
if not sigma:
sigma = 5
img_mean = skimage.filters.gaussian(self.im, sigma, mode="constant")
img_squared = skimage.filters.gaussian(self.im ** 2, sigma, mode="constant")
return img_squared - img_mean ** 2
def tophat(self,diam=False):
if not diam:
diam = 5
return skimage.morphology.white_tophat(self.im,footprint=skimage.morphology.disk(diam/2))
def set_named_cache(self, name):
#Probably do a check that the name isn't "current" (if we don't want user to have to save all things going to math)
#Could also maybe use it if we want to be able to add custom intermediates to the display box - will ponder when we get there
self.named_cache[name] = self.im
return self.im
def get_named_image(self,name):
if name.lower() == '"current"':
return self.im
else:
if name not in self.named_cache.keys():
print(f"You have an undefined name in your list; {name} cannot be easily mapped to any of the following: {", ".join(self.named_cache.keys())}. Please check spelling and try again")
return self.named_cache[name]
def subtract(self,im1name,val_or_im2name):
if type(val_or_im2name) != float:
val_or_im2name = self.get_named_image(val_or_im2name)
return self.get_named_image(im1name) - val_or_im2name
def add(self,im1name,val_or_im2name):
pass
def divide(self,im1name,val_or_im2name):
pass
def multiply(self,im1name,val_or_im2name):
pass
def clip(self):
pass
def label(self):
pass
def measure(self):
pass
def score(self):
pass
def distance_transform(self):
pass
def fill_holes(self,diam=False):
pass
def erode(self,diam=False):
pass
def dilate(self,diam=False):
pass
def open(self,diam=False):
pass
def close(self,diam=False):
pass
def test_for_numeric(self,param):
#This is gross but isnumeric barfs on decimals, which is insane
try:
return float(param.strip())
except:
return param.strip()
def parse_vals(self,value_list):
self.im = self.raw
current_processing_state=""
defined_funcs = {
'blur':self.blur, 'rollingball':self.roll_ball, 'powertransform':self.power_transform,
'invert':self.invert, 'tophat':self.tophat, 'ridge':self.sato,
'savelaststepas':self.set_named_cache, 'subtract':self.subtract, 'raw':self.return_raw, 'texturetransform':self.texture_transform}
for val in value_list:
if "(" in val:
userfunc, userargstring = val.split("(")
userargs= [self.test_for_numeric(x) for x in userargstring.split(")")[0].split(",")]
else:
userfunc=val
userargs=False
parseduserfunc = userfunc.lower().replace(" ","")
if parseduserfunc not in defined_funcs.keys(): #TODO later: fuzzy matching (but this is fine for now)
print(f"You have an undefined function in your list; {userfunc} cannot be easily mapped to any of the following: {", ".join(defined_funcs.keys())}. Please check spelling and try again")
return(self.im)
else:
func = defined_funcs[parseduserfunc]
if not userargs:
current_processing_state+=f"{parseduserfunc};"
if current_processing_state not in self.cache.keys():
self.im = func()
self.cache[current_processing_state] = self.im
else:
self.im = self.cache[current_processing_state]
else:
current_processing_state+=f"{parseduserfunc}({','.join([str(x) for x in userargs])});"
if current_processing_state not in self.cache.keys():
self.im = func(*userargs)
self.cache[current_processing_state] = self.im
else:
self.im = self.cache[current_processing_state]
return self.im
@app.cell(hide_code=True)
def _():
mo.md(
r"""
#Lesson 1 - Filters of Many Flavors (Making sure the thing I care about is bright, and everything else is dark)
## Let's Start With A Raw Image
All these images come from the `skimage.data` library - see that library for more information on any image!
"""
)
return
@app.cell(hide_code=True)
def _():
def load_image(imdesc):
data_dir = "public/data/"
loaded_image = skimage.io.imread(os.path.join(mo.notebook_location(), data_dir,imdesc["name"]))
if imdesc["3D_slice"]:
loaded_image = skimage.util.img_as_float(loaded_image[imdesc["3D_slice"],:,:])
if imdesc["hist_deconvolve"]:
loaded_image = skimage.color.rgb2hed(loaded_image)[:,:,2]
loaded_image = skimage.util.img_as_float(skimage.exposure.rescale_intensity(loaded_image, in_range = (0, numpy.percentile(loaded_image,99))))
if imdesc["4D"]:
loaded_image = skimage.util.img_as_float(loaded_image[14,1,:,:])
if imdesc["orig_color"]:
loaded_image = skimage.util.img_as_float(skimage.color.rgb2gray(loaded_image))
return loaded_image
test_images = {
"Brain Slice":{"name":"brain.tiff","orig_color":False,"hist_deconvolve":False,"3D_slice":5, "4D":False},
"Bricks":{"name":"brick.png","orig_color":False,"hist_deconvolve":False,"3D_slice":False, "4D":False},
"Hubble Deep Field":{"name":"hubble_deep_field.jpg","orig_color":True,"hist_deconvolve":False,"3D_slice":False, "4D":False},
"Human Nuclei":{"name":"mitosis.tif","orig_color":False,"hist_deconvolve":False,"3D_slice":False, "4D":False},
"Kidney Cells": {"name":"ihc.png","orig_color":False,"hist_deconvolve":True,"3D_slice":False, "4D":False},
"Moon Surface":{"name":"moon.png","orig_color":False,"hist_deconvolve":False,"3D_slice":False, "4D":False},
"Nuclear Envelope": {"name":"protein_transport.tif","orig_color":False,"hist_deconvolve":False,"3D_slice":False, "4D":True},
"Pompeii Coins":{"name":"coins.png","orig_color":False,"hist_deconvolve":False,"3D_slice":False, "4D":False},
}
im_choice = mo.ui.dropdown(label="Pick an image to use (if nothing shows up at first, just select any option",options=test_images.keys(),value="Human Nuclei")
im_choice
return im_choice, load_image, test_images
@app.cell
def _(im_choice, load_image, test_images):
image = load_image(test_images[im_choice.value])
processed_image = ProcessedImage(image)
mo.image(processed_image.raw)
return (processed_image,)
@app.cell(hide_code=True)
def _():
mo.md(
r"""
## What shall we do to it?
Pass one or more implemented operations into the list (_try literally cutting and pasting them!_), and see how your output image changes!
Some operations may be slow (especially if you make them very different than the listed default value); you must wait for the current process to be finish to adjust options in the list, and the whole pipeline runs every time. This is somewhat accelerated by caching all the intermediates; future improvements may be to turn caching off (or limit cache length or have a "clear cache button") and the ability to turn on-the-fly processing on and off, author time permitting.
Currently implemented operations are listed below - some contain optional parameters that you may (or may not) wish to play with. IE for `Blur`, you can pass `Blur (20)` or `Blur(2)`; you can also just pass `Blur` and it will try to use a reasonable default. More operations (and links to more detailed help) coming soon!
"""
)
return
@app.cell(hide_code=True)
def _():
lesson_1_funcs = r"""
* Blur(10)
* *Gaussian Blur; Number in parentheses equals: diameter (in pixels) of the smoothing kernel sigma*
* Rolling Ball(100)
* *Rolling ball background subtraction; Number in parentheses equals: diameter (in pixels) of the ball to be rolled*
* Invert
* *Invert the image (best for grayscale, or after something like ridge enhancement*
* PowerTransform(1)
* *Multiply the image by an exponent - an exponent value of >1 will make the brightest and dimmest pixel be proportionately farther from each other (but all at lower absolute numbers); conversely, an exponent of <1 will decrease the fold-change between foreground and background but will make the numbers higher. A value of 1 will do nothing!*
* Tophat(5)
* *Enhance round-ish things of the diameter passed in - remove anything larger than that diameter, approximately.*
* Ridge(5)
* *Enhance anything vaguely tube-like*
* TextureTransform(5)
* *Enhance areas with textures of a particular size. See how it performs on an inverted (vs not) image!*
"""
mo.accordion({"See the list of options":mo.md(lesson_1_funcs)})
return
@app.cell
def _():
mo.md(r"""## Let's get filtering!""")
return
@app.cell(hide_code=True)
def _():
imfunctions = mo.ui.anywidget(
wigglystuff.SortableList(
[
"Blur(2)",
"Rolling Ball(50)"
],addable=True, removable=True, editable=True
)
)
imfunctions
#TODO - maybe also add switch/button for "update image live", if this becomes slow
#TODO - also maybe a clear cache button, if eventually figure that out
#TODO - "Clear all" button
#TODO - "Generate Suggested" button (once we can add a few different images)
return (imfunctions,)
@app.cell
def _():
mo.md(
r"""
## Behold, a Processed Image!
**How close did you get to "the thing I care about is bright, and everything else is dark?"**
"""
)
return
@app.cell(hide_code=True)
def _(imfunctions, processed_image):
processed_image.parse_vals(imfunctions.value["value"])
to_show = processed_image.im
mo.image(to_show)
return (to_show,)
@app.cell
def _():
mo.md(
r"""
## What happens now if we try to segment from this image?
For now, we'll just do a simple thresholding and treat all connected components as part of the same object. We'll talk about more advanced options soon!
Note that many of these thresholding methods also have tunable parameters; for now, we'll just keep them using their defaults.
"""
)
return
@app.cell
def _():
thresholding_options = {"Li":skimage.filters.threshold_li, "Otsu":skimage.filters.threshold_otsu, "Sauvola":skimage.filters.threshold_sauvola}
possible_thresholds = list(thresholding_options.keys())
threshold = mo.ui.dropdown(label="Pick a thresholding method",options=possible_thresholds,value=possible_thresholds[0])
clear_edges = mo.ui.checkbox(label="Remove objects that touch the edge?")
mo.vstack([threshold,clear_edges])
return clear_edges, threshold, thresholding_options
@app.cell
def _(clear_edges, processed_image, threshold, thresholding_options, to_show):
#Why aren't we making it part of the class? Because it doesn't respond to changes in .im
def threshold_and_label(processed_image, raw_image, thresh_dict, threshold_name, do_clear_edges, size_range):
to_label = processed_image > thresh_dict[threshold_name](processed_image)
if do_clear_edges:
to_label = skimage.segmentation.clear_border(to_label)
labels = skimage.measure.label(to_label)
return skimage.color.label2rgb(labels,image=raw_image, bg_label=0)
mo.image(threshold_and_label(to_show, processed_image.raw, thresholding_options,threshold.value,clear_edges.value,False))
return
if __name__ == "__main__":
app.run()