easymode 0.0.1__tar.gz
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.
- easymode-0.0.1/PKG-INFO +20 -0
- easymode-0.0.1/easymode/__init__.py +0 -0
- easymode-0.0.1/easymode/core/__init__.py +0 -0
- easymode-0.0.1/easymode/core/augmentations.py +53 -0
- easymode-0.0.1/easymode/core/config.py +49 -0
- easymode-0.0.1/easymode/core/distribution.py +206 -0
- easymode-0.0.1/easymode/core/inference.py +257 -0
- easymode-0.0.1/easymode/core/model.py +205 -0
- easymode-0.0.1/easymode/core/packaging.py +25 -0
- easymode-0.0.1/easymode/core/settings.txt +5 -0
- easymode-0.0.1/easymode/core/train.py +168 -0
- easymode-0.0.1/easymode/core/warp.py +181 -0
- easymode-0.0.1/easymode/main.py +115 -0
- easymode-0.0.1/easymode/membrain_fourier_augmentations/__init__.py +0 -0
- easymode-0.0.1/easymode/membrain_fourier_augmentations/filter_utils.py +77 -0
- easymode-0.0.1/easymode/membrain_fourier_augmentations/fourier_augmentations.py +299 -0
- easymode-0.0.1/easymode/membrain_fourier_augmentations/transforms.py +30 -0
- easymode-0.0.1/easymode.egg-info/PKG-INFO +20 -0
- easymode-0.0.1/easymode.egg-info/SOURCES.txt +23 -0
- easymode-0.0.1/easymode.egg-info/dependency_links.txt +1 -0
- easymode-0.0.1/easymode.egg-info/entry_points.txt +2 -0
- easymode-0.0.1/easymode.egg-info/requires.txt +7 -0
- easymode-0.0.1/easymode.egg-info/top_level.txt +1 -0
- easymode-0.0.1/setup.cfg +4 -0
- easymode-0.0.1/setup.py +36 -0
easymode-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easymode
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Easymode - a collection of pretrained general networks for segmenting common eukaryotic features in cryoET
|
|
5
|
+
Home-page:
|
|
6
|
+
Author: mgflast
|
|
7
|
+
Author-email: mgflast@gmail.com
|
|
8
|
+
License: GPL v3
|
|
9
|
+
Requires-Dist: mrcfile
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: scipy
|
|
12
|
+
Requires-Dist: huggingface_hub
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: tifffile
|
|
15
|
+
Requires-Dist: psutil
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: license
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: summary
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import random
|
|
3
|
+
from scipy.ndimage import rotate, gaussian_filter, median_filter
|
|
4
|
+
|
|
5
|
+
from easymode.membrain_fourier_augmentations.fourier_augmentations import MissingWedgeMaskAndFourierAmplitudeMatchingCombined
|
|
6
|
+
|
|
7
|
+
ROT_XZ_YZ_MAX_ANGLE = 10.0
|
|
8
|
+
ROT_XY_MAX_ANGLE = 10.0
|
|
9
|
+
|
|
10
|
+
def rotate_90_xy(img, label):
|
|
11
|
+
k = random.randint(0, 3)
|
|
12
|
+
img = np.rot90(img, k=k, axes=(1, 2))
|
|
13
|
+
label = np.rot90(label, k=k, axes=(1, 2))
|
|
14
|
+
return img, label
|
|
15
|
+
|
|
16
|
+
def rotate_90_xz(img, label):
|
|
17
|
+
k = random.randint(0, 1) * 2
|
|
18
|
+
img = np.rot90(img, k=k, axes=(0, 2))
|
|
19
|
+
label = np.rot90(label, k=k, axes=(0, 2))
|
|
20
|
+
return img, label
|
|
21
|
+
|
|
22
|
+
def flip(img, label):
|
|
23
|
+
k = random.choice([None, 0, 1, 2])
|
|
24
|
+
if k is not None:
|
|
25
|
+
img = np.flip(img, axis=k)
|
|
26
|
+
label = np.flip(label, axis=k)
|
|
27
|
+
return img, label
|
|
28
|
+
|
|
29
|
+
def rotate_continuous_xz_or_yz(img, label):
|
|
30
|
+
plane = random.choice([(0, 2), (0, 1)])
|
|
31
|
+
angle = np.random.uniform(-ROT_XZ_YZ_MAX_ANGLE, ROT_XZ_YZ_MAX_ANGLE)
|
|
32
|
+
|
|
33
|
+
img = rotate(img, angle, axes=plane, order=1, mode='reflect', prefilter=False, reshape=False)
|
|
34
|
+
label = rotate(label, angle, axes=plane, order=0, mode='constant', cval=2, reshape=False)
|
|
35
|
+
|
|
36
|
+
return img, label
|
|
37
|
+
|
|
38
|
+
def rotate_continuous_xy(img, label):
|
|
39
|
+
angle = np.random.uniform(-ROT_XY_MAX_ANGLE, ROT_XY_MAX_ANGLE)
|
|
40
|
+
|
|
41
|
+
img = rotate(img, angle, axes=(1, 2), order=1, mode='reflect', prefilter=False, reshape=False)
|
|
42
|
+
label = rotate(label, angle, axes=(1, 2), order=0, mode='constant', cval=2, reshape=False)
|
|
43
|
+
|
|
44
|
+
return img, label
|
|
45
|
+
|
|
46
|
+
def remove_wedge(img, label):
|
|
47
|
+
membrain_fourier_trickery_machine = MissingWedgeMaskAndFourierAmplitudeMatchingCombined()
|
|
48
|
+
img = membrain_fourier_trickery_machine(img)
|
|
49
|
+
return img, label
|
|
50
|
+
|
|
51
|
+
def filter_gaussian(img, label):
|
|
52
|
+
img = gaussian_filter(img, sigma=random.uniform(0.3, 1.5))
|
|
53
|
+
return img, label
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os, shutil, json
|
|
2
|
+
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
license = "GNU GPL v3"
|
|
5
|
+
|
|
6
|
+
root = os.path.dirname(os.path.dirname(__file__))
|
|
7
|
+
settings_path = os.path.join(os.path.expanduser("~"), "easymode", "settings.txt")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_settings():
|
|
11
|
+
# If settings file not found, copy the one from core to the right location.
|
|
12
|
+
if not os.path.exists(settings_path):
|
|
13
|
+
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
|
|
14
|
+
shutil.copy(os.path.join(root, "core", "settings.txt"), settings_path)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
with open(settings_path, 'r') as f:
|
|
18
|
+
sdict = json.load(f)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
shutil.copy(os.path.join(root, "core", "settings.txt"), settings_path)
|
|
21
|
+
parse_settings()
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Read settings - if any parameters are missing, insert them.
|
|
25
|
+
with open(os.path.join(root, "core", "settings.txt"), 'r') as f:
|
|
26
|
+
default_settings = json.load(f)
|
|
27
|
+
|
|
28
|
+
for key in default_settings:
|
|
29
|
+
if key not in sdict:
|
|
30
|
+
sdict[key] = default_settings[key]
|
|
31
|
+
|
|
32
|
+
if sdict["MODEL_DIRECTORY"] == "" or not os.path.exists(sdict["MODEL_DIRECTORY"]):
|
|
33
|
+
sdict["MODEL_DIRECTORY"] = os.path.join(os.path.expanduser("~"), "easymode")
|
|
34
|
+
|
|
35
|
+
with open(settings_path, 'w') as f:
|
|
36
|
+
json.dump(sdict, f, indent=2)
|
|
37
|
+
|
|
38
|
+
return sdict
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
settings = parse_settings()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def edit_setting(key, value):
|
|
45
|
+
global settings
|
|
46
|
+
settings[key] = value
|
|
47
|
+
with open(settings_path, 'w') as f:
|
|
48
|
+
json.dump(settings, f, indent=2)
|
|
49
|
+
print(key, value)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from huggingface_hub import hf_hub_download, HfApi
|
|
6
|
+
from easymode.core.model import create
|
|
7
|
+
import tensorflow as tf
|
|
8
|
+
import easymode.core.config as cfg
|
|
9
|
+
|
|
10
|
+
# Configuration
|
|
11
|
+
HF_REPO_ID = "mgflast/easymode" # Single repo for all models
|
|
12
|
+
MODEL_CACHE_DIR = cfg.settings["MODEL_DIRECTORY"]
|
|
13
|
+
VERSION_FILE = "model_info.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_model_info(model_title):
|
|
17
|
+
"""Get model repository and filename info."""
|
|
18
|
+
filename = f"{model_title}_3d.h5"
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
'repo_id': HF_REPO_ID,
|
|
22
|
+
'filename': filename,
|
|
23
|
+
'local_path': os.path.join(MODEL_CACHE_DIR, filename),
|
|
24
|
+
'version_path': os.path.join(MODEL_CACHE_DIR, f"{model_title}_3d_info.json")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_online():
|
|
29
|
+
"""Check if internet connection is available."""
|
|
30
|
+
try:
|
|
31
|
+
response = requests.get("https://huggingface.co", timeout=5)
|
|
32
|
+
return response.status_code == 200
|
|
33
|
+
except:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_remote_version(repo_id):
|
|
38
|
+
"""Get latest version info from Hugging Face."""
|
|
39
|
+
try:
|
|
40
|
+
api = HfApi()
|
|
41
|
+
repo_info = api.repo_info(repo_id)
|
|
42
|
+
|
|
43
|
+
# Use last modified time as version identifier
|
|
44
|
+
last_modified = repo_info.last_modified
|
|
45
|
+
return {
|
|
46
|
+
'version': last_modified.isoformat() if last_modified else "unknown",
|
|
47
|
+
'commit_hash': repo_info.sha[:8] if repo_info.sha else "unknown"
|
|
48
|
+
}
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Warning: Could not get remote version info: {e}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_local_version(version_path):
|
|
55
|
+
"""Get local version info."""
|
|
56
|
+
if not os.path.exists(version_path):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(version_path, 'r') as f:
|
|
61
|
+
return json.load(f)
|
|
62
|
+
except:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_version_info(version_path, version_info):
|
|
67
|
+
"""Save version info locally."""
|
|
68
|
+
os.makedirs(os.path.dirname(version_path), exist_ok=True)
|
|
69
|
+
with open(version_path, 'w') as f:
|
|
70
|
+
json.dump(version_info, f, indent=2)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def download_model(repo_id, filename, local_path, version_path):
|
|
74
|
+
"""Download model from Hugging Face."""
|
|
75
|
+
print(f"Downloading {repo_id}/{filename}...")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Ensure cache directory exists
|
|
79
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
80
|
+
|
|
81
|
+
# Download file
|
|
82
|
+
downloaded_path = hf_hub_download(
|
|
83
|
+
repo_id=repo_id,
|
|
84
|
+
filename=filename,
|
|
85
|
+
cache_dir=MODEL_CACHE_DIR,
|
|
86
|
+
local_dir=os.path.dirname(local_path)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Get and save version info
|
|
90
|
+
remote_version = get_remote_version(repo_id)
|
|
91
|
+
if remote_version:
|
|
92
|
+
save_version_info(version_path, remote_version)
|
|
93
|
+
|
|
94
|
+
print(f"\nDownloaded successfully to {local_path}")
|
|
95
|
+
return local_path
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise RuntimeError(f"Failed to download {repo_id}: {e}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_model_weights(weights_path):
|
|
102
|
+
model = create()
|
|
103
|
+
dummy_input = tf.zeros((1, 160, 160, 160, 1))
|
|
104
|
+
_ = model(dummy_input)
|
|
105
|
+
model.load_weights(weights_path)
|
|
106
|
+
return model
|
|
107
|
+
|
|
108
|
+
def cache_model(model_title, force_download=False, silent=False):
|
|
109
|
+
info = get_model_info(model_title)
|
|
110
|
+
online = is_online()
|
|
111
|
+
|
|
112
|
+
# Check if local file exists
|
|
113
|
+
local_exists = os.path.exists(info['local_path'])
|
|
114
|
+
|
|
115
|
+
if force_download or not local_exists:
|
|
116
|
+
# Need to download
|
|
117
|
+
if not online:
|
|
118
|
+
if local_exists:
|
|
119
|
+
print("Local model found. There may be updates available, but we cannot check without an internet connection.")
|
|
120
|
+
else:
|
|
121
|
+
print(f"The required network weights are not available in the local cache {MODEL_CACHE_DIR} and there is no internet connection available to download them - aborting...")
|
|
122
|
+
exit()
|
|
123
|
+
else:
|
|
124
|
+
print(f"The required network weights for {model_title} are not available in the local cache. Downloading now...")
|
|
125
|
+
download_model(info['repo_id'], info['filename'],
|
|
126
|
+
info['local_path'], info['version_path'])
|
|
127
|
+
|
|
128
|
+
elif local_exists and online:
|
|
129
|
+
# Check if we need to update
|
|
130
|
+
local_version = get_local_version(info['version_path'])
|
|
131
|
+
remote_version = get_remote_version(info['repo_id'])
|
|
132
|
+
|
|
133
|
+
if remote_version and local_version:
|
|
134
|
+
if remote_version['version'] != local_version['version']:
|
|
135
|
+
if not silent:
|
|
136
|
+
print(f"New version available for {model_title}, updating...")
|
|
137
|
+
download_model(info['repo_id'], info['filename'], info['local_path'], info['version_path'])
|
|
138
|
+
elif remote_version and not local_version:
|
|
139
|
+
if not silent:
|
|
140
|
+
print("No local version info, checking for updates...")
|
|
141
|
+
save_version_info(info['version_path'], remote_version)
|
|
142
|
+
|
|
143
|
+
return info['local_path']
|
|
144
|
+
|
|
145
|
+
def load_model(local_path):
|
|
146
|
+
return load_model_weights(local_path)
|
|
147
|
+
|
|
148
|
+
def clear_model_cache(model_title=None):
|
|
149
|
+
"""Clear local model cache."""
|
|
150
|
+
if model_title:
|
|
151
|
+
# Clear specific model
|
|
152
|
+
info = get_model_info(model_title)
|
|
153
|
+
files_to_remove = [info['local_path'], info['version_path']]
|
|
154
|
+
|
|
155
|
+
for file_path in files_to_remove:
|
|
156
|
+
if os.path.exists(file_path):
|
|
157
|
+
os.remove(file_path)
|
|
158
|
+
print(f"Removed {file_path}")
|
|
159
|
+
else:
|
|
160
|
+
# Clear all models
|
|
161
|
+
import shutil
|
|
162
|
+
if os.path.exists(MODEL_CACHE_DIR):
|
|
163
|
+
shutil.rmtree(MODEL_CACHE_DIR)
|
|
164
|
+
print(f"Cleared model cache: {MODEL_CACHE_DIR}")
|
|
165
|
+
|
|
166
|
+
def list_remote_models():
|
|
167
|
+
"""List all available models in the Hugging Face repository."""
|
|
168
|
+
if not is_online():
|
|
169
|
+
print("Cannot list remote models: No internet connection")
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
api = HfApi()
|
|
174
|
+
repo_files = api.list_repo_files(HF_REPO_ID)
|
|
175
|
+
|
|
176
|
+
# Filter for .h5 model files
|
|
177
|
+
model_files = [f for f in repo_files if f.endswith('.h5')]
|
|
178
|
+
|
|
179
|
+
if not model_files:
|
|
180
|
+
print("No model files found in repository")
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
print()
|
|
184
|
+
print(f"Easymode can currently segment the following features:")
|
|
185
|
+
print()
|
|
186
|
+
models = []
|
|
187
|
+
|
|
188
|
+
for model_file in sorted(model_files):
|
|
189
|
+
model_name = model_file.replace('.h5', '')
|
|
190
|
+
if '_' in model_name:
|
|
191
|
+
title, dim = model_name.rsplit('_', 1)
|
|
192
|
+
models.append({'title': title, 'filename': model_file})
|
|
193
|
+
|
|
194
|
+
# Check if we have it locally
|
|
195
|
+
local_path = os.path.join(MODEL_CACHE_DIR, model_file)
|
|
196
|
+
local_status = "weights local" if os.path.exists(local_path) else "weights available for download"
|
|
197
|
+
|
|
198
|
+
print(f" > {title} - [{local_status}]")
|
|
199
|
+
else:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
return models
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"Error listing remote models: {e}")
|
|
206
|
+
return []
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import os, glob, time, multiprocessing, psutil
|
|
2
|
+
import tensorflow as tf
|
|
3
|
+
import gc
|
|
4
|
+
from tensorflow.keras import mixed_precision
|
|
5
|
+
import mrcfile
|
|
6
|
+
import numpy as np
|
|
7
|
+
from easymode.core.distribution import cache_model, load_model
|
|
8
|
+
|
|
9
|
+
TILE_SIZE = 160
|
|
10
|
+
OVERLAP = 32
|
|
11
|
+
MAX_CHUNK_SIZE = 64
|
|
12
|
+
|
|
13
|
+
def tile_volume(volume, patch_size=TILE_SIZE, overlap=OVERLAP):
|
|
14
|
+
d, h, w = volume.shape
|
|
15
|
+
stride = patch_size - 2 * overlap
|
|
16
|
+
|
|
17
|
+
z_boxes = max(1, (d + stride - 1) // stride)
|
|
18
|
+
y_boxes = max(1, (h + stride - 1) // stride)
|
|
19
|
+
x_boxes = max(1, (w + stride - 1) // stride)
|
|
20
|
+
|
|
21
|
+
tiles = []
|
|
22
|
+
positions = []
|
|
23
|
+
|
|
24
|
+
for z_idx in range(z_boxes):
|
|
25
|
+
for y_idx in range(y_boxes):
|
|
26
|
+
for x_idx in range(x_boxes):
|
|
27
|
+
z_start = z_idx * stride - overlap
|
|
28
|
+
y_start = y_idx * stride - overlap
|
|
29
|
+
x_start = x_idx * stride - overlap
|
|
30
|
+
|
|
31
|
+
vol_z_start = max(0, z_start)
|
|
32
|
+
vol_y_start = max(0, y_start)
|
|
33
|
+
vol_x_start = max(0, x_start)
|
|
34
|
+
|
|
35
|
+
vol_z_end = min(d, z_start + patch_size)
|
|
36
|
+
vol_y_end = min(h, y_start + patch_size)
|
|
37
|
+
vol_x_end = min(w, x_start + patch_size)
|
|
38
|
+
|
|
39
|
+
extracted = volume[vol_z_start:vol_z_end, vol_y_start:vol_y_end, vol_x_start:vol_x_end]
|
|
40
|
+
|
|
41
|
+
tile = np.zeros((patch_size, patch_size, patch_size), dtype=volume.dtype)
|
|
42
|
+
|
|
43
|
+
tile_z_start = vol_z_start - z_start
|
|
44
|
+
tile_y_start = vol_y_start - y_start
|
|
45
|
+
tile_x_start = vol_x_start - x_start
|
|
46
|
+
|
|
47
|
+
tile[tile_z_start:tile_z_start + extracted.shape[0],
|
|
48
|
+
tile_y_start:tile_y_start + extracted.shape[1],
|
|
49
|
+
tile_x_start:tile_x_start + extracted.shape[2]] = extracted
|
|
50
|
+
|
|
51
|
+
tiles.append(tile)
|
|
52
|
+
positions.append((z_idx * stride, y_idx * stride, x_idx * stride))
|
|
53
|
+
|
|
54
|
+
tiles = np.array(tiles)
|
|
55
|
+
tiles = np.expand_dims(tiles, axis=-1)
|
|
56
|
+
|
|
57
|
+
return tiles, positions, volume.shape
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def detile_volume(segmented_tiles, positions, original_shape, patch_size=TILE_SIZE, overlap=OVERLAP):
|
|
61
|
+
d, h, w = original_shape
|
|
62
|
+
output_volume = np.zeros((d, h, w), dtype=np.float32)
|
|
63
|
+
stride = patch_size - 2 * overlap
|
|
64
|
+
|
|
65
|
+
if segmented_tiles.ndim == 5:
|
|
66
|
+
segmented_tiles = segmented_tiles.squeeze(-1)
|
|
67
|
+
|
|
68
|
+
for tile, (z_pos, y_pos, x_pos) in zip(segmented_tiles, positions):
|
|
69
|
+
center_region = tile[overlap:overlap + stride, overlap:overlap + stride, overlap:overlap + stride]
|
|
70
|
+
|
|
71
|
+
z_end = min(z_pos + stride, d)
|
|
72
|
+
y_end = min(y_pos + stride, h)
|
|
73
|
+
x_end = min(x_pos + stride, w)
|
|
74
|
+
|
|
75
|
+
actual_z = z_end - z_pos
|
|
76
|
+
actual_y = y_end - y_pos
|
|
77
|
+
actual_x = x_end - x_pos
|
|
78
|
+
|
|
79
|
+
output_volume[z_pos:z_end, y_pos:y_end, x_pos:x_end] = center_region[:actual_z, :actual_y, :actual_x]
|
|
80
|
+
|
|
81
|
+
return output_volume
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_weight_matrix(patch_size, border_size=OVERLAP):
|
|
85
|
+
"""Create weight matrix that reduces border artifacts."""
|
|
86
|
+
weights = np.ones((patch_size, patch_size, patch_size), dtype=np.float32)
|
|
87
|
+
fade_size = 16
|
|
88
|
+
|
|
89
|
+
for dim in range(3):
|
|
90
|
+
profile = np.ones(patch_size)
|
|
91
|
+
|
|
92
|
+
for i in range(border_size + fade_size):
|
|
93
|
+
if i < border_size:
|
|
94
|
+
weight = 0.1
|
|
95
|
+
else:
|
|
96
|
+
weight = 0.1 + 0.9 * (i - border_size) / fade_size
|
|
97
|
+
|
|
98
|
+
profile[i] = weight
|
|
99
|
+
profile[-(i + 1)] = weight
|
|
100
|
+
|
|
101
|
+
if dim == 0:
|
|
102
|
+
weights *= profile[:, np.newaxis, np.newaxis]
|
|
103
|
+
elif dim == 1:
|
|
104
|
+
weights *= profile[np.newaxis, :, np.newaxis]
|
|
105
|
+
else:
|
|
106
|
+
weights *= profile[np.newaxis, np.newaxis, :]
|
|
107
|
+
|
|
108
|
+
return weights
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _segment_tile_list(tiles, model, batch_size=8, max_chunk_size=MAX_CHUNK_SIZE):
|
|
112
|
+
num_tiles = len(tiles)
|
|
113
|
+
segmented_tiles = []
|
|
114
|
+
for i in range(0, num_tiles, max_chunk_size):
|
|
115
|
+
chunk_end = min(i + max_chunk_size, num_tiles)
|
|
116
|
+
chunk = tiles[i:chunk_end]
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
chunk_result = model.predict(chunk, verbose=0, batch_size=batch_size)
|
|
120
|
+
segmented_tiles.extend(chunk_result)
|
|
121
|
+
|
|
122
|
+
except tf.errors.ResourceExhaustedError:
|
|
123
|
+
print(f"Memory error with chunk size {len(chunk)}, falling back to smaller chunks")
|
|
124
|
+
fallback_chunk_size = max(1, len(chunk) // 4)
|
|
125
|
+
for j in range(i, chunk_end, fallback_chunk_size):
|
|
126
|
+
small_chunk = tiles[j:min(j + fallback_chunk_size, chunk_end)]
|
|
127
|
+
small_result = model.predict(small_chunk, verbose=0, batch_size=batch_size)
|
|
128
|
+
segmented_tiles.extend(small_result)
|
|
129
|
+
|
|
130
|
+
return segmented_tiles
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _segment_tomogram_instance(volume, model, batch_size):
|
|
134
|
+
tiles, positions, original_shape = tile_volume(volume)
|
|
135
|
+
segmented_tiles = _segment_tile_list(tiles, model, batch_size=batch_size, max_chunk_size=MAX_CHUNK_SIZE)
|
|
136
|
+
segmented_tiles = np.array(segmented_tiles)
|
|
137
|
+
segmented_volume = detile_volume(segmented_tiles, positions, original_shape)
|
|
138
|
+
|
|
139
|
+
tf.keras.backend.clear_session()
|
|
140
|
+
gc.collect()
|
|
141
|
+
return segmented_volume.astype(np.float32)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def segment_tomogram(model, tomogram_path, tta=1, batch_size=2):
|
|
145
|
+
volume = mrcfile.read(tomogram_path)
|
|
146
|
+
volume -= np.mean(volume)
|
|
147
|
+
volume /= np.std(volume) + 1e-8
|
|
148
|
+
|
|
149
|
+
segmented_volume = np.zeros_like(volume)
|
|
150
|
+
k_xy = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
|
|
151
|
+
k_fx = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
|
|
152
|
+
k_yz = [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1]
|
|
153
|
+
for j in range(tta):
|
|
154
|
+
tta_vol = volume.copy()
|
|
155
|
+
tta_vol = np.rot90(tta_vol, k=k_xy[j], axes=(1, 2))
|
|
156
|
+
tta_vol = tta_vol if not k_fx[j] else np.flip(tta_vol, axis=1)
|
|
157
|
+
tta_vol = np.rot90(tta_vol, k=2 * k_yz[j], axes=(0, 1))
|
|
158
|
+
segmented_tta_vol = _segment_tomogram_instance(tta_vol, model, batch_size)
|
|
159
|
+
segmented_tta_vol = np.rot90(segmented_tta_vol, k=-2 * k_yz[j], axes=(0, 1))
|
|
160
|
+
segmented_tta_vol = segmented_tta_vol if not k_fx[j] else np.flip(segmented_tta_vol, axis=1)
|
|
161
|
+
segmented_tta_vol = np.rot90(segmented_tta_vol, k=-k_xy[j], axes=(1, 2))
|
|
162
|
+
segmented_volume += segmented_tta_vol
|
|
163
|
+
segmented_volume /= tta
|
|
164
|
+
|
|
165
|
+
segmented_volume[:32, :, :] = 0
|
|
166
|
+
segmented_volume[-32:, :, :] = 0
|
|
167
|
+
segmented_volume[:, :32, :] = 0
|
|
168
|
+
segmented_volume[:, -32:, :] = 0
|
|
169
|
+
segmented_volume[:, :, :32] = 0
|
|
170
|
+
segmented_volume[:, :, -32:] = 0
|
|
171
|
+
return segmented_volume
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def save_mrc(pxd, path, data_format, voxel_size=10.0):
|
|
175
|
+
if data_format == 'float32':
|
|
176
|
+
pxd = pxd.astype(np.float32)
|
|
177
|
+
elif data_format == 'uint16':
|
|
178
|
+
pxd = (pxd * 255).astype(np.uint16) # scaling to [0, 255] because that's what we're used to in Ais
|
|
179
|
+
elif data_format == 'int8':
|
|
180
|
+
pxd = (pxd * 127).astype(np.int8)
|
|
181
|
+
with mrcfile.new(path, overwrite=True) as m:
|
|
182
|
+
m.set_data(pxd)
|
|
183
|
+
m.voxel_size = voxel_size
|
|
184
|
+
|
|
185
|
+
def segmentation_thread(tomogram_list, model_path, feature, output_dir, gpu, batch_size, tta, overwrite, data_format):
|
|
186
|
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu)
|
|
187
|
+
os.environ['TF_DISABLE_MKL'] = '1'
|
|
188
|
+
os.environ['TF_XLA_FLAGS'] = '--tf_xla_enable_xla_devices=false'
|
|
189
|
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
|
|
190
|
+
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
|
|
191
|
+
for device in tf.config.list_physical_devices('GPU'):
|
|
192
|
+
tf.config.experimental.set_memory_growth(device, True)
|
|
193
|
+
mixed_precision.set_global_policy('mixed_float16')
|
|
194
|
+
|
|
195
|
+
process_start_time = psutil.Process().create_time()
|
|
196
|
+
|
|
197
|
+
model = load_model(model_path)
|
|
198
|
+
|
|
199
|
+
for j, tomogram_path in enumerate(tomogram_list, 1):
|
|
200
|
+
tomo_name = os.path.splitext(os.path.basename(tomogram_path))[0]
|
|
201
|
+
output_file = os.path.join(output_dir, f"{tomo_name}__{feature}.mrc")
|
|
202
|
+
wrote_temporary = False
|
|
203
|
+
try:
|
|
204
|
+
if os.path.exists(output_file):
|
|
205
|
+
file_age = os.path.getmtime(output_file)
|
|
206
|
+
if not overwrite or file_age > process_start_time - 60:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
with mrcfile.new(output_file, overwrite=True) as m:
|
|
210
|
+
m.set_data(-1.0 * np.ones((10, 10, 10), dtype=np.float32))
|
|
211
|
+
wrote_temporary = True
|
|
212
|
+
segmented_volume = segment_tomogram(model, tomogram_path, tta, batch_size)
|
|
213
|
+
|
|
214
|
+
save_mrc(segmented_volume, output_file, data_format)
|
|
215
|
+
print(f"{j}/{len(tomogram_list)} (on GPU {gpu}) - {feature} - {os.path.basename(output_file)}")
|
|
216
|
+
except Exception as e:
|
|
217
|
+
if wrote_temporary:
|
|
218
|
+
os.remove(output_file)
|
|
219
|
+
print(f"{j}/{len(tomogram_list)} (on GPU {gpu}) - {feature} - {os.path.basename(output_file)} - ERROR: {e}")
|
|
220
|
+
|
|
221
|
+
def dispatch_segment(feature, data_directory, output_directory, tta=1, gpus=(0,1,2,3), batch_size=8, overwrite=False, data_format='int8'):
|
|
222
|
+
if output_directory is None:
|
|
223
|
+
output_directory = data_directory
|
|
224
|
+
gpus = [int(n) for n in gpus.split(',')]
|
|
225
|
+
|
|
226
|
+
print(f'easymode segment\n'
|
|
227
|
+
f'feature: {feature}\n'
|
|
228
|
+
f'data_directory: {data_directory}\n'
|
|
229
|
+
f'output_directory: {output_directory}\n'
|
|
230
|
+
f'output_format: {data_format}\n'
|
|
231
|
+
f'gpus: {gpus}\n'
|
|
232
|
+
f'tta: {tta}\n'
|
|
233
|
+
f'overwrite: {overwrite}\n'
|
|
234
|
+
f'batch_size: {batch_size}\n')
|
|
235
|
+
|
|
236
|
+
tomograms = sorted(glob.glob(os.path.join(data_directory, '*.mrc')))
|
|
237
|
+
|
|
238
|
+
model_path = cache_model(feature)
|
|
239
|
+
|
|
240
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
241
|
+
|
|
242
|
+
processes = []
|
|
243
|
+
for gpu in gpus:
|
|
244
|
+
p = multiprocessing.Process(target=segmentation_thread,
|
|
245
|
+
args=(tomograms, model_path, feature, output_directory, gpu, batch_size, tta, overwrite, data_format))
|
|
246
|
+
processes.append(p)
|
|
247
|
+
p.start()
|
|
248
|
+
time.sleep(2)
|
|
249
|
+
|
|
250
|
+
for p in processes:
|
|
251
|
+
p.join()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|