Skip to content

Commit 2326bb6

Browse files
author
tibuch
authored
Merge pull request #31 from juglab/issue_20
Issue 20
2 parents 71b1769 + f479967 commit 2326bb6

18 files changed

Lines changed: 658 additions & 280 deletions

.travis.yml

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,22 @@ env:
1313
matrix:
1414
include:
1515
- os: linux
16-
python: 3.5
17-
- os: linux
18-
python: 3.6
19-
###########
20-
- os: linux
21-
python: 3.6
22-
if: branch = master
23-
env: TENSORFLOW='tensorflow<1.12' KERAS='keras'
24-
- os: linux
16+
dist: bionic
2517
python: 3.6
26-
if: branch = master
27-
env: TENSORFLOW='tensorflow<1.11' KERAS='keras'
18+
env: TENSORFLOW='tensorflow==1.14.0' KERAS='keras==2.2.4'
2819
- os: linux
20+
dist: bionic
2921
python: 3.6
30-
if: branch = master
31-
env: TENSORFLOW='tensorflow<1.10' KERAS='keras==2.2.2'
22+
env: TENSORFLOW='tensorflow==1.12.0' KERAS='keras==2.2.4'
3223
- os: linux
33-
python: 3.6
34-
if: branch = master
35-
# env: TENSORFLOW='tensorflow<1.9' KERAS='keras==2.2.0' # causes segmentation fault, why?
36-
env: TENSORFLOW='tensorflow<1.8' KERAS='keras==2.2.0'
37-
- os: linux
38-
python: 3.6
39-
if: branch = master
40-
env: TENSORFLOW='tensorflow<1.8' KERAS='keras==2.1.6'
41-
- os: linux
42-
python: 3.6
43-
if: branch = master
44-
# lowest supported keras version, last tensorflow release that supports CUDA 8
45-
env: TENSORFLOW='tensorflow==1.4.1' KERAS='keras==2.1.6'
46-
###########
24+
dist: bionic
25+
python: 3.7
26+
env: TENSORFLOW='tensorflow==1.14.0' KERAS='keras==2.2.4'
4727

4828
install:
4929
- pip install $TENSORFLOW $KERAS
5030
- pip install .
5131

5232
script:
53-
- pytest -v --durations=50
33+
- cd tests; pytest -v -s test*.py
34+
- cd functional; ./test_training2D_RGB.py; ./test_prediction2D_RGB.py; ./test_training3D.py; ./test_prediction3D.py

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ Our implementation is based on [CSBDEEP](http://csbdeep.bioimagecomputing.com) (
1212

1313
## Installation
1414
This implementation requires [Tensorflow](https://www.tensorflow.org/install/).
15-
We have tested Noise2Void on LinuxMint 18.3 using python 3.6 and tensorflow-gpu 1.12.0.
15+
We have tested Noise2Void on LinuxMint 19 and Ubuntu 18.0 using python 3.6 and 3.7 and tensorflow-gpu 1.12.0 and 1.14.0.
1616

1717
#### If you start from scratch...
18-
We recommend using [conda](https://docs.conda.io/en/latest/miniconda.html).
18+
We recommend using [miniconda](https://docs.conda.io/en/latest/miniconda.html).
1919
If you do not yet have a strong opinion, just use it too!
2020

2121
After installing Miniconda, the following lines might are likely the easiest way to get Tensorflow and CuDNN installed on your machine (_Note:_ Macs are not supported, and if you sit on a Windows machine all this might also require some modifications.):
2222

2323
```
2424
$ conda create -n 'n2v' python=3.6
2525
$ source activate n2v
26-
$ conda install tensorflow-gpu==1.12
26+
$ conda install tensorflow-gpu==1.14
2727
$ pip install jupyter
2828
```
2929

@@ -35,11 +35,13 @@ $ pip install n2v
3535
```
3636

3737
#### Option 2: Git-Clone and install from sources
38-
Or clone the repository:
38+
This option is ideal if you want to edit the code. Clone the repository:
39+
3940
```
4041
$ git clone https://github.com/juglab/n2v.git
4142
```
4243
Change into its directory and install it:
44+
4345
```
4446
$ cd n2v
4547
$ pip install -e .

examples/2D/denoising2D_RGB/01_training.ipynb

Lines changed: 41 additions & 52 deletions
Large diffs are not rendered by default.

examples/2D/denoising2D_RGB/02_prediction.ipynb

Lines changed: 4 additions & 7 deletions
Large diffs are not rendered by default.

examples/2D/denoising2D_SEM/01_training.ipynb

Lines changed: 34 additions & 24 deletions
Large diffs are not rendered by default.

examples/2D/denoising2D_SEM/02_prediction.ipynb

Lines changed: 19 additions & 11 deletions
Large diffs are not rendered by default.

examples/3D/01_training.ipynb

Lines changed: 42 additions & 54 deletions
Large diffs are not rendered by default.

examples/3D/02_prediction.ipynb

Lines changed: 9 additions & 9 deletions
Large diffs are not rendered by default.

n2v/models/n2v_config.py

Lines changed: 94 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import keras.backend as K
44

5+
from csbdeep.models import BaseConfig
56
from csbdeep.utils import _raise, axes_check_and_normalize, axes_dict, backend_channels_last
67

78
from six import string_types
89

910
import numpy as np
11+
from logging.config import BaseConfigurator
1012

1113
# This class is a adapted version of csbdeep.models.config.py.
1214
class N2VConfig(argparse.Namespace):
@@ -68,83 +70,87 @@ class N2VConfig(argparse.Namespace):
6870
"""
6971

7072
def __init__(self, X, **kwargs):
71-
"""See class docstring."""
72-
73-
assert len(X.shape) == 4 or len(X.shape) == 5, "Only 'SZYXC' or 'SYXC' as dimensions is supported."
74-
75-
n_dim = len(X.shape) - 2
76-
n_channel_in = X.shape[-1]
77-
n_channel_out = n_channel_in
78-
mean = np.mean(X)
79-
std = np.std(X)
80-
81-
if n_dim == 2:
82-
axes = 'SYXC'
83-
elif n_dim == 3:
84-
axes = 'SZYXC'
85-
86-
# parse and check axes
87-
axes = axes_check_and_normalize(axes)
88-
ax = axes_dict(axes)
89-
ax = {a: (ax[a] is not None) for a in ax}
90-
91-
(ax['X'] and ax['Y']) or _raise(ValueError('lateral axes X and Y must be present.'))
92-
not (ax['Z'] and ax['T']) or _raise(ValueError('using Z and T axes together not supported.'))
93-
94-
axes.startswith('S') or (not ax['S']) or _raise(ValueError('sample axis S must be first.'))
95-
axes = axes.replace('S','') # remove sample axis if it exists
96-
97-
if backend_channels_last():
98-
if ax['C']:
99-
axes[-1] == 'C' or _raise(ValueError('channel axis must be last for backend (%s).' % K.backend()))
73+
74+
# X is empty if config is None
75+
if (X.size != 0):
76+
77+
assert len(X.shape) == 4 or len(X.shape) == 5, "Only 'SZYXC' or 'SYXC' as dimensions is supported."
78+
79+
n_dim = len(X.shape) - 2
80+
n_channel_in = X.shape[-1]
81+
n_channel_out = n_channel_in
82+
mean = np.mean(X)
83+
std = np.std(X)
84+
85+
if n_dim == 2:
86+
axes = 'SYXC'
87+
elif n_dim == 3:
88+
axes = 'SZYXC'
89+
90+
# parse and check axes
91+
axes = axes_check_and_normalize(axes)
92+
ax = axes_dict(axes)
93+
ax = {a: (ax[a] is not None) for a in ax}
94+
95+
(ax['X'] and ax['Y']) or _raise(ValueError('lateral axes X and Y must be present.'))
96+
not (ax['Z'] and ax['T']) or _raise(ValueError('using Z and T axes together not supported.'))
97+
98+
axes.startswith('S') or (not ax['S']) or _raise(ValueError('sample axis S must be first.'))
99+
axes = axes.replace('S','') # remove sample axis if it exists
100+
101+
if backend_channels_last():
102+
if ax['C']:
103+
axes[-1] == 'C' or _raise(ValueError('channel axis must be last for backend (%s).' % K.backend()))
104+
else:
105+
axes += 'C'
100106
else:
101-
axes += 'C'
102-
else:
103-
if ax['C']:
104-
axes[0] == 'C' or _raise(ValueError('channel axis must be first for backend (%s).' % K.backend()))
107+
if ax['C']:
108+
axes[0] == 'C' or _raise(ValueError('channel axis must be first for backend (%s).' % K.backend()))
109+
else:
110+
axes = 'C'+axes
111+
112+
# normalization parameters
113+
self.mean = str(mean)
114+
self.std = str(std)
115+
# directly set by parameters
116+
self.n_dim = n_dim
117+
self.axes = axes
118+
self.n_channel_in = int(n_channel_in)
119+
self.n_channel_out = int(n_channel_out)
120+
121+
# default config (can be overwritten by kwargs below)
122+
self.unet_residual = False
123+
self.unet_n_depth = 2
124+
self.unet_kern_size = 5 if self.n_dim==2 else 3
125+
self.unet_n_first = 32
126+
self.unet_last_activation = 'linear'
127+
if backend_channels_last():
128+
self.unet_input_shape = self.n_dim*(None,) + (self.n_channel_in,)
105129
else:
106-
axes = 'C'+axes
107-
108-
# normalization parameters
109-
self.mean = str(mean)
110-
self.std = str(std)
111-
# directly set by parameters
112-
self.n_dim = n_dim
113-
self.axes = axes
114-
self.n_channel_in = int(n_channel_in)
115-
self.n_channel_out = int(n_channel_out)
116-
117-
# default config (can be overwritten by kwargs below)
118-
self.unet_residual = False
119-
self.unet_n_depth = 2
120-
self.unet_kern_size = 5 if self.n_dim==2 else 3
121-
self.unet_n_first = 32
122-
self.unet_last_activation = 'linear'
123-
if backend_channels_last():
124-
self.unet_input_shape = self.n_dim*(None,) + (self.n_channel_in,)
125-
else:
126-
self.unet_input_shape = (self.n_channel_in,) + self.n_dim*(None,)
127-
128-
self.train_loss = 'mae'
129-
self.train_epochs = 100
130-
self.train_steps_per_epoch = 400
131-
self.train_learning_rate = 0.0004
132-
self.train_batch_size = 16
133-
self.train_tensorboard = True
134-
self.train_checkpoint = 'weights_best.h5'
135-
self.train_reduce_lr = {'factor': 0.5, 'patience': 10}
136-
self.batch_norm = True
137-
self.n2v_perc_pix = 1.5
138-
self.n2v_patch_shape = (64, 64) if self.n_dim==2 else (64, 64, 64)
139-
self.n2v_manipulator = 'uniform_withCP'
140-
self.n2v_neighborhood_radius = 5
141-
142-
# disallow setting 'n_dim' manually
143-
try:
144-
del kwargs['n_dim']
145-
# warnings.warn("ignoring parameter 'n_dim'")
146-
except:
147-
pass
130+
self.unet_input_shape = (self.n_channel_in,) + self.n_dim*(None,)
131+
132+
self.train_loss = 'mae'
133+
self.train_epochs = 100
134+
self.train_steps_per_epoch = 400
135+
self.train_learning_rate = 0.0004
136+
self.train_batch_size = 16
137+
self.train_tensorboard = True
138+
self.train_checkpoint = 'weights_best.h5'
139+
self.train_reduce_lr = {'factor': 0.5, 'patience': 10}
140+
self.batch_norm = True
141+
self.n2v_perc_pix = 1.5
142+
self.n2v_patch_shape = (64, 64) if self.n_dim==2 else (64, 64, 64)
143+
self.n2v_manipulator = 'uniform_withCP'
144+
self.n2v_neighborhood_radius = 5
145+
146+
# disallow setting 'n_dim' manually
147+
try:
148+
del kwargs['n_dim']
149+
# warnings.warn("ignoring parameter 'n_dim'")
150+
except:
151+
pass
152+
153+
self.probabilistic = False
148154

149155
for k in kwargs:
150156
setattr(self, k, kwargs[k])
@@ -215,3 +221,16 @@ def _is_int(v,low=None,high=None):
215221
return all(ok.values()), tuple(k for (k,v) in ok.items() if not v)
216222
else:
217223
return all(ok.values())
224+
225+
def update_parameters(self, allow_new=True, **kwargs):
226+
if not allow_new:
227+
attr_new = []
228+
for k in kwargs:
229+
try:
230+
getattr(self, k)
231+
except AttributeError:
232+
attr_new.append(k)
233+
if len(attr_new) > 0:
234+
raise AttributeError("Not allowed to add new parameters (%s)" % ', '.join(attr_new))
235+
for k in kwargs:
236+
setattr(self, k, kwargs[k])

n2v/models/n2v_standard.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from csbdeep.models import CARE
2-
from csbdeep.utils import _raise, axes_check_and_normalize, axes_dict
2+
from csbdeep.utils import _raise, axes_check_and_normalize, axes_dict, load_json, save_json
33
from csbdeep.internals import nets
44
from csbdeep.internals.predict import Progress
55

@@ -64,25 +64,32 @@ class N2V(CARE):
6464
"""
6565

6666
def __init__(self, config, name=None, basedir='.'):
67-
"""See class docstring"""
68-
config is None or isinstance(config, N2VConfig) or _raise(ValueError('Invalid configuration: %s' % str(config)))
67+
"""See class docstring."""
68+
69+
config is None or isinstance(config,self._config_class) or _raise (
70+
ValueError("Invalid configuration of type '%s', was expecting type '%s'." % (type(config).__name__, self._config_class.__name__))
71+
)
6972
if config is not None and not config.is_valid():
7073
invalid_attr = config.is_valid(True)[1]
7174
raise ValueError('Invalid configuration attributes: ' + ', '.join(invalid_attr))
72-
(not (config is None and basedir is None)) or _raise(ValueError())
75+
(not (config is None and basedir is None)) or _raise(ValueError("No config provided and cannot be loaded from disk since basedir=None."))
7376

74-
name is None or isinstance(name, string_types) or _raise(ValueError())
75-
basedir is None or isinstance(basedir, (string_types, Path)) or _raise(ValueError())
77+
name is None or (isinstance(name,string_types) and len(name)>0) or _raise(ValueError("No valid name: '%s'" % str(name)))
78+
basedir is None or isinstance(basedir,(string_types,Path)) or _raise(ValueError("No valid basedir: '%s'" % str(basedir)))
7679
self.config = config
7780
self.name = name if name is not None else datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S.%f")
7881
self.basedir = Path(basedir) if basedir is not None else None
82+
if config is not None:
83+
# config was provided -> update before it is saved to disk
84+
self._update_and_check_config()
7985
self._set_logdir()
86+
if config is None:
87+
# config was loaded from disk -> update it after loading
88+
self._update_and_check_config()
8089
self._model_prepared = False
8190
self.keras_model = self._build()
8291
if config is None:
8392
self._find_and_load_weights()
84-
else:
85-
config.probabilistic = False
8693

8794

8895
def _build(self):
@@ -205,8 +212,8 @@ def train(self, X, validation_X, epochs=None, steps_per_epoch=None):
205212
self.config.train_batch_size, int(train_num_pix/100 * self.config.n2v_perc_pix),
206213
self.config.n2v_patch_shape, manipulator)
207214

208-
# validation_Y is also validation_X plus a concatinated masking channel.
209-
# To speed things up, we precomupte the masking vo the validation data.
215+
# validation_Y is also validation_X plus a concatenated masking channel.
216+
# To speed things up, we precompute the masking vo the validation data.
210217
validation_Y = np.concatenate((validation_X, np.zeros(validation_X.shape, dtype=validation_X.dtype)), axis=axes.index('C'))
211218
n2v_utils.manipulate_val_data(validation_X, validation_Y,
212219
num_pix=int(val_num_pix/100 * self.config.n2v_perc_pix),
@@ -272,13 +279,16 @@ def on_epoch_end(self, epoch, logs=None):
272279
if epoch % self.freq == 0:
273280
# TODO: implement batched calls to sess.run
274281
# (current call will likely go OOM on GPU)
282+
tensors = self.model.inputs + self.gt_outputs + self.model.sample_weights
275283
if self.model.uses_learning_phase:
276-
cut_v_data = len(self.model.inputs)
277-
val_data = [self.validation_data[0][:self.n_images]] + [0]
278-
tensors = self.model.inputs + [K.learning_phase()]
284+
tensors += [K.learning_phase()]
285+
val_data = list(v[:self.n_images] for v in self.validation_data[:-1])
286+
val_data += self.validation_data[-1:]
279287
else:
280288
val_data = list(v[:self.n_images] for v in self.validation_data)
281-
tensors = self.model.inputs
289+
# GIT issue 20: We need to remove the masking component from the validation data to prevent crash.
290+
end_index = (val_data[1].shape)[-1]//2
291+
val_data[1] = val_data[1][...,:end_index]
282292
feed_dict = dict(zip(tensors, val_data))
283293
result = self.sess.run([self.merged], feed_dict=feed_dict)
284294
summary_str = result[0]
@@ -365,3 +375,26 @@ def predict(self, img, axes, resizer=PadAndCropResizer(), n_tiles=None):
365375
pred = self._predict_mean_and_scale(normalized, axes=axes, normalizer=None, resizer=resizer, n_tiles=n_tiles)[0]
366376

367377
return self.__denormalize__(pred, mean, std)
378+
379+
def _set_logdir(self):
380+
self.logdir = self.basedir / self.name
381+
382+
config_file = self.logdir / 'config.json'
383+
if self.config is None:
384+
if config_file.exists():
385+
config_dict = load_json(str(config_file))
386+
self.config = self._config_class(np.array([]), **config_dict)
387+
if not self.config.is_valid():
388+
invalid_attr = self.config.is_valid(True)[1]
389+
raise ValueError('Invalid attributes in loaded config: ' + ', '.join(invalid_attr))
390+
else:
391+
raise FileNotFoundError("config file doesn't exist: %s" % str(config_file.resolve()))
392+
else:
393+
if self.logdir.exists():
394+
warnings.warn('output path for model already exists, files may be overwritten: %s' % str(self.logdir.resolve()))
395+
self.logdir.mkdir(parents=True, exist_ok=True)
396+
save_json(vars(self.config), str(config_file))
397+
398+
@property
399+
def _config_class(self):
400+
return N2VConfig

0 commit comments

Comments
 (0)