spacr 0.9.1__py3-none-any.whl → 0.9.3__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.
- spacr/app_annotate.py +4 -0
- spacr/gui_core.py +2 -2
- spacr/gui_elements.py +79 -10
- spacr/gui_utils.py +44 -2
- spacr/measure.py +166 -23
- spacr/plot.py +1 -1
- spacr/resources/icons/flow_chart_v2.png +0 -0
- spacr/resources/icons/flow_chart_v3.png +0 -0
- spacr/settings.py +9 -3
- spacr/utils.py +10 -1
- {spacr-0.9.1.dist-info → spacr-0.9.3.dist-info}/LICENSE +1 -1
- spacr-0.9.3.dist-info/METADATA +207 -0
- {spacr-0.9.1.dist-info → spacr-0.9.3.dist-info}/RECORD +16 -14
- spacr-0.9.1.dist-info/METADATA +0 -144
- {spacr-0.9.1.dist-info → spacr-0.9.3.dist-info}/WHEEL +0 -0
- {spacr-0.9.1.dist-info → spacr-0.9.3.dist-info}/entry_points.txt +0 -0
- {spacr-0.9.1.dist-info → spacr-0.9.3.dist-info}/top_level.txt +0 -0
spacr/app_annotate.py
CHANGED
@@ -30,6 +30,10 @@ def initiate_annotation_app(parent_frame):
|
|
30
30
|
settings['percentiles'] = list(map(convert_to_number, settings['percentiles'].split(','))) if settings['percentiles'] else [2, 98]
|
31
31
|
settings['normalize'] = settings['normalize'].lower() == 'true'
|
32
32
|
settings['normalize_channels'] = settings['normalize_channels'].split(',')
|
33
|
+
settings['outline'] = settings['outline'].split(',') if settings['outline'] else None
|
34
|
+
settings['outline_threshold_factor'] = float(settings['outline_threshold_factor']) if settings['outline_threshold_factor'] else 1.0
|
35
|
+
settings['outline_sigma'] = float(settings['outline_threshold_factor']) if settings['outline_threshold_factor'] else 1.0
|
36
|
+
|
33
37
|
try:
|
34
38
|
settings['measurement'] = settings['measurement'].split(',') if settings['measurement'] else None
|
35
39
|
settings['threshold'] = None if settings['threshold'].lower() == 'none' else int(settings['threshold'])
|
spacr/gui_core.py
CHANGED
@@ -361,8 +361,8 @@ def setup_plot_section(vertical_container, settings_type):
|
|
361
361
|
if settings_type == 'map_barcodes':
|
362
362
|
current_dir = os.path.dirname(__file__)
|
363
363
|
resources_path = os.path.join(current_dir, 'resources', 'icons')
|
364
|
-
gif_path = os.path.join(resources_path, 'dna_matrix.mp4')
|
365
|
-
display_media_in_plot_frame(gif_path, plot_frame)
|
364
|
+
#gif_path = os.path.join(resources_path, 'dna_matrix.mp4')
|
365
|
+
#display_media_in_plot_frame(gif_path, plot_frame)
|
366
366
|
|
367
367
|
canvas = FigureCanvasTkAgg(figure, master=plot_frame)
|
368
368
|
canvas.get_tk_widget().configure(cursor='arrow', highlightthickness=0)
|
spacr/gui_elements.py
CHANGED
@@ -10,18 +10,23 @@ import numpy as np
|
|
10
10
|
import pandas as pd
|
11
11
|
from PIL import Image, ImageOps, ImageTk, ImageDraw, ImageFont, ImageEnhance
|
12
12
|
from concurrent.futures import ThreadPoolExecutor
|
13
|
-
from skimage.exposure import rescale_intensity
|
14
13
|
from IPython.display import display, HTML
|
15
14
|
import imageio.v2 as imageio
|
16
15
|
from collections import deque
|
16
|
+
from skimage.filters import threshold_otsu
|
17
|
+
from skimage.exposure import rescale_intensity
|
17
18
|
from skimage.draw import polygon, line
|
18
19
|
from skimage.transform import resize
|
19
|
-
from
|
20
|
+
from skimage.morphology import dilation, disk
|
21
|
+
from skimage.segmentation import find_boundaries
|
22
|
+
from skimage.util import img_as_ubyte
|
23
|
+
from scipy.ndimage import binary_fill_holes, label, gaussian_filter
|
20
24
|
from tkinter import ttk, scrolledtext
|
21
25
|
from sklearn.model_selection import train_test_split
|
22
26
|
from xgboost import XGBClassifier
|
23
27
|
from sklearn.metrics import classification_report, confusion_matrix
|
24
28
|
|
29
|
+
|
25
30
|
fig = None
|
26
31
|
|
27
32
|
def restart_gui_app(root):
|
@@ -2209,7 +2214,7 @@ class ModifyMaskApp:
|
|
2209
2214
|
self.update_display()
|
2210
2215
|
|
2211
2216
|
class AnnotateApp:
|
2212
|
-
def __init__(self, root, db_path, src, image_type=None, channels=None, image_size=200, annotation_column='annotate', normalize=False, percentiles=(1, 99), measurement=None, threshold=None, normalize_channels=None):
|
2217
|
+
def __init__(self, root, db_path, src, image_type=None, channels=None, image_size=200, annotation_column='annotate', normalize=False, percentiles=(1, 99), measurement=None, threshold=None, normalize_channels=None, outline=None, outline_threshold_factor=1, outline_sigma=1):
|
2213
2218
|
self.root = root
|
2214
2219
|
self.db_path = db_path
|
2215
2220
|
self.src = src
|
@@ -2237,7 +2242,10 @@ class AnnotateApp:
|
|
2237
2242
|
self.measurement = measurement
|
2238
2243
|
self.threshold = threshold
|
2239
2244
|
self.normalize_channels = normalize_channels
|
2240
|
-
|
2245
|
+
self.outline = outline #([s.strip().lower() for s in outline.split(',') if s.strip()]if isinstance(outline, str) and outline else None)
|
2246
|
+
self.outline_threshold_factor = outline_threshold_factor
|
2247
|
+
self.outline_sigma = outline_sigma
|
2248
|
+
|
2241
2249
|
style_out = set_dark_style(ttk.Style())
|
2242
2250
|
self.font_loader = style_out['font_loader']
|
2243
2251
|
self.font_size = style_out['font_size']
|
@@ -2337,7 +2345,12 @@ class AnnotateApp:
|
|
2337
2345
|
'percentiles': ','.join(map(str, self.percentiles)),
|
2338
2346
|
'measurement': ','.join(self.measurement) if self.measurement else '',
|
2339
2347
|
'threshold': str(self.threshold) if self.threshold is not None else '',
|
2340
|
-
'normalize_channels': ','.join(self.normalize_channels) if self.normalize_channels else ''
|
2348
|
+
'normalize_channels': ','.join(self.normalize_channels) if self.normalize_channels else '',
|
2349
|
+
'outline': ','.join(self.outline) if self.outline else '',
|
2350
|
+
'outline_threshold_factor': str(self.outline_threshold_factor) if hasattr(self, 'outline_threshold_factor') else '1.0',
|
2351
|
+
'outline_sigma': str(self.outline_sigma) if hasattr(self, 'outline_sigma') else '1.0',
|
2352
|
+
'src': self.src,
|
2353
|
+
'db_path': self.db_path,
|
2341
2354
|
}
|
2342
2355
|
|
2343
2356
|
for key, data in vars_dict.items():
|
@@ -2354,7 +2367,10 @@ class AnnotateApp:
|
|
2354
2367
|
settings['percentiles'] = list(map(convert_to_number, settings['percentiles'].split(','))) if settings['percentiles'] else [1, 99]
|
2355
2368
|
settings['normalize'] = settings['normalize'].lower() == 'true'
|
2356
2369
|
settings['normalize_channels'] = settings['normalize_channels'].split(',') if settings['normalize_channels'] else None
|
2357
|
-
|
2370
|
+
settings['outline'] = settings['outline'].split(',') if settings['outline'] else None
|
2371
|
+
settings['outline_threshold_factor'] = float(settings['outline_threshold_factor'].replace(',', '.')) if settings['outline_threshold_factor'] else 1.0
|
2372
|
+
settings['outline_sigma'] = float(settings['outline_sigma'].replace(',', '.')) if settings['outline_sigma'] else 1.0
|
2373
|
+
|
2358
2374
|
try:
|
2359
2375
|
settings['measurement'] = settings['measurement'].split(',') if settings['measurement'] else None
|
2360
2376
|
settings['threshold'] = None if settings['threshold'].lower() == 'none' else int(settings['threshold'])
|
@@ -2379,7 +2395,12 @@ class AnnotateApp:
|
|
2379
2395
|
'percentiles': settings.get('percentiles'),
|
2380
2396
|
'measurement': settings.get('measurement'),
|
2381
2397
|
'threshold': settings.get('threshold'),
|
2382
|
-
'normalize_channels': settings.get('normalize_channels')
|
2398
|
+
'normalize_channels': settings.get('normalize_channels'),
|
2399
|
+
'outline': settings.get('outline'),
|
2400
|
+
'outline_threshold_factor': settings.get('outline_threshold_factor'),
|
2401
|
+
'outline_sigma': settings.get('outline_sigma'),
|
2402
|
+
'src': self.src,
|
2403
|
+
'db_path': self.db_path
|
2383
2404
|
})
|
2384
2405
|
|
2385
2406
|
settings_window.destroy()
|
@@ -2389,22 +2410,32 @@ class AnnotateApp:
|
|
2389
2410
|
|
2390
2411
|
def update_settings(self, **kwargs):
|
2391
2412
|
allowed_attributes = {
|
2392
|
-
'image_type', 'channels', 'image_size', 'annotation_column',
|
2393
|
-
'normalize', 'percentiles', 'measurement', 'threshold', 'normalize_channels'
|
2413
|
+
'image_type', 'channels', 'image_size', 'annotation_column', 'src', 'db_path',
|
2414
|
+
'normalize', 'percentiles', 'measurement', 'threshold', 'normalize_channels', 'outline', 'outline_threshold_factor', 'outline_sigma'
|
2394
2415
|
}
|
2395
2416
|
|
2396
2417
|
updated = False
|
2397
|
-
|
2418
|
+
|
2398
2419
|
for attr, value in kwargs.items():
|
2399
2420
|
if attr in allowed_attributes and value is not None:
|
2421
|
+
if attr == 'outline':
|
2422
|
+
if isinstance(value, str):
|
2423
|
+
value = [s.strip().lower() for s in value.split(',') if s.strip()]
|
2424
|
+
elif attr == 'outline_threshold_factor':
|
2425
|
+
value = float(value)
|
2426
|
+
elif attr == 'outline_sigma':
|
2427
|
+
value = float(value)
|
2400
2428
|
setattr(self, attr, value)
|
2401
2429
|
updated = True
|
2402
2430
|
|
2431
|
+
|
2403
2432
|
if 'image_size' in kwargs:
|
2404
2433
|
if isinstance(self.image_size, list):
|
2405
2434
|
self.image_size = (int(self.image_size[0]), int(self.image_size[0]))
|
2406
2435
|
elif isinstance(self.image_size, int):
|
2407
2436
|
self.image_size = (self.image_size, self.image_size)
|
2437
|
+
elif isinstance(self.image_size, tuple) and len(self.image_size) == 2:
|
2438
|
+
self.image_size = tuple(map(int, self.image_size))
|
2408
2439
|
else:
|
2409
2440
|
raise ValueError("Invalid image size")
|
2410
2441
|
|
@@ -2599,9 +2630,47 @@ class AnnotateApp:
|
|
2599
2630
|
img = self.normalize_image(img, self.normalize, self.percentiles, self.normalize_channels)
|
2600
2631
|
img = img.convert('RGB')
|
2601
2632
|
img = self.filter_channels(img)
|
2633
|
+
|
2634
|
+
if self.outline:
|
2635
|
+
img = self.outline_image(img, self.outline_sigma)
|
2636
|
+
|
2602
2637
|
img = img.resize(self.image_size)
|
2603
2638
|
return img, annotation
|
2604
2639
|
|
2640
|
+
def outline_image(self, img, edge_sigma=1, edge_thickness=1):
|
2641
|
+
"""
|
2642
|
+
For each selected channel, compute a continuous outline from the intensity landscape
|
2643
|
+
using Otsu threshold scaled by a correction factor. Replace only that channel.
|
2644
|
+
"""
|
2645
|
+
arr = np.asarray(img)
|
2646
|
+
if arr.ndim != 3 or arr.shape[2] != 3:
|
2647
|
+
return img # not RGB
|
2648
|
+
|
2649
|
+
out_img = arr.copy()
|
2650
|
+
channel_map = {'r': 0, 'g': 1, 'b': 2}
|
2651
|
+
factor = getattr(self, 'outline_threshold_factor', 1.0)
|
2652
|
+
|
2653
|
+
for ch in self.outline:
|
2654
|
+
if ch not in channel_map:
|
2655
|
+
continue
|
2656
|
+
idx = channel_map[ch]
|
2657
|
+
channel_data = arr[:, :, idx]
|
2658
|
+
|
2659
|
+
try:
|
2660
|
+
channel_data = gaussian_filter(channel_data, sigma=edge_sigma)
|
2661
|
+
otsu_thresh = threshold_otsu(channel_data)
|
2662
|
+
corrected_thresh = min(255, otsu_thresh * factor)
|
2663
|
+
fg_mask = channel_data > corrected_thresh
|
2664
|
+
except Exception:
|
2665
|
+
continue
|
2666
|
+
|
2667
|
+
edge = find_boundaries(fg_mask, mode='inner')
|
2668
|
+
thick_edge = dilation(edge, disk(edge_thickness))
|
2669
|
+
|
2670
|
+
out_img[:, :, idx] = (thick_edge * 255).astype(np.uint8)
|
2671
|
+
|
2672
|
+
return Image.fromarray(out_img)
|
2673
|
+
|
2605
2674
|
@staticmethod
|
2606
2675
|
def normalize_image(img, normalize=False, percentiles=(1, 99), normalize_channels=None):
|
2607
2676
|
"""
|
spacr/gui_utils.py
CHANGED
@@ -252,7 +252,7 @@ def annotate(settings):
|
|
252
252
|
app.load_images()
|
253
253
|
root.mainloop()
|
254
254
|
|
255
|
-
def
|
255
|
+
def generate_annotate_fields_v1(frame):
|
256
256
|
from .settings import set_annotate_default_settings
|
257
257
|
from .gui_elements import set_dark_style
|
258
258
|
|
@@ -281,6 +281,48 @@ def generate_annotate_fields(frame):
|
|
281
281
|
|
282
282
|
return vars_dict
|
283
283
|
|
284
|
+
def generate_annotate_fields(frame):
|
285
|
+
from .settings import set_annotate_default_settings
|
286
|
+
from .gui_elements import set_dark_style
|
287
|
+
|
288
|
+
style_out = set_dark_style(ttk.Style())
|
289
|
+
font_loader = style_out['font_loader']
|
290
|
+
font_size = style_out['font_size'] - 2
|
291
|
+
|
292
|
+
vars_dict = {}
|
293
|
+
settings = set_annotate_default_settings(settings={})
|
294
|
+
|
295
|
+
for setting in settings:
|
296
|
+
vars_dict[setting] = {
|
297
|
+
'entry': ttk.Entry(frame),
|
298
|
+
'value': settings[setting]
|
299
|
+
}
|
300
|
+
|
301
|
+
# Arrange input fields and labels
|
302
|
+
for row, (name, data) in enumerate(vars_dict.items()):
|
303
|
+
tk.Label(
|
304
|
+
frame,
|
305
|
+
text=f"{name.replace('_', ' ').capitalize()}:",
|
306
|
+
bg=style_out['bg_color'],
|
307
|
+
fg=style_out['fg_color'],
|
308
|
+
font=font_loader.get_font(size=font_size)
|
309
|
+
).grid(row=row, column=0)
|
310
|
+
|
311
|
+
value = data['value']
|
312
|
+
if isinstance(value, list):
|
313
|
+
string_value = ','.join(map(str, value))
|
314
|
+
elif isinstance(value, (int, float, bool)):
|
315
|
+
string_value = str(value)
|
316
|
+
elif value is None:
|
317
|
+
string_value = ''
|
318
|
+
else:
|
319
|
+
string_value = value
|
320
|
+
|
321
|
+
data['entry'].insert(0, string_value)
|
322
|
+
data['entry'].grid(row=row, column=1)
|
323
|
+
|
324
|
+
return vars_dict
|
325
|
+
|
284
326
|
def run_annotate_app(vars_dict, parent_frame):
|
285
327
|
settings = {key: data['entry'].get() for key, data in vars_dict.items()}
|
286
328
|
settings['channels'] = settings['channels'].split(',')
|
@@ -349,7 +391,7 @@ def annotate_with_image_refs(settings, root, shutdown_callback):
|
|
349
391
|
screen_height = root.winfo_screenheight()
|
350
392
|
root.geometry(f"{screen_width}x{screen_height}")
|
351
393
|
|
352
|
-
app = AnnotateApp(root, db, src, image_type=settings['image_type'], channels=settings['channels'], image_size=settings['img_size'], annotation_column=settings['annotation_column'], normalize=settings['normalize'], percentiles=settings['percentiles'], measurement=settings['measurement'], threshold=settings['threshold'], normalize_channels=settings['normalize_channels'])
|
394
|
+
app = AnnotateApp(root, db, src, image_type=settings['image_type'], channels=settings['channels'], image_size=settings['img_size'], annotation_column=settings['annotation_column'], normalize=settings['normalize'], percentiles=settings['percentiles'], measurement=settings['measurement'], threshold=settings['threshold'], normalize_channels=settings['normalize_channels'], outline=settings['outline'], outline_threshold_factor=settings['outline_threshold_factor'], outline_sigma=settings['outline_sigma'])
|
353
395
|
|
354
396
|
# Set the canvas background to black
|
355
397
|
root.configure(bg='black')
|
spacr/measure.py
CHANGED
@@ -2,12 +2,11 @@ import os, cv2, time, sqlite3, traceback, shutil
|
|
2
2
|
import numpy as np
|
3
3
|
import pandas as pd
|
4
4
|
from collections import defaultdict
|
5
|
-
from scipy.stats import pearsonr
|
5
|
+
from scipy.stats import pearsonr, skew, kurtosis, mode
|
6
6
|
import multiprocessing as mp
|
7
|
-
from scipy.ndimage import distance_transform_edt, generate_binary_structure
|
7
|
+
from scipy.ndimage import distance_transform_edt, generate_binary_structure, binary_dilation, gaussian_filter, center_of_mass
|
8
8
|
from skimage.measure import regionprops, regionprops_table, shannon_entropy
|
9
9
|
from skimage.exposure import rescale_intensity
|
10
|
-
from scipy.ndimage import binary_dilation
|
11
10
|
from skimage.segmentation import find_boundaries
|
12
11
|
from skimage.feature import graycomatrix, graycoprops
|
13
12
|
from mahotas.features import zernike_moments
|
@@ -16,7 +15,6 @@ from skimage.util import img_as_bool
|
|
16
15
|
import matplotlib.pyplot as plt
|
17
16
|
from math import ceil, sqrt
|
18
17
|
|
19
|
-
|
20
18
|
def get_components(cell_mask, nucleus_mask, pathogen_mask):
|
21
19
|
"""
|
22
20
|
Get the components (nucleus and pathogens) for each cell in the given masks.
|
@@ -251,25 +249,95 @@ def _create_dataframe(radial_distributions, object_type):
|
|
251
249
|
|
252
250
|
def _extended_regionprops_table(labels, image, intensity_props):
|
253
251
|
"""
|
254
|
-
Calculate extended region properties table.
|
255
|
-
|
256
|
-
Args:
|
257
|
-
labels (ndarray): Labeled image.
|
258
|
-
image (ndarray): Input image.
|
259
|
-
intensity_props (list): List of intensity properties to calculate.
|
260
|
-
|
261
|
-
Returns:
|
262
|
-
DataFrame: Extended region properties table.
|
263
|
-
|
252
|
+
Calculate extended region properties table, adding a suite of advanced quantitative features.
|
264
253
|
"""
|
265
|
-
|
254
|
+
|
255
|
+
def _gini(array):
|
256
|
+
# Compute Gini coefficient (nan safe)
|
257
|
+
array = np.abs(array[~np.isnan(array)])
|
258
|
+
n = array.size
|
259
|
+
if n == 0:
|
260
|
+
return np.nan
|
261
|
+
array = np.sort(array)
|
262
|
+
index = np.arange(1, n + 1)
|
263
|
+
return (np.sum((2 * index - n - 1) * array)) / (n * np.sum(array)) if np.sum(array) else np.nan
|
264
|
+
|
266
265
|
props = regionprops_table(labels, image, properties=intensity_props)
|
267
|
-
|
266
|
+
df = pd.DataFrame(props)
|
267
|
+
|
268
|
+
regions = regionprops(labels, intensity_image=image)
|
269
|
+
integrated_intensity = []
|
270
|
+
std_intensity = []
|
271
|
+
median_intensity = []
|
272
|
+
skew_intensity = []
|
273
|
+
kurtosis_intensity = []
|
274
|
+
mode_intensity = []
|
275
|
+
range_intensity = []
|
276
|
+
iqr_intensity = []
|
277
|
+
cv_intensity = []
|
278
|
+
gini_intensity = []
|
279
|
+
frac_high90 = []
|
280
|
+
frac_low10 = []
|
281
|
+
entropy_intensity = []
|
282
|
+
|
283
|
+
for region in regions:
|
284
|
+
intens = region.intensity_image[region.image]
|
285
|
+
intens = intens[~np.isnan(intens)]
|
286
|
+
if intens.size == 0:
|
287
|
+
integrated_intensity.append(np.nan)
|
288
|
+
std_intensity.append(np.nan)
|
289
|
+
median_intensity.append(np.nan)
|
290
|
+
skew_intensity.append(np.nan)
|
291
|
+
kurtosis_intensity.append(np.nan)
|
292
|
+
mode_intensity.append(np.nan)
|
293
|
+
range_intensity.append(np.nan)
|
294
|
+
iqr_intensity.append(np.nan)
|
295
|
+
cv_intensity.append(np.nan)
|
296
|
+
gini_intensity.append(np.nan)
|
297
|
+
frac_high90.append(np.nan)
|
298
|
+
frac_low10.append(np.nan)
|
299
|
+
entropy_intensity.append(np.nan)
|
300
|
+
else:
|
301
|
+
integrated_intensity.append(np.sum(intens))
|
302
|
+
std_intensity.append(np.std(intens))
|
303
|
+
median_intensity.append(np.median(intens))
|
304
|
+
skew_intensity.append(skew(intens) if intens.size > 2 else np.nan)
|
305
|
+
kurtosis_intensity.append(kurtosis(intens) if intens.size > 3 else np.nan)
|
306
|
+
# Mode (use first mode value if multimodal)
|
307
|
+
try:
|
308
|
+
mode_val = mode(intens, nan_policy='omit').mode
|
309
|
+
mode_intensity.append(mode_val[0] if len(mode_val) > 0 else np.nan)
|
310
|
+
except Exception:
|
311
|
+
mode_intensity.append(np.nan)
|
312
|
+
range_intensity.append(np.ptp(intens))
|
313
|
+
iqr_intensity.append(np.percentile(intens, 75) - np.percentile(intens, 25))
|
314
|
+
cv_intensity.append(np.std(intens) / np.mean(intens) if np.mean(intens) != 0 else np.nan)
|
315
|
+
gini_intensity.append(_gini(intens))
|
316
|
+
frac_high90.append(np.mean(intens > np.percentile(intens, 90)))
|
317
|
+
frac_low10.append(np.mean(intens < np.percentile(intens, 10)))
|
318
|
+
entropy_intensity.append(shannon_entropy(intens) if intens.size > 1 else 0.0)
|
319
|
+
|
320
|
+
df['integrated_intensity'] = integrated_intensity
|
321
|
+
df['std_intensity'] = std_intensity
|
322
|
+
df['median_intensity'] = median_intensity
|
323
|
+
df['skew_intensity'] = skew_intensity
|
324
|
+
df['kurtosis_intensity'] = kurtosis_intensity
|
325
|
+
df['mode_intensity'] = mode_intensity
|
326
|
+
df['range_intensity'] = range_intensity
|
327
|
+
df['iqr_intensity'] = iqr_intensity
|
328
|
+
df['cv_intensity'] = cv_intensity
|
329
|
+
df['gini_intensity'] = gini_intensity
|
330
|
+
df['frac_high90'] = frac_high90
|
331
|
+
df['frac_low10'] = frac_low10
|
332
|
+
df['entropy_intensity'] = entropy_intensity
|
333
|
+
|
334
|
+
percentiles = [5, 10, 25, 75, 85, 95]
|
268
335
|
for p in percentiles:
|
269
|
-
|
270
|
-
np.percentile(region.intensity_image
|
271
|
-
for region in regions
|
272
|
-
|
336
|
+
df[f'percentile_{p}'] = [
|
337
|
+
np.percentile(region.intensity_image[region.image], p)
|
338
|
+
for region in regions
|
339
|
+
]
|
340
|
+
return df
|
273
341
|
|
274
342
|
def _calculate_homogeneity(label, channel, distances=[2,4,8,16,32,64]):
|
275
343
|
"""
|
@@ -495,8 +563,75 @@ def _estimate_blur(image):
|
|
495
563
|
# Compute and return the variance of the Laplacian
|
496
564
|
return lap.var()
|
497
565
|
|
566
|
+
def _measure_intensity_distance(cell_mask, nucleus_mask, pathogen_mask, channel_arrays, settings):
|
567
|
+
"""
|
568
|
+
Compute Gaussian-smoothed intensity-weighted centroid distances for each cell object.
|
569
|
+
"""
|
570
|
+
|
571
|
+
sigma = settings.get('distance_gaussian_sigma', 1.0)
|
572
|
+
cell_labels = np.unique(cell_mask)
|
573
|
+
cell_labels = cell_labels[cell_labels > 0]
|
574
|
+
|
575
|
+
dfs = []
|
576
|
+
nucleus_dt = distance_transform_edt(nucleus_mask == 0)
|
577
|
+
pathogen_dt = distance_transform_edt(pathogen_mask == 0)
|
578
|
+
|
579
|
+
for ch in range(channel_arrays.shape[-1]):
|
580
|
+
channel_img = channel_arrays[:, :, ch]
|
581
|
+
blurred_img = gaussian_filter(channel_img, sigma=sigma)
|
582
|
+
|
583
|
+
data = []
|
584
|
+
for label in cell_labels:
|
585
|
+
cell_coords = np.argwhere(cell_mask == label)
|
586
|
+
if cell_coords.size == 0:
|
587
|
+
data.append([label, np.nan, np.nan])
|
588
|
+
continue
|
589
|
+
|
590
|
+
minr, minc = np.min(cell_coords, axis=0)
|
591
|
+
maxr, maxc = np.max(cell_coords, axis=0) + 1
|
592
|
+
|
593
|
+
cell_submask = (cell_mask[minr:maxr, minc:maxc] == label)
|
594
|
+
blurred_subimg = blurred_img[minr:maxr, minc:maxc]
|
595
|
+
|
596
|
+
if np.sum(cell_submask) == 0:
|
597
|
+
data.append([label, np.nan, np.nan])
|
598
|
+
continue
|
599
|
+
|
600
|
+
masked_intensity = blurred_subimg * cell_submask
|
601
|
+
com_local = center_of_mass(masked_intensity)
|
602
|
+
if np.isnan(com_local[0]):
|
603
|
+
data.append([label, np.nan, np.nan])
|
604
|
+
continue
|
605
|
+
|
606
|
+
com_global = (com_local[0] + minr, com_local[1] + minc)
|
607
|
+
com_global_int = tuple(np.round(com_global).astype(int))
|
608
|
+
|
609
|
+
x, y = com_global_int
|
610
|
+
if not (0 <= x < cell_mask.shape[0] and 0 <= y < cell_mask.shape[1]):
|
611
|
+
data.append([label, np.nan, np.nan])
|
612
|
+
continue
|
613
|
+
|
614
|
+
nucleus_dist = nucleus_dt[x, y]
|
615
|
+
pathogen_dist = pathogen_dt[x, y]
|
616
|
+
|
617
|
+
data.append([label, nucleus_dist, pathogen_dist])
|
618
|
+
|
619
|
+
df = pd.DataFrame(data, columns=['label',
|
620
|
+
f'cell_channel_{ch}_distance_to_nucleus',
|
621
|
+
f'cell_channel_{ch}_distance_to_pathogen'])
|
622
|
+
dfs.append(df)
|
623
|
+
|
624
|
+
# Merge all channel dataframes on label
|
625
|
+
merged_df = dfs[0]
|
626
|
+
for df in dfs[1:]:
|
627
|
+
merged_df = merged_df.merge(df, on='label', how='outer')
|
628
|
+
|
629
|
+
return merged_df
|
630
|
+
|
631
|
+
|
498
632
|
#@log_function_call
|
499
633
|
def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, channel_arrays, settings, sizes=[3, 6, 12, 24], periphery=True, outside=True):
|
634
|
+
|
500
635
|
"""
|
501
636
|
Calculate various intensity measurements for different regions in the image.
|
502
637
|
|
@@ -536,8 +671,8 @@ def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_ma
|
|
536
671
|
df.append(empty_df)
|
537
672
|
continue
|
538
673
|
|
539
|
-
mask_intensity_df = _extended_regionprops_table(label, channel, intensity_props)
|
540
|
-
mask_intensity_df['shannon_entropy'] = shannon_entropy(channel, base=2)
|
674
|
+
mask_intensity_df = _extended_regionprops_table(label, channel, intensity_props)
|
675
|
+
#mask_intensity_df['shannon_entropy'] = shannon_entropy(channel, base=2)
|
541
676
|
|
542
677
|
if homogeneity:
|
543
678
|
homogeneity_df = _calculate_homogeneity(label, channel, distances)
|
@@ -558,6 +693,13 @@ def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_ma
|
|
558
693
|
|
559
694
|
mask_intensity_df.columns = [f'{ls[j]}_channel_{i}_{col}' if col != 'label' else col for col in mask_intensity_df.columns]
|
560
695
|
df.append(mask_intensity_df)
|
696
|
+
|
697
|
+
if isinstance(settings['distance_gaussian_sigma'], int):
|
698
|
+
if settings['distance_gaussian_sigma'] != 0:
|
699
|
+
if settings['cell_mask_dim'] != None:
|
700
|
+
if settings['nucleus_mask_dim'] != None or settings['pathogen_mask_dim'] != None:
|
701
|
+
intensity_distance_df = _measure_intensity_distance(cell_mask, nucleus_mask, pathogen_mask, channel_arrays, settings)
|
702
|
+
cell_dfs.append(intensity_distance_df)
|
561
703
|
|
562
704
|
if radial_dist:
|
563
705
|
if np.max(nucleus_mask) != 0:
|
@@ -565,7 +707,7 @@ def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_ma
|
|
565
707
|
nucleus_df = _create_dataframe(nucleus_radial_distributions, 'nucleus')
|
566
708
|
dfs[1].append(nucleus_df)
|
567
709
|
|
568
|
-
if np.max(
|
710
|
+
if np.max(pathogen_mask) != 0:
|
569
711
|
pathogen_radial_distributions = _calculate_radial_distribution(cell_mask, pathogen_mask, channel_arrays, num_bins=6)
|
570
712
|
pathogen_df = _create_dataframe(pathogen_radial_distributions, 'pathogen')
|
571
713
|
dfs[2].append(pathogen_df)
|
@@ -785,6 +927,7 @@ def _measure_crop_core(index, time_ls, file, settings):
|
|
785
927
|
#merge skeleton_df with cell_df here
|
786
928
|
|
787
929
|
cell_intensity_df, nucleus_intensity_df, pathogen_intensity_df, cytoplasm_intensity_df = _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, channel_arrays, settings, sizes=[1, 2, 3, 4, 5], periphery=True, outside=True)
|
930
|
+
|
788
931
|
if settings['cell_mask_dim'] is not None:
|
789
932
|
cell_merged_df = _merge_and_save_to_database(cell_df, cell_intensity_df, 'cell', source_folder, file_name, settings['experiment'], settings['timelapse'])
|
790
933
|
if settings['nucleus_mask_dim'] is not None:
|
spacr/plot.py
CHANGED
@@ -1154,7 +1154,7 @@ def _plot_cropped_arrays(stack, filename, figuresize=10, cmap='inferno', thresho
|
|
1154
1154
|
for channel in range(num_channels):
|
1155
1155
|
plot_single_array(stack[:, :, channel], axs[channel], f'C. {channel}', plt.get_cmap(cmap))
|
1156
1156
|
fig.tight_layout()
|
1157
|
-
print(f'{filename}')
|
1157
|
+
#print(f'{filename}')
|
1158
1158
|
return fig
|
1159
1159
|
|
1160
1160
|
def _visualize_and_save_timelapse_stack_with_tracks(masks, tracks_df, save, src, name, plot, filenames, object_type, mode='btrack', interactive=False):
|
Binary file
|
Binary file
|
spacr/settings.py
CHANGED
@@ -313,7 +313,9 @@ def get_measure_crop_settings(settings={}):
|
|
313
313
|
settings.setdefault('pathogen_min_size',0)
|
314
314
|
settings.setdefault('cytoplasm_min_size',0)
|
315
315
|
settings.setdefault('merge_edge_pathogen_cells', True)
|
316
|
-
|
316
|
+
|
317
|
+
settings.setdefault('distance_gaussian_sigma', 10)
|
318
|
+
|
317
319
|
if settings['test_mode']:
|
318
320
|
settings['verbose'] = True
|
319
321
|
settings['plot'] = True
|
@@ -1000,7 +1002,8 @@ expected_types = {
|
|
1000
1002
|
"cell_diamiter":int,
|
1001
1003
|
"nucleus_diamiter":int,
|
1002
1004
|
"pathogen_diamiter":int,
|
1003
|
-
"consolidate":bool
|
1005
|
+
"consolidate":bool,
|
1006
|
+
"distance_gaussian_sigma": (int, type(None))
|
1004
1007
|
}
|
1005
1008
|
|
1006
1009
|
categories = {"Paths":[ "src", "grna", "barcodes", "custom_model_path", "dataset","model_path","grna_csv","row_csv","column_csv", "metadata_files", "score_data","count_data"],
|
@@ -1022,7 +1025,7 @@ categories = {"Paths":[ "src", "grna", "barcodes", "custom_model_path", "dataset
|
|
1022
1025
|
"Plot": ["split_axis_lims", "x_lim","log_x","log_y", "plot_control", "plot_nr", "examples_to_plot", "normalize_plots", "cmap", "figuresize", "plot_cluster_grids", "img_zoom", "row_limit", "color_by", "plot_images", "smooth_lines", "plot_points", "plot_outlines", "black_background", "plot_by_cluster", "heatmap_feature","grouping","min_max","cmap","save_figure"],
|
1023
1026
|
"Timelapse": ["timelapse", "fps", "timelapse_displacement", "timelapse_memory", "timelapse_frame_limits", "timelapse_remove_transient", "timelapse_mode", "timelapse_objects", "compartments"],
|
1024
1027
|
"Advanced": ["merge_edge_pathogen_cells", "test_images", "random_test", "test_nr", "test", "test_split", "normalize", "target_unique_count","threshold_multiplier", "threshold_method", "min_n","shuffle", "target_intensity_min", "cells_per_well", "nuclei_limit", "pathogen_limit", "background", "backgrounds", "schedule", "test_size","exclude","n_repeats","top_features", "model_type_ml", "model_type","minimum_cell_count","n_estimators","preprocess", "remove_background", "normalize", "lower_percentile", "merge_pathogens", "batch_size", "filter", "save", "masks", "verbose", "randomize", "n_jobs"],
|
1025
|
-
"Beta": ["all_to_mip", "upscale", "upscale_factor", "consolidate"]
|
1028
|
+
"Beta": ["all_to_mip", "upscale", "upscale_factor", "consolidate", "distance_gaussian_sigma"]
|
1026
1029
|
}
|
1027
1030
|
|
1028
1031
|
|
@@ -1464,6 +1467,9 @@ def set_annotate_default_settings(settings):
|
|
1464
1467
|
settings.setdefault('annotation_column', 'test')
|
1465
1468
|
settings.setdefault('normalize', 'False')
|
1466
1469
|
settings.setdefault('normalize_channels', "r,g,b")
|
1470
|
+
settings.setdefault('outline', None)
|
1471
|
+
settings.setdefault('outline_threshold_factor', 1)
|
1472
|
+
settings.setdefault('outline_sigma', 1)
|
1467
1473
|
settings.setdefault('percentiles', [2, 98])
|
1468
1474
|
settings.setdefault('measurement', '') #'cytoplasm_channel_3_mean_intensity,pathogen_channel_3_mean_intensity')
|
1469
1475
|
settings.setdefault('threshold', '') #'2')
|
spacr/utils.py
CHANGED
@@ -4602,7 +4602,10 @@ def adjust_cell_masks(parasite_folder, cell_folder, nuclei_folder, overlap_thres
|
|
4602
4602
|
raise ValueError("The number of files in the folders do not match.")
|
4603
4603
|
|
4604
4604
|
# Match files by name
|
4605
|
-
|
4605
|
+
time_ls = []
|
4606
|
+
files_to_process = len(parasite_files)
|
4607
|
+
for files_processed, file_name in enumerate(parasite_files):
|
4608
|
+
start = time.time()
|
4606
4609
|
parasite_path = os.path.join(parasite_folder, file_name)
|
4607
4610
|
cell_path = os.path.join(cell_folder, file_name)
|
4608
4611
|
nuclei_path = os.path.join(nuclei_folder, file_name)
|
@@ -4621,6 +4624,12 @@ def adjust_cell_masks(parasite_folder, cell_folder, nuclei_folder, overlap_thres
|
|
4621
4624
|
|
4622
4625
|
# Overwrite the original cell mask file with the merged result
|
4623
4626
|
np.save(cell_path, merged_cell_mask)
|
4627
|
+
|
4628
|
+
stop = time.time()
|
4629
|
+
duration = (stop - start)
|
4630
|
+
time_ls.append(duration)
|
4631
|
+
files_processed += 1
|
4632
|
+
print_progress(files_processed, files_to_process, n_jobs=1, time_ls=time_ls, batch_size=None, operation_type=f'adjust_cell_masks')
|
4624
4633
|
|
4625
4634
|
def process_masks(mask_folder, image_folder, channel, batch_size=50, n_clusters=2, plot=False):
|
4626
4635
|
|
@@ -0,0 +1,207 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: spacr
|
3
|
+
Version: 0.9.3
|
4
|
+
Summary: Spatial phenotype analysis of crisp screens (SpaCr)
|
5
|
+
Home-page: https://github.com/EinarOlafsson/spacr
|
6
|
+
Author: Einar Birnir Olafsson
|
7
|
+
Author-email: olafsson@med.umich.com
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Description-Content-Type: text/x-rst
|
12
|
+
License-File: LICENSE
|
13
|
+
Requires-Dist: numpy <2.0,>=1.26.4
|
14
|
+
Requires-Dist: pandas <3.0,>=2.2.1
|
15
|
+
Requires-Dist: scipy <2.0,>=1.12.0
|
16
|
+
Requires-Dist: cellpose <4.0,>=3.0.6
|
17
|
+
Requires-Dist: scikit-image <1.0,>=0.22.0
|
18
|
+
Requires-Dist: scikit-learn <2.0,>=1.4.1
|
19
|
+
Requires-Dist: scikit-posthocs <0.20,>=0.10.0
|
20
|
+
Requires-Dist: mahotas <2.0,>=1.4.13
|
21
|
+
Requires-Dist: btrack <1.0,>=0.6.5
|
22
|
+
Requires-Dist: trackpy <1.0,>=0.6.2
|
23
|
+
Requires-Dist: statsmodels <1.0,>=0.14.1
|
24
|
+
Requires-Dist: shap <1.0,>=0.45.0
|
25
|
+
Requires-Dist: torch <3.0,>=2.0
|
26
|
+
Requires-Dist: torchvision <1.0,>=0.1
|
27
|
+
Requires-Dist: torch-geometric <3.0,>=2.5
|
28
|
+
Requires-Dist: torchcam <1.0,>=0.4.0
|
29
|
+
Requires-Dist: transformers <5.0,>=4.45.2
|
30
|
+
Requires-Dist: segmentation-models-pytorch >=0.3.3
|
31
|
+
Requires-Dist: monai >=1.3.0
|
32
|
+
Requires-Dist: captum <1.0,>=0.7.0
|
33
|
+
Requires-Dist: seaborn <1.0,>=0.13.2
|
34
|
+
Requires-Dist: matplotlib <4.0,>=3.8.3
|
35
|
+
Requires-Dist: matplotlib-venn <2.0,>=1.1
|
36
|
+
Requires-Dist: adjustText <2.0,>=1.2.0
|
37
|
+
Requires-Dist: bottleneck <2.0,>=1.3.6
|
38
|
+
Requires-Dist: numexpr <3.0,>=2.8.4
|
39
|
+
Requires-Dist: opencv-python-headless <5.0,>=4.9.0.80
|
40
|
+
Requires-Dist: pillow <11.0,>=10.2.0
|
41
|
+
Requires-Dist: tifffile >=2023.4.12
|
42
|
+
Requires-Dist: nd2reader <4.0,>=3.3.0
|
43
|
+
Requires-Dist: czifile
|
44
|
+
Requires-Dist: pylibCZIrw <6.0,>=5.0.0
|
45
|
+
Requires-Dist: aicspylibczi
|
46
|
+
Requires-Dist: readlif
|
47
|
+
Requires-Dist: imageio <3.0,>=2.34.0
|
48
|
+
Requires-Dist: pingouin <1.0,>=0.5.5
|
49
|
+
Requires-Dist: umap-learn <1.0,>=0.5.6
|
50
|
+
Requires-Dist: ttkthemes <4.0,>=3.2.2
|
51
|
+
Requires-Dist: xgboost <3.0,>=2.0.3
|
52
|
+
Requires-Dist: PyWavelets <2.0,>=1.6.0
|
53
|
+
Requires-Dist: ttf-opensans >=2020.10.30
|
54
|
+
Requires-Dist: customtkinter <6.0,>=5.2.2
|
55
|
+
Requires-Dist: biopython <2.0,>=1.80
|
56
|
+
Requires-Dist: lxml <6.0,>=5.1.0
|
57
|
+
Requires-Dist: psutil <6.0,>=5.9.8
|
58
|
+
Requires-Dist: gputil <2.0,>=1.4.0
|
59
|
+
Requires-Dist: gpustat <2.0,>=1.1.1
|
60
|
+
Requires-Dist: pyautogui <1.0,>=0.9.54
|
61
|
+
Requires-Dist: tables <4.0,>=3.8.0
|
62
|
+
Requires-Dist: rapidfuzz <4.0,>=3.9
|
63
|
+
Requires-Dist: keyring <16.0,>=15.1
|
64
|
+
Requires-Dist: screeninfo <1.0,>=0.8.1
|
65
|
+
Requires-Dist: fastremap >=1.14.1
|
66
|
+
Requires-Dist: pytz >=2023.3.post1
|
67
|
+
Requires-Dist: tqdm >=4.65.0
|
68
|
+
Requires-Dist: wandb >=0.16.2
|
69
|
+
Requires-Dist: openai <2.0,>=1.50.2
|
70
|
+
Requires-Dist: gdown
|
71
|
+
Requires-Dist: IPython <9.0,>=8.18.1
|
72
|
+
Requires-Dist: ipykernel
|
73
|
+
Requires-Dist: ipywidgets <9.0,>=8.1.2
|
74
|
+
Requires-Dist: brokenaxes <1.0,>=0.6.2
|
75
|
+
Requires-Dist: huggingface-hub <0.25,>=0.24.0
|
76
|
+
Provides-Extra: dev
|
77
|
+
Requires-Dist: pytest <3.11,>=3.9 ; extra == 'dev'
|
78
|
+
Provides-Extra: full
|
79
|
+
Requires-Dist: opencv-python ; extra == 'full'
|
80
|
+
Provides-Extra: headless
|
81
|
+
Requires-Dist: opencv-python-headless ; extra == 'headless'
|
82
|
+
|
83
|
+
.. |Docs| image:: https://github.com/EinarOlafsson/spacr/actions/workflows/pages/pages-build-deployment/badge.svg
|
84
|
+
:target: https://einarolafsson.github.io/spacr/index.html
|
85
|
+
.. |PyPI version| image:: https://badge.fury.io/py/spacr.svg
|
86
|
+
:target: https://badge.fury.io/py/spacr
|
87
|
+
.. |Python version| image:: https://img.shields.io/pypi/pyversions/spacr
|
88
|
+
:target: https://pypistats.org/packages/spacr
|
89
|
+
.. |Licence: MIT| image:: https://img.shields.io/github/license/EinarOlafsson/spacr
|
90
|
+
:target: https://github.com/EinarOlafsson/spacr/blob/main/LICENSE
|
91
|
+
.. |repo size| image:: https://img.shields.io/github/repo-size/EinarOlafsson/spacr
|
92
|
+
:target: https://github.com/EinarOlafsson/spacr/
|
93
|
+
|
94
|
+
.. _docs: https://einarolafsson.github.io/spacr/index.html
|
95
|
+
|
96
|
+
Badges
|
97
|
+
------
|
98
|
+
|Docs| |PyPI version| |Python version| |Licence: MIT| |repo size|
|
99
|
+
|
100
|
+
SpaCr
|
101
|
+
=====
|
102
|
+
|
103
|
+
**Spatial phenotype analysis of CRISPR-Cas9 screens (SpaCr).**
|
104
|
+
|
105
|
+
The spatial organization of organelles and proteins within cells constitutes a key level of functional regulation. In the context of infectious disease, the spatial relationships between host cell structures and intracellular pathogens are critical to understanding host clearance mechanisms and how pathogens evade them. SpaCr is a Python-based software package for generating single-cell image data for deep-learning sub-cellular/cellular phenotypic classification from pooled genetic CRISPR-Cas9 screens. SpaCr provides a flexible toolset to extract single-cell images and measurements from high-content cell painting experiments, train deep-learning models to classify cellular/subcellular phenotypes, simulate, and analyze pooled CRISPR-Cas9 imaging screens.
|
106
|
+
|
107
|
+
Features
|
108
|
+
--------
|
109
|
+
|
110
|
+
- **Generate Masks:** Generate cellpose masks of cell, nuclei, and pathogen objects.
|
111
|
+
- **Object Measurements:** Measurements for each object including scikit-image regionprops, intensity percentiles, shannon-entropy, Pearson’s and Manders’ correlations, homogeneity, and radial distribution. Measurements are saved to a SQL database in object-level tables.
|
112
|
+
- **Crop Images:** Save objects (cells, nuclei, pathogen, cytoplasm) as images. Object image paths are saved in a SQL database.
|
113
|
+
- **Train CNNs or Transformers:** Train Torch models to classify single object images.
|
114
|
+
- **Manual Annotation:** Supports manual annotation of single-cell images and segmentation to refine training datasets for training CNNs/Transformers or cellpose, respectively.
|
115
|
+
- **Finetune Cellpose Models:** Adjust pre-existing Cellpose models to your specific dataset for improved performance.
|
116
|
+
- **Timelapse Data Support:** Track objects in timelapse image data.
|
117
|
+
- **Simulations:** Simulate spatial phenotype screens.
|
118
|
+
- **Sequencing:** Map FASTQ reads to barcode and gRNA barcode metadata.
|
119
|
+
- **Misc:** Analyze Ca oscillation, recruitment, infection rate, plaque size/count.
|
120
|
+
|
121
|
+
.. image:: https://github.com/EinarOlafsson/spacr/raw/main/spacr/resources/icons/flow_chart_v3.png
|
122
|
+
:alt: SpaCr workflow
|
123
|
+
:align: center
|
124
|
+
|
125
|
+
|
126
|
+
**Overview and data organization of spaCR.**
|
127
|
+
|
128
|
+
**a.** Schematic workflow of the spaCR pipeline for pooled image-based CRISPR screens. Microscopy images (TIFF, LIF, CZI, NDI) and sequencing reads (FASTQ) are used as inputs (black). The main modules (teal) are: (1) Mask: generates object masks for cells, nuclei, pathogens, and cytoplasm; (2) Measure: extracts object-level features and crops object images, storing quantitative data in an SQL database; (3) Classify—applies machine learning (ML, e.g., XGBoost) or deep learning (DL, e.g., PyTorch) models to classify objects, summarizing results as well-level classification scores; (4) Map Barcodes: extracts and maps row, column, and gRNA barcodes from sequencing data to corresponding wells; (5) Regression: estimates gRNA effect sizes and gene scores via multiple linear regression using well-level summary statistics.
|
129
|
+
**b.** Downstream submodules available for extended analyses at each stage.
|
130
|
+
**c.** Output folder structure for each module, including locations for raw and processed images, masks, object-level measurements, datasets, and results.
|
131
|
+
**d.** List of all spaCR package modules.
|
132
|
+
|
133
|
+
Installation
|
134
|
+
------------
|
135
|
+
|
136
|
+
**Linux recommended.**
|
137
|
+
If using Windows, switch to Linux—it's free, open-source, and better.
|
138
|
+
|
139
|
+
**macOS prerequisites (before install):**
|
140
|
+
|
141
|
+
::
|
142
|
+
|
143
|
+
brew install libomp
|
144
|
+
brew install hdf5
|
145
|
+
|
146
|
+
**Linux GUI requirement:**
|
147
|
+
SpaCr GUI requires Tkinter.
|
148
|
+
|
149
|
+
::
|
150
|
+
|
151
|
+
sudo apt-get install python3-tk
|
152
|
+
|
153
|
+
**Installation:**
|
154
|
+
|
155
|
+
::
|
156
|
+
|
157
|
+
pip install spacr
|
158
|
+
|
159
|
+
**Run SpaCr GUI:**
|
160
|
+
|
161
|
+
::
|
162
|
+
|
163
|
+
spacr
|
164
|
+
|
165
|
+
Data Availability
|
166
|
+
-----------------
|
167
|
+
|
168
|
+
- **Raw sequencing data** are available from NCBI BioProject `PRJNA1261935 <https://www.ncbi.nlm.nih.gov/bioproject/PRJNA1261935>`_ and SRA accessions: `SRR33531217 <https://www.ncbi.nlm.nih.gov/sra/SRR33531217>`_, `SRR33531218 <https://www.ncbi.nlm.nih.gov/sra/SRR33531218>`_, `SRR33531219 <https://www.ncbi.nlm.nih.gov/sra/SRR33531219>`_, `SRR33531220 <https://www.ncbi.nlm.nih.gov/sra/SRR33531220>`_
|
169
|
+
|
170
|
+
- **Image data** is deposited at EBI BioStudies under accession:
|
171
|
+
`S-BIAD2076 <https://www.ebi.ac.uk/biostudies/studies/S-BIAD2076>`_
|
172
|
+
*(If the link redirects to the main BioStudies portal, copy and paste it directly into your browser.)*
|
173
|
+
|
174
|
+
|
175
|
+
Example Notebooks
|
176
|
+
-----------------
|
177
|
+
|
178
|
+
The following example Jupyter notebooks illustrate common workflows using spaCR.
|
179
|
+
|
180
|
+
- `Generate masks <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/1_spacr_generate_masks.ipynb>`_
|
181
|
+
*Generate cell, nuclei, and pathogen segmentation masks from microscopy images using Cellpose.*
|
182
|
+
|
183
|
+
- `Capture single cell images and measurements <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/2_spacr_generate_mesurments_crop_images.ipynb>`_
|
184
|
+
*Extract object-level measurements and crop single-cell images for downstream analysis.*
|
185
|
+
|
186
|
+
- `Machine learning based object classification <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/3a_spacr_machine_learning.ipynb>`_
|
187
|
+
*Train traditional machine learning models (e.g., XGBoost) to classify cell phenotypes based on extracted features.*
|
188
|
+
|
189
|
+
- `Computer vision based object classification <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/3b_spacr_computer_vision.ipynb>`_
|
190
|
+
*Train and evaluate deep learning models (PyTorch CNNs/Transformers) on cropped object images.*
|
191
|
+
|
192
|
+
- `Map sequencing barcodes <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/4_spacr_map_barecodes.ipynb>`_
|
193
|
+
*Map sequencing reads to row, column, and gRNA barcodes for CRISPR screen genotype-phenotype mapping.*
|
194
|
+
|
195
|
+
- `Finetune cellpose models <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/5_spacr_train_cellpose.ipynb>`_
|
196
|
+
*Finetune Cellpose models using your own annotated training data for improved segmentation accuracy.*
|
197
|
+
|
198
|
+
License
|
199
|
+
-------
|
200
|
+
spaCR is distributed under the terms of the MIT License.
|
201
|
+
See the `LICENSE <https://github.com/EinarOlafsson/spacr/blob/main/LICENSE>`_ file for details.
|
202
|
+
|
203
|
+
How to Cite
|
204
|
+
-----------
|
205
|
+
If you use spaCR in your research, please cite:
|
206
|
+
Olafsson EB, et al. SpaCr: Spatial phenotype analysis of CRISPR-Cas9 screens. *Manuscript in preparation*.
|
207
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
spacr/__init__.py,sha256=EoGInYks0M4foZElYNhksrQK6aEO1au7cncWexWNhRw,1376
|
2
2
|
spacr/__main__.py,sha256=H4MjaMF9ohZL6xfl1kTxVn1Nt_vEhhZArENMMBv8f4E,77
|
3
|
-
spacr/app_annotate.py,sha256=
|
3
|
+
spacr/app_annotate.py,sha256=p0OyvgFycIug7RcLfejFmc4HWB7yQskCBxxy3Sdq_Y0,2905
|
4
4
|
spacr/app_classify.py,sha256=urTP_wlZ58hSyM5a19slYlBxN0PdC-9-ga0hvq8CGWc,165
|
5
5
|
spacr/app_make_masks.py,sha256=pqDhRpluiHZz-kPX2Zh_KbYe4TsU43qYBa_7f-rsjpw,1694
|
6
6
|
spacr/app_mask.py,sha256=l-dBY8ftzCMdDe6-pXc2Nh_u-idNL9G7UOARiLJBtds,153
|
@@ -11,25 +11,25 @@ spacr/chat_bot.py,sha256=n3Fhqg3qofVXHmh3H9sUcmfYy9MmgRnr48663MVdY9E,1244
|
|
11
11
|
spacr/core.py,sha256=w4E3Pg-ZnA8BOK0iUMTjiNO0GeR5YCEs8fUTbESzqjY,47392
|
12
12
|
spacr/deep_spacr.py,sha256=055tIo3WP3elGFiIuSZaLURgu2XyUDxAdbw5ezASEqM,54526
|
13
13
|
spacr/gui.py,sha256=NhMh96KoArrSAaJBV6PhDQpIC1cQpxgb6SclhRbYG8s,8122
|
14
|
-
spacr/gui_core.py,sha256=
|
15
|
-
spacr/gui_elements.py,sha256=
|
16
|
-
spacr/gui_utils.py,sha256=
|
14
|
+
spacr/gui_core.py,sha256=RtpdB8S8yF9WARRsUjrZ1szZi4ZMfG7R_W34BTBEGYo,52729
|
15
|
+
spacr/gui_elements.py,sha256=OTU7aeLrPiMUTnyCT-J7ygng3beI9tdA0MmypOavEkw,156123
|
16
|
+
spacr/gui_utils.py,sha256=F6KfNY3OqNkvfkOP1rxwBha5IOdLVyBgqZYPw3xPLes,42293
|
17
17
|
spacr/io.py,sha256=SYLhupKnOJJscNSGE4N67E32-ywhwrjRccIfZrL38Uk,157966
|
18
18
|
spacr/logger.py,sha256=lJhTqt-_wfAunCPl93xE65Wr9Y1oIHJWaZMjunHUeIw,1538
|
19
|
-
spacr/measure.py,sha256=
|
19
|
+
spacr/measure.py,sha256=nYvrfVfCIqD1AUk4QBE2jtpeSFtLdfUcnkhkqf9G4xQ,60877
|
20
20
|
spacr/mediar.py,sha256=p0F515eFbm6_rePSnChsgqrgH-H5Sr_3zWrghtOnAUg,14863
|
21
21
|
spacr/ml.py,sha256=XCRZeX7UkbMctQICIoskeWVx8CCmmCoHNauUOAkfFq0,91692
|
22
22
|
spacr/openai.py,sha256=5vBZ3Jl2llYcW3oaTEXgdyCB2aJujMUIO5K038z7w_A,1246
|
23
|
-
spacr/plot.py,sha256=
|
23
|
+
spacr/plot.py,sha256=Y1ON8Bu-FsZZZasXIK7nvnOohFzucCvFhyPE2bDGz1A,167340
|
24
24
|
spacr/sequencing.py,sha256=EY12RdW5QRKpHDRQCw1QoAlxCq8FK2v6WoVa5uuDBXQ,26745
|
25
|
-
spacr/settings.py,sha256=
|
25
|
+
spacr/settings.py,sha256=gT0FEP6anfhM6sbFofmLRhOwaQptgpcI18VX6nRqmtQ,87661
|
26
26
|
spacr/sim.py,sha256=1xKhXimNU3ukzIw-3l9cF3Znc_brW8h20yv8fSTzvss,71173
|
27
27
|
spacr/sp_stats.py,sha256=mbhwsyIqt5upsSD346qGjdCw7CFBa0tIS7zHU9e0jNI,9536
|
28
28
|
spacr/spacr_cellpose.py,sha256=RBHMs2vwXcfkj0xqAULpALyzJYXddSRycgZSzmwI7v0,14755
|
29
29
|
spacr/submodules.py,sha256=Z2i4kv_rWdxqoXsOKCF7BaSXtvaCZB69Ow8_FQBnZsY,83093
|
30
30
|
spacr/timelapse.py,sha256=-5ZupTsCCpbenIQ2zsUmnwXh45B82fO-gPrSXOxu2s8,42980
|
31
31
|
spacr/toxo.py,sha256=GoNfgyH-NJx3WOzNQPgzODir7Jp65fs7UM46XpzcrUo,26056
|
32
|
-
spacr/utils.py,sha256=
|
32
|
+
spacr/utils.py,sha256=cw5zM6zpFWWUZQKwtYvXc_rNfBMW2ldbnlw8s6f6bFQ,234397
|
33
33
|
spacr/version.py,sha256=axH5tnGwtgSnJHb5IDhiu4Zjk5GhLyAEDRe-rnaoFOA,409
|
34
34
|
spacr/resources/data/lopit.csv,sha256=ERI5f9W8RdJGiSx_khoaylD374f8kmvLia1xjhD_mII,4421709
|
35
35
|
spacr/resources/data/toxoplasma_metadata.csv,sha256=9TXx0VlClDHAxQmaLhoklE8NuETduXaGHZjhR_6lZfs,2969409
|
@@ -82,6 +82,8 @@ spacr/resources/icons/convert.png,sha256=vLyTkQeUZ9q-pirhtZeXDq3-DzfjoPMjLlgKl5W
|
|
82
82
|
spacr/resources/icons/default.png,sha256=KoNhaSHukO4wDyivyYEgSbb5mGj-sAxmhKikLLtNpWs,20341
|
83
83
|
spacr/resources/icons/dna_matrix.mp4,sha256=NegOQkn4q4kHhFgqcIX2dd58wVytBtnkmbgg0ZegL8U,23462876
|
84
84
|
spacr/resources/icons/download.png,sha256=1nUoWRaTc4vIsK6gompdeqk0cIv2GdH-gCNHaEBX6Mc,20467
|
85
|
+
spacr/resources/icons/flow_chart_v2.png,sha256=4U4uzJlyQ8L-exWIXIhyqtkoO-KIiubO23kA7eLZYYE,640609
|
86
|
+
spacr/resources/icons/flow_chart_v3.png,sha256=Vw7ykdgmXEpA5BLUpDEnp_bAaJ6gPz94EVYqMHXmn1k,638047
|
85
87
|
spacr/resources/icons/logo.pdf,sha256=VB4cS41V3VV_QxD7l6CwdQKQiYLErugLBxWoCoxjQU0,377925
|
86
88
|
spacr/resources/icons/logo_spacr.png,sha256=qG3e3bdrAefhl1281rfo0R2XP0qA-c-oaBCXjxMGXkw,42587
|
87
89
|
spacr/resources/icons/logo_spacr_1.png,sha256=g9y2ZmnV3hab8r1idDfytm8AaHbBiQdu_93Jd7YKzwA,610892
|
@@ -101,9 +103,9 @@ spacr/resources/icons/umap.png,sha256=dOLF3DeLYy9k0nkUybiZMe1wzHQwLJFRmgccppw-8b
|
|
101
103
|
spacr/resources/images/plate1_E01_T0001F001L01A01Z01C02.tif,sha256=Tl0ZUfZ_AYAbu0up_nO0tPRtF1BxXhWQ3T3pURBCCRo,7958528
|
102
104
|
spacr/resources/images/plate1_E01_T0001F001L01A02Z01C01.tif,sha256=m8N-V71rA1TT4dFlENNg8s0Q0YEXXs8slIn7yObmZJQ,7958528
|
103
105
|
spacr/resources/images/plate1_E01_T0001F001L01A03Z01C03.tif,sha256=Pbhk7xn-KUP6RSIhJsxQcrHFImBm3GEpLkzx7WOc-5M,7958528
|
104
|
-
spacr-0.9.
|
105
|
-
spacr-0.9.
|
106
|
-
spacr-0.9.
|
107
|
-
spacr-0.9.
|
108
|
-
spacr-0.9.
|
109
|
-
spacr-0.9.
|
106
|
+
spacr-0.9.3.dist-info/LICENSE,sha256=t0Pov6pnK8thLteoF4xZGmdCwe5mhNwl3OXxLYTGD9U,1081
|
107
|
+
spacr-0.9.3.dist-info/METADATA,sha256=67z09Wghste6IUXWFchysvsqH38M6GVnr9IgHYixVNg,10088
|
108
|
+
spacr-0.9.3.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
109
|
+
spacr-0.9.3.dist-info/entry_points.txt,sha256=BMC0ql9aNNpv8lUZ8sgDLQMsqaVnX5L535gEhKUP5ho,296
|
110
|
+
spacr-0.9.3.dist-info/top_level.txt,sha256=GJPU8FgwRXGzKeut6JopsSRY2R8T3i9lDgya42tLInY,6
|
111
|
+
spacr-0.9.3.dist-info/RECORD,,
|
spacr-0.9.1.dist-info/METADATA
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: spacr
|
3
|
-
Version: 0.9.1
|
4
|
-
Summary: Spatial phenotype analysis of crisp screens (SpaCr)
|
5
|
-
Home-page: https://github.com/EinarOlafsson/spacr
|
6
|
-
Author: Einar Birnir Olafsson
|
7
|
-
Author-email: olafsson@med.umich.com
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Description-Content-Type: text/x-rst
|
12
|
-
License-File: LICENSE
|
13
|
-
Requires-Dist: numpy <2.0,>=1.26.4
|
14
|
-
Requires-Dist: pandas <3.0,>=2.2.1
|
15
|
-
Requires-Dist: scipy <2.0,>=1.12.0
|
16
|
-
Requires-Dist: cellpose <4.0,>=3.0.6
|
17
|
-
Requires-Dist: scikit-image <1.0,>=0.22.0
|
18
|
-
Requires-Dist: scikit-learn <2.0,>=1.4.1
|
19
|
-
Requires-Dist: scikit-posthocs <0.20,>=0.10.0
|
20
|
-
Requires-Dist: mahotas <2.0,>=1.4.13
|
21
|
-
Requires-Dist: btrack <1.0,>=0.6.5
|
22
|
-
Requires-Dist: trackpy <1.0,>=0.6.2
|
23
|
-
Requires-Dist: statsmodels <1.0,>=0.14.1
|
24
|
-
Requires-Dist: shap <1.0,>=0.45.0
|
25
|
-
Requires-Dist: torch <3.0,>=2.0
|
26
|
-
Requires-Dist: torchvision <1.0,>=0.1
|
27
|
-
Requires-Dist: torch-geometric <3.0,>=2.5
|
28
|
-
Requires-Dist: torchcam <1.0,>=0.4.0
|
29
|
-
Requires-Dist: transformers <5.0,>=4.45.2
|
30
|
-
Requires-Dist: segmentation-models-pytorch >=0.3.3
|
31
|
-
Requires-Dist: monai >=1.3.0
|
32
|
-
Requires-Dist: captum <1.0,>=0.7.0
|
33
|
-
Requires-Dist: seaborn <1.0,>=0.13.2
|
34
|
-
Requires-Dist: matplotlib <4.0,>=3.8.3
|
35
|
-
Requires-Dist: matplotlib-venn <2.0,>=1.1
|
36
|
-
Requires-Dist: adjustText <2.0,>=1.2.0
|
37
|
-
Requires-Dist: bottleneck <2.0,>=1.3.6
|
38
|
-
Requires-Dist: numexpr <3.0,>=2.8.4
|
39
|
-
Requires-Dist: opencv-python-headless <5.0,>=4.9.0.80
|
40
|
-
Requires-Dist: pillow <11.0,>=10.2.0
|
41
|
-
Requires-Dist: tifffile >=2023.4.12
|
42
|
-
Requires-Dist: nd2reader <4.0,>=3.3.0
|
43
|
-
Requires-Dist: czifile
|
44
|
-
Requires-Dist: pylibCZIrw <6.0,>=5.0.0
|
45
|
-
Requires-Dist: aicspylibczi
|
46
|
-
Requires-Dist: readlif
|
47
|
-
Requires-Dist: imageio <3.0,>=2.34.0
|
48
|
-
Requires-Dist: pingouin <1.0,>=0.5.5
|
49
|
-
Requires-Dist: umap-learn <1.0,>=0.5.6
|
50
|
-
Requires-Dist: ttkthemes <4.0,>=3.2.2
|
51
|
-
Requires-Dist: xgboost <3.0,>=2.0.3
|
52
|
-
Requires-Dist: PyWavelets <2.0,>=1.6.0
|
53
|
-
Requires-Dist: ttf-opensans >=2020.10.30
|
54
|
-
Requires-Dist: customtkinter <6.0,>=5.2.2
|
55
|
-
Requires-Dist: biopython <2.0,>=1.80
|
56
|
-
Requires-Dist: lxml <6.0,>=5.1.0
|
57
|
-
Requires-Dist: psutil <6.0,>=5.9.8
|
58
|
-
Requires-Dist: gputil <2.0,>=1.4.0
|
59
|
-
Requires-Dist: gpustat <2.0,>=1.1.1
|
60
|
-
Requires-Dist: pyautogui <1.0,>=0.9.54
|
61
|
-
Requires-Dist: tables <4.0,>=3.8.0
|
62
|
-
Requires-Dist: rapidfuzz <4.0,>=3.9
|
63
|
-
Requires-Dist: keyring <16.0,>=15.1
|
64
|
-
Requires-Dist: screeninfo <1.0,>=0.8.1
|
65
|
-
Requires-Dist: fastremap >=1.14.1
|
66
|
-
Requires-Dist: pytz >=2023.3.post1
|
67
|
-
Requires-Dist: tqdm >=4.65.0
|
68
|
-
Requires-Dist: wandb >=0.16.2
|
69
|
-
Requires-Dist: openai <2.0,>=1.50.2
|
70
|
-
Requires-Dist: gdown
|
71
|
-
Requires-Dist: IPython <9.0,>=8.18.1
|
72
|
-
Requires-Dist: ipykernel
|
73
|
-
Requires-Dist: ipywidgets <9.0,>=8.1.2
|
74
|
-
Requires-Dist: brokenaxes <1.0,>=0.6.2
|
75
|
-
Requires-Dist: huggingface-hub <0.25,>=0.24.0
|
76
|
-
Provides-Extra: dev
|
77
|
-
Requires-Dist: pytest <3.11,>=3.9 ; extra == 'dev'
|
78
|
-
Provides-Extra: full
|
79
|
-
Requires-Dist: opencv-python ; extra == 'full'
|
80
|
-
Provides-Extra: headless
|
81
|
-
Requires-Dist: opencv-python-headless ; extra == 'headless'
|
82
|
-
|
83
|
-
.. |Documentation Status| image:: https://readthedocs.org/projects/spacr/badge/?version=latest
|
84
|
-
:target: https://einarolafsson.github.io/spacr
|
85
|
-
.. |PyPI version| image:: https://badge.fury.io/py/spacr.svg
|
86
|
-
:target: https://badge.fury.io/py/spacr
|
87
|
-
.. |Python version| image:: https://img.shields.io/pypi/pyversions/spacr
|
88
|
-
:target: https://pypistats.org/packages/spacr
|
89
|
-
.. |Licence: GPL v3| image:: https://img.shields.io/github/license/EinarOlafsson/spacr
|
90
|
-
:target: https://github.com/EinarOlafsson/spacr/blob/master/LICENSE
|
91
|
-
.. |repo size| image:: https://img.shields.io/github/repo-size/EinarOlafsson/spacr
|
92
|
-
:target: https://github.com/EinarOlafsson/spacr/
|
93
|
-
|
94
|
-
|Documentation Status| |PyPI version| |Python version| |Licence: GPL v3| |repo size|
|
95
|
-
|
96
|
-
SpaCr
|
97
|
-
=====
|
98
|
-
|
99
|
-
Spatial phenotype analysis of CRISPR-Cas9 screens (SpaCr). The spatial organization of organelles and proteins within cells constitutes a key level of functional regulation. In the context of infectious disease, the spatial relationships between host cell structures and intracellular pathogens are critical to understanding host clearance mechanisms and how pathogens evade them. SpaCr is a Python-based software package for generating single-cell image data for deep-learning sub-cellular/cellular phenotypic classification from pooled genetic CRISPR-Cas9 screens. SpaCr provides a flexible toolset to extract single-cell images and measurements from high-content cell painting experiments, train deep-learning models to classify cellular/subcellular phenotypes, simulate, and analyze pooled CRISPR-Cas9 imaging screens.
|
100
|
-
|
101
|
-
Features
|
102
|
-
--------
|
103
|
-
|
104
|
-
- **Generate Masks:** Generate cellpose masks of cell, nuclei, and pathogen objects.
|
105
|
-
|
106
|
-
- **Object Measurements:** Measurements for each object including scikit-image-regionprops, intensity percentiles, shannon-entropy, pearsons and manders correlations, homogeneity, and radial distribution. Measurements are saved to a SQL database in object-level tables.
|
107
|
-
|
108
|
-
- **Crop Images:** Save objects (cells, nuclei, pathogen, cytoplasm) as images. Object image paths are saved in a SQL database.
|
109
|
-
|
110
|
-
- **Train CNNs or Transformers:** Train Torch models to classify single object images.
|
111
|
-
|
112
|
-
- **Manual Annotation:** Supports manual annotation of single-cell images and segmentation to refine training datasets for training CNNs/Transformers or cellpose, respectively.
|
113
|
-
|
114
|
-
- **Finetune Cellpose Models:** Adjust pre-existing Cellpose models to your specific dataset for improved performance.
|
115
|
-
|
116
|
-
- **Timelapse Data Support:** Track objects in timelapse image data.
|
117
|
-
|
118
|
-
- **Simulations:** Simulate spatial phenotype screens.
|
119
|
-
|
120
|
-
- **Sequencing:** Map FASTQ reads to barcode and gRNA barcode metadata.
|
121
|
-
|
122
|
-
- **Misc:** Analyze Ca oscillation, recruitment, infection rate, plaque size/count.
|
123
|
-
|
124
|
-
Installation
|
125
|
-
------------
|
126
|
-
|
127
|
-
If using Windows, switch to Linux—it's free, open-source, and better.
|
128
|
-
|
129
|
-
Before installing SpaCr on OSX ensure OpenMP is installed::
|
130
|
-
|
131
|
-
brew install libomp
|
132
|
-
brew install hdf5
|
133
|
-
|
134
|
-
SpaCr GUI requires Tkinter. On Linux, ensure Tkinter is installed. (Tkinter is included with the standard Python installation on macOS and Windows)::
|
135
|
-
|
136
|
-
sudo apt-get install python3-tk
|
137
|
-
|
138
|
-
Install SpaCr with pip::
|
139
|
-
|
140
|
-
pip install spacr
|
141
|
-
|
142
|
-
Run SpaCr GUI::
|
143
|
-
|
144
|
-
spacr
|
File without changes
|
File without changes
|
File without changes
|