MilanMagik commited on
Commit
ec188ed
·
verified ·
1 Parent(s): 574aa8f

Add StarNet PyTorch source code

Browse files
StarNetPyTorch/convert_tf2_h5.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ import h5py
5
+ import numpy as np
6
+ import torch
7
+
8
+ from model import Generator
9
+
10
+
11
+ def _decode_names(values):
12
+ return [value.decode("utf-8") if isinstance(value, bytes) else str(value) for value in values]
13
+
14
+
15
+ def _read_layer_weights(layer_group):
16
+ weight_names = _decode_names(layer_group.attrs.get("weight_names", []))
17
+ weights = {}
18
+ for weight_name in weight_names:
19
+ short_name = weight_name.rsplit("/", 1)[-1].split(":", 1)[0]
20
+ try:
21
+ weights[short_name] = np.asarray(layer_group[weight_name])
22
+ except KeyError:
23
+ relative_name = weight_name.split("/", 1)[-1]
24
+ weights[short_name] = np.asarray(layer_group[relative_name])
25
+ return weights
26
+
27
+
28
+ def _load_keras_layers(h5_path):
29
+ layers = []
30
+ with h5py.File(h5_path, "r") as handle:
31
+ root = handle
32
+ if "layer_names" not in root.attrs and "model_weights" in root:
33
+ root = root["model_weights"]
34
+
35
+ layer_names = _decode_names(root.attrs.get("layer_names", []))
36
+ if not layer_names:
37
+ raise ValueError(
38
+ "Unsupported H5 layout: no Keras layer_names attribute was found. "
39
+ "Use a TensorFlow/Keras save_weights H5 file."
40
+ )
41
+
42
+ for layer_name in layer_names:
43
+ layer_group = root[layer_name]
44
+ weights = _read_layer_weights(layer_group)
45
+ if weights:
46
+ layers.append((layer_name, weights))
47
+ return layers
48
+
49
+
50
+ def _copy_conv(state, torch_name, weights, transpose=False):
51
+ required = {"kernel", "bias"}
52
+ if not required.issubset(weights):
53
+ raise ValueError("Layer %s is missing kernel or bias" % torch_name)
54
+
55
+ kernel = torch.from_numpy(weights["kernel"]).permute(3, 2, 0, 1).contiguous()
56
+ if transpose:
57
+ state[torch_name + ".weight"] = kernel
58
+ else:
59
+ state[torch_name + ".weight"] = kernel
60
+ state[torch_name + ".bias"] = torch.from_numpy(weights["bias"])
61
+
62
+
63
+ def _copy_batch_norm(state, torch_name, weights):
64
+ names = {
65
+ "weight": "gamma",
66
+ "bias": "beta",
67
+ "running_mean": "moving_mean",
68
+ "running_var": "moving_variance",
69
+ }
70
+ missing = [source for source in names.values() if source not in weights]
71
+ if missing:
72
+ raise ValueError("Layer %s is missing: %s" % (torch_name, ", ".join(missing)))
73
+ for target, source in names.items():
74
+ state[torch_name + "." + target] = torch.from_numpy(weights[source])
75
+
76
+
77
+ def convert(input_path, output_path, mode):
78
+ input_channels = 3 if mode == "RGB" else 1
79
+ layers = _load_keras_layers(input_path)
80
+
81
+ conv_layers = [weights for name, weights in layers if name.startswith("conv2d") and "transpose" not in name]
82
+ deconv_layers = [weights for name, weights in layers if name.startswith("conv2d_transpose")]
83
+ batch_norm_layers = [weights for name, weights in layers if name.startswith("batch_normalization")]
84
+
85
+ if len(conv_layers) != 8 or len(deconv_layers) != 8 or len(batch_norm_layers) != 14:
86
+ raise ValueError(
87
+ "Unexpected generator layout: found %d Conv2D, %d Conv2DTranspose, and "
88
+ "%d BatchNormalization layers; expected 8, 8, and 14."
89
+ % (len(conv_layers), len(deconv_layers), len(batch_norm_layers))
90
+ )
91
+
92
+ generator = Generator(input_channels=input_channels, batch_norm_eps=1e-3)
93
+ state = generator.state_dict()
94
+
95
+ for index, weights in enumerate(conv_layers):
96
+ _copy_conv(state, "down_convs.%d" % index, weights)
97
+ for index, weights in enumerate(deconv_layers):
98
+ _copy_conv(state, "up_convs.%d" % index, weights, transpose=True)
99
+ for index, weights in enumerate(batch_norm_layers[:7]):
100
+ _copy_batch_norm(state, "down_bns.%d" % index, weights)
101
+ for index, weights in enumerate(batch_norm_layers[7:]):
102
+ _copy_batch_norm(state, "up_bns.%d" % index, weights)
103
+
104
+ generator.load_state_dict(state)
105
+ output_path = Path(output_path)
106
+ output_path.parent.mkdir(parents=True, exist_ok=True)
107
+ torch.save(
108
+ {
109
+ "state_dict": generator.state_dict(),
110
+ "mode": mode,
111
+ "source": str(Path(input_path)),
112
+ },
113
+ output_path,
114
+ )
115
+ return output_path
116
+
117
+
118
+ def main():
119
+ parser = argparse.ArgumentParser(description="Convert StarNet TF2 Keras generator H5 weights")
120
+ parser.add_argument("input", help="Keras generator H5 file, such as weights_G_Greyscale.h5")
121
+ parser.add_argument(
122
+ "output",
123
+ nargs="?",
124
+ default=None,
125
+ help="Output .pth file; defaults to the input filename with a .pth suffix",
126
+ )
127
+ parser.add_argument("--mode", choices=("RGB", "Greyscale"), default="Greyscale")
128
+ args = parser.parse_args()
129
+
130
+ output = args.output or str(Path(args.input).with_suffix(".pth"))
131
+ output_path = convert(args.input, output, args.mode)
132
+ print("Saved PyTorch weights: %s" % output_path)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
StarNetPyTorch/convert_tf_checkpoint.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ import torch
5
+
6
+ from model import Generator
7
+
8
+
9
+ def _read_tf_checkpoint(checkpoint_path):
10
+ try:
11
+ import tensorflow as tf
12
+ except ImportError as exc:
13
+ raise RuntimeError("TensorFlow is required only for checkpoint conversion.") from exc
14
+
15
+ return tf.train.load_checkpoint(str(checkpoint_path))
16
+
17
+
18
+ def _copy_conv(reader, state, tf_name, torch_name):
19
+ state[torch_name + ".weight"] = torch.from_numpy(reader.get_tensor(tf_name + "/kernel")).permute(3, 2, 0, 1)
20
+ state[torch_name + ".bias"] = torch.from_numpy(reader.get_tensor(tf_name + "/bias"))
21
+
22
+
23
+ def _copy_deconv(reader, state, tf_name, torch_name):
24
+ state[torch_name + ".weight"] = torch.from_numpy(reader.get_tensor(tf_name + "/kernel")).permute(3, 2, 0, 1)
25
+ state[torch_name + ".bias"] = torch.from_numpy(reader.get_tensor(tf_name + "/bias"))
26
+
27
+
28
+ def _copy_bn(reader, state, tf_name, torch_name):
29
+ state[torch_name + ".weight"] = torch.from_numpy(reader.get_tensor(tf_name + "/gamma"))
30
+ state[torch_name + ".bias"] = torch.from_numpy(reader.get_tensor(tf_name + "/beta"))
31
+ state[torch_name + ".running_mean"] = torch.from_numpy(reader.get_tensor(tf_name + "/moving_mean"))
32
+ state[torch_name + ".running_var"] = torch.from_numpy(reader.get_tensor(tf_name + "/moving_variance"))
33
+
34
+
35
+ def convert(checkpoint_path, output_path):
36
+ checkpoint_path = Path(checkpoint_path)
37
+ output_path = Path(output_path)
38
+
39
+ reader = _read_tf_checkpoint(checkpoint_path)
40
+ generator = Generator()
41
+ state = generator.state_dict()
42
+
43
+ _copy_conv(reader, state, "generator/g_conv0/conv2d", "down_convs.0")
44
+ for i in range(1, 8):
45
+ _copy_conv(reader, state, "generator/g_conv%d/conv2d" % i, "down_convs.%d" % i)
46
+ _copy_bn(
47
+ reader,
48
+ state,
49
+ "generator/g_conv%d/batch_normalization" % i,
50
+ "down_bns.%d" % (i - 1),
51
+ )
52
+
53
+ for i in range(8):
54
+ _copy_deconv(reader, state, "generator/g_deconv%d/conv2d_transpose" % i, "up_convs.%d" % i)
55
+ if i < 7:
56
+ _copy_bn(
57
+ reader,
58
+ state,
59
+ "generator/g_deconv%d/batch_normalization" % i,
60
+ "up_bns.%d" % i,
61
+ )
62
+
63
+ generator.load_state_dict(state)
64
+ output_path.parent.mkdir(parents=True, exist_ok=True)
65
+ torch.save({"state_dict": generator.state_dict()}, output_path)
66
+ return output_path
67
+
68
+
69
+ def main():
70
+ parser = argparse.ArgumentParser(description="Convert StarNet TF1 generator checkpoint to PyTorch")
71
+ parser.add_argument("--checkpoint", default="model.ckpt", help="TensorFlow checkpoint prefix")
72
+ parser.add_argument("--output", default="starnet_generator.pth", help="Output PyTorch weights")
73
+ args = parser.parse_args()
74
+
75
+ output_path = convert(args.checkpoint, args.output)
76
+ print("Saved PyTorch weights to %s" % output_path)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
StarNetPyTorch/cvt.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python convert_tf2_h5.py \
2
+ /data/xch/chm_workspace2/Models/starnet_weights2/weights_D_Greyscale.h5 \
3
+ /data/xch/chm_workspace2/Models/starnet_weights2/weights_D_Greyscale.pth \
4
+ --mode Greyscale
5
+
6
+
7
+
8
+ # /data/xch/chm_workspace2/Models/starnet_weights2/weights_G_RGB.h5 \
9
+ # /data/xch/chm_workspace2/Models/starnet_weights2/weights_G_RGB.pth \
10
+
11
+ # /data/xch/chm_workspace2/Models/starnet_weights2/weights_G_Greyscale.h5 \
12
+ # /data/xch/chm_workspace2/Models/starnet_weights2/weights_G_Greyscale.pth \
13
+ # --mode Greyscale
StarNetPyTorch/model.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+
6
+ class TFBatchNorm2d(nn.Module):
7
+ """BatchNorm compatible with the original TF graph's training=True usage."""
8
+
9
+ def __init__(self, channels, eps=1e-5, momentum=0.99):
10
+ super().__init__()
11
+ self.eps = eps
12
+ self.momentum = momentum
13
+ self.weight = nn.Parameter(torch.ones(channels))
14
+ self.bias = nn.Parameter(torch.zeros(channels))
15
+ self.register_buffer("running_mean", torch.zeros(channels))
16
+ self.register_buffer("running_var", torch.ones(channels))
17
+
18
+ def forward(self, x):
19
+ mean = x.mean(dim=(0, 2, 3), keepdim=True)
20
+ var = (x - mean).pow(2).mean(dim=(0, 2, 3), keepdim=True)
21
+ if self.training:
22
+ with torch.no_grad():
23
+ self.running_mean.mul_(self.momentum).add_(
24
+ mean.reshape(-1), alpha=1.0 - self.momentum
25
+ )
26
+ self.running_var.mul_(self.momentum).add_(
27
+ var.reshape(-1), alpha=1.0 - self.momentum
28
+ )
29
+ weight = self.weight.view(1, -1, 1, 1)
30
+ bias = self.bias.view(1, -1, 1, 1)
31
+ return (x - mean) * torch.rsqrt(var + self.eps) * weight + bias
32
+
33
+
34
+ class Generator(nn.Module):
35
+ def __init__(self, input_channels=3, batch_norm_eps=1e-5):
36
+ super().__init__()
37
+ filters = [64, 128, 256, 512, 512, 512, 512, 512, 512, 512, 512, 512, 256, 128, 64]
38
+
39
+ self.down_convs = nn.ModuleList()
40
+ in_channels = input_channels
41
+ for out_channels in filters[:8]:
42
+ self.down_convs.append(
43
+ nn.Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1)
44
+ )
45
+ in_channels = out_channels
46
+
47
+ self.down_bns = nn.ModuleList(
48
+ [TFBatchNorm2d(channels, eps=batch_norm_eps) for channels in filters[1:8]]
49
+ )
50
+
51
+ self.up_convs = nn.ModuleList()
52
+ up_in_channels = [
53
+ filters[7],
54
+ filters[8] + filters[6],
55
+ filters[9] + filters[5],
56
+ filters[10] + filters[4],
57
+ filters[11] + filters[3],
58
+ filters[12] + filters[2],
59
+ filters[13] + filters[1],
60
+ filters[14] + filters[0],
61
+ ]
62
+ up_out_channels = filters[8:] + [input_channels]
63
+ for in_channels, out_channels in zip(up_in_channels, up_out_channels):
64
+ self.up_convs.append(
65
+ nn.ConvTranspose2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1)
66
+ )
67
+
68
+ self.up_bns = nn.ModuleList(
69
+ [TFBatchNorm2d(channels, eps=batch_norm_eps) for channels in filters[8:15]]
70
+ )
71
+ self.apply(_initialize_layer)
72
+
73
+ def forward(self, x):
74
+ original = x
75
+ layers = []
76
+
77
+ y = self.down_convs[0](x)
78
+ layers.append(y)
79
+
80
+ for i in range(1, 8):
81
+ y = F.leaky_relu(layers[-1], negative_slope=0.2)
82
+ y = self.down_convs[i](y)
83
+ y = self.down_bns[i - 1](y)
84
+ layers.append(y)
85
+
86
+ y = F.relu(layers[-1])
87
+ y = self.up_convs[0](y)
88
+ y = self.up_bns[0](y)
89
+ layers.append(y)
90
+
91
+ for up_index, skip_index in enumerate([6, 5, 4, 3, 2, 1], start=1):
92
+ y = torch.cat([layers[-1], layers[skip_index]], dim=1)
93
+ y = F.relu(y)
94
+ y = self.up_convs[up_index](y)
95
+ y = self.up_bns[up_index](y)
96
+ layers.append(y)
97
+
98
+ y = torch.cat([layers[-1], layers[0]], dim=1)
99
+ y = F.relu(y)
100
+ y = self.up_convs[7](y)
101
+ y = F.relu(y)
102
+ return original - y
103
+
104
+
105
+ class Discriminator(nn.Module):
106
+ def __init__(self, input_channels=3, batch_norm_eps=1e-3):
107
+ super().__init__()
108
+ filters = [32, 64, 64, 128, 128, 256, 256, 256, 8]
109
+ strides = [1, 2, 1, 2, 1, 2, 1, 2, 2]
110
+
111
+ self.convs = nn.ModuleList()
112
+ in_channels = input_channels
113
+ for index, (out_channels, stride) in enumerate(zip(filters, strides)):
114
+ padding = 1 if stride == 1 else 0
115
+ self.convs.append(
116
+ nn.Conv2d(
117
+ in_channels,
118
+ out_channels,
119
+ kernel_size=3,
120
+ stride=stride,
121
+ padding=padding,
122
+ )
123
+ )
124
+ in_channels = out_channels
125
+
126
+ self.batch_norms = nn.ModuleList(
127
+ [TFBatchNorm2d(channels, eps=batch_norm_eps) for channels in filters[1:]]
128
+ )
129
+ self.dense = nn.Conv2d(filters[-1], 1, kernel_size=1)
130
+ self.apply(_initialize_layer)
131
+
132
+ def forward(self, x):
133
+ features = []
134
+
135
+ y = F.leaky_relu(self.convs[0](x), negative_slope=0.2)
136
+ features.append(y)
137
+
138
+ for index in range(1, len(self.convs)):
139
+ y = self.convs[index](features[-1])
140
+ y = self.batch_norms[index - 1](y)
141
+ y = F.leaky_relu(y, negative_slope=0.2)
142
+ features.append(y)
143
+
144
+ prediction = torch.sigmoid(self.dense(features[-1]))
145
+ return (*features[:8], prediction)
146
+
147
+
148
+ def _initialize_layer(module):
149
+ if isinstance(module, (nn.Conv2d, nn.ConvTranspose2d)):
150
+ nn.init.xavier_uniform_(module.weight)
151
+ if module.bias is not None:
152
+ nn.init.zeros_(module.bias)
153
+
154
+
155
+ def create_generator(
156
+ weights_path=None,
157
+ device=None,
158
+ input_channels=3,
159
+ batch_norm_eps=1e-5,
160
+ ):
161
+ device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
162
+ net = Generator(
163
+ input_channels=input_channels,
164
+ batch_norm_eps=batch_norm_eps,
165
+ ).to(device)
166
+ if weights_path:
167
+ checkpoint = torch.load(weights_path, map_location=device)
168
+ state_dict = checkpoint.get("state_dict", checkpoint)
169
+ net.load_state_dict(state_dict)
170
+ net.eval()
171
+ return net
172
+
173
+
174
+ def create_discriminator(
175
+ weights_path=None,
176
+ device=None,
177
+ input_channels=3,
178
+ batch_norm_eps=1e-3,
179
+ ):
180
+ device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
181
+ net = Discriminator(
182
+ input_channels=input_channels,
183
+ batch_norm_eps=batch_norm_eps,
184
+ ).to(device)
185
+ if weights_path:
186
+ checkpoint = torch.load(weights_path, map_location=device)
187
+ state_dict = checkpoint.get("state_dict", checkpoint)
188
+ net.load_state_dict(state_dict)
189
+ net.eval()
190
+ return net
StarNetPyTorch/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ numpy>=1.24
2
+ tifffile>=2023.7
3
+ torch>=2.1
4
+ matplotlib>=3.7
5
+
StarNetPyTorch/run ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+
StarNetPyTorch/run.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python starnet_v1_pytorch_transform.py \
2
+ rgb_test5.tif \
3
+ rgb_test5_starless.tif \
4
+ --weights ../starnet_weights2/weights \
5
+ --mode RGB \
6
+ --window-size 512 \
7
+ --stride 128 \
8
+ --device cuda
9
+
10
+
11
+
StarNetPyTorch/starnet.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import time
3
+
4
+ import transform as transform_module
5
+
6
+
7
+ epochs = 10000
8
+ batch = 1
9
+ steps = 1000
10
+ output_freq = 100
11
+ verbose = True
12
+ images = True
13
+ log_freq = 50
14
+ gen_plots = True
15
+ learning_rates = [0.000002, 0.0000005]
16
+ stride = 64
17
+
18
+
19
+ def main():
20
+ parser = argparse.ArgumentParser(description="PyTorch StarNet transform entry point")
21
+ subparsers = parser.add_subparsers(dest="command")
22
+
23
+ transform_parser = subparsers.add_parser("transform", help="Remove stars from a TIFF image")
24
+ transform_parser.add_argument("image", help="Input TIFF image")
25
+ transform_parser.add_argument("--stride", type=int, default=stride, help="Sliding window stride")
26
+ transform_parser.add_argument("--weights", default=None, help="Path to starnet_generator.pth")
27
+ transform_parser.add_argument("--device", default=None, help="Torch device, for example cuda or cpu")
28
+
29
+ args = parser.parse_args()
30
+ if args.command == "transform":
31
+ start = time.time()
32
+ transform_module.transform(
33
+ imageName=args.image,
34
+ stride=args.stride,
35
+ weights_path=args.weights,
36
+ device=args.device,
37
+ )
38
+ stop = time.time()
39
+ minutes = float((stop - start) / 60)
40
+ if minutes > 60.0:
41
+ print("Total time taken: %.1f hours" % (minutes / 60))
42
+ else:
43
+ print("Total time taken: %.1f minutes" % minutes)
44
+ else:
45
+ parser.print_help()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
StarNetPyTorch/starnet_v1_pytorch.py ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+ from pathlib import Path
3
+
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ import tifffile as tiff
7
+ import torch
8
+ import torch.nn.functional as F
9
+
10
+ from model import Discriminator, Generator
11
+
12
+
13
+ class StarNet:
14
+ def __init__(
15
+ self,
16
+ mode,
17
+ window_size=512,
18
+ stride=256,
19
+ lr=1e-4,
20
+ train_folder="./train/",
21
+ batch_size=1,
22
+ device=None,
23
+ ):
24
+ if mode not in ("RGB", "Greyscale"):
25
+ raise ValueError("Mode should be either RGB or Greyscale")
26
+ if window_size % 256 != 0:
27
+ raise ValueError("window_size must be divisible by 256")
28
+ if stride <= 0 or stride > window_size:
29
+ raise ValueError("stride must be greater than zero and no larger than window_size")
30
+ if (window_size - stride) % 2 != 0:
31
+ raise ValueError("window_size - stride must be even")
32
+
33
+ self.mode = mode
34
+ self.input_channels = 3 if mode == "RGB" else 1
35
+ self.window_size = window_size
36
+ self.stride = stride
37
+ self.lr = lr
38
+ self.train_folder = Path(train_folder)
39
+ self.batch_size = batch_size
40
+ self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
41
+
42
+ self.history = {}
43
+ self._ema = 0.9999
44
+ self.weights = []
45
+ self.original = []
46
+ self.starless = []
47
+ self.iters_per_epoch = 0
48
+
49
+ self.G = None
50
+ self.D = None
51
+ self.gen_optimizer = None
52
+ self.dis_optimizer = None
53
+
54
+ def __str__(self):
55
+ return "StarNet PyTorch instance"
56
+
57
+ def load_training_dataset(self):
58
+ original_dir = self.train_folder / "original"
59
+ starless_dir = self.train_folder / "starless"
60
+ original_files = sorted(path.name for path in original_dir.glob("*.tif"))
61
+ starless_files = sorted(path.name for path in starless_dir.glob("*.tif"))
62
+
63
+ if not original_files or not starless_files:
64
+ raise ValueError("No training data found in %s" % self.train_folder)
65
+ if original_files != starless_files:
66
+ raise ValueError(
67
+ "Corresponding names in the original and starless folders must be equal"
68
+ )
69
+
70
+ print("Total training images found: %d" % len(original_files))
71
+ self.original = [tiff.imread(original_dir / name) for name in original_files]
72
+ self.starless = [tiff.imread(starless_dir / name) for name in starless_files]
73
+ self.weights = []
74
+ total_pixels = 0
75
+
76
+ for name, original, starless in zip(original_files, self.original, self.starless):
77
+ if original.shape != starless.shape:
78
+ raise ValueError("Image sizes are not equal for %s" % name)
79
+ if original.shape[0] < self.window_size or original.shape[1] < self.window_size:
80
+ raise ValueError(
81
+ "%s is smaller than the configured window size %d"
82
+ % (name, self.window_size)
83
+ )
84
+ pixels = original.shape[0] * original.shape[1]
85
+ total_pixels += pixels
86
+ self.weights.append(pixels)
87
+
88
+ self.iters_per_epoch = max(1, total_pixels // (self.window_size * self.window_size))
89
+ self.weights = np.asarray(self.weights, dtype=np.float64)
90
+ self.weights /= self.weights.sum()
91
+
92
+ print("Total size of training images: %.2f MP" % (total_pixels / 1e6))
93
+ print("One epoch is set to %d iterations" % self.iters_per_epoch)
94
+ print("Training dataset has been successfully loaded!")
95
+
96
+ def _generator(self, m=64):
97
+ if m != 64:
98
+ raise ValueError("The translated architecture supports m=64")
99
+ return Generator(input_channels=self.input_channels, batch_norm_eps=1e-3)
100
+
101
+ def _discriminator(self):
102
+ return Discriminator(input_channels=self.input_channels, batch_norm_eps=1e-3)
103
+
104
+ def _weight_paths(self, weights):
105
+ path = Path(weights)
106
+
107
+ if path.suffix.lower() in (".pth", ".pt"):
108
+ return path, None
109
+ return (
110
+ Path(str(path) + "_G_" + self.mode + ".pth"),
111
+ Path(str(path) + "_D_" + self.mode + ".pth"),
112
+ )
113
+
114
+ @staticmethod
115
+ def _load_state(module, path, device):
116
+ try:
117
+ checkpoint = torch.load(path, map_location=device, weights_only=True)
118
+ except TypeError: # PyTorch < 2.0 compatibility
119
+ checkpoint = torch.load(path, map_location=device)
120
+ state_dict = checkpoint.get("state_dict", checkpoint)
121
+ module.load_state_dict(state_dict)
122
+
123
+ def load_model(self, weights=None, history=None):
124
+ self.G = self._generator().to(self.device)
125
+ self.D = self._discriminator().to(self.device)
126
+ self.gen_optimizer = torch.optim.Adam(self.G.parameters(), lr=self.lr)
127
+ self.dis_optimizer = torch.optim.Adam(self.D.parameters(), lr=self.lr / 4)
128
+
129
+ if weights:
130
+ generator_path, discriminator_path = self._weight_paths(weights)
131
+ if not generator_path.exists():
132
+ raise FileNotFoundError("Generator weights were not found at %s" % generator_path)
133
+ self._load_state(self.G, generator_path, self.device)
134
+
135
+ if discriminator_path and discriminator_path.exists():
136
+ self._load_state(self.D, discriminator_path, self.device)
137
+ elif discriminator_path:
138
+ print("Discriminator weights were not found; using initialized weights.")
139
+
140
+ if history:
141
+ history_path = Path(str(history) + "_" + self.mode + ".pkl")
142
+ with history_path.open("rb") as handle:
143
+ self.history = pickle.load(handle)
144
+
145
+ self.G.eval()
146
+ self.D.eval()
147
+ return self.G
148
+
149
+ def initialize_model(self):
150
+ return self.load_model()
151
+
152
+ @staticmethod
153
+ def _ramp(x):
154
+ if torch.is_tensor(x):
155
+ return torch.clamp(x, 0.0, 1.0)
156
+ return np.clip(x, 0.0, 1.0)
157
+
158
+ @staticmethod
159
+ def _image_scale(image):
160
+ if image.dtype == np.uint8:
161
+ return 255.0
162
+ if image.dtype == np.uint16:
163
+ return 65535.0
164
+ if np.issubdtype(image.dtype, np.floating):
165
+ return 1.0
166
+ raise ValueError("Unsupported training image dtype: %s" % image.dtype)
167
+
168
+ def _get_sample(self, r, h, w, type):
169
+ if type not in ("original", "starless"):
170
+ raise ValueError("type must be original or starless")
171
+ source = self.original if type == "original" else self.starless
172
+ sample = source[r][h:h + self.window_size, w:w + self.window_size]
173
+ return sample.astype(np.float32) / self._image_scale(sample)
174
+
175
+ def _augmentator(self, original, starless):
176
+ original = np.array(original, copy=True)
177
+ starless = np.array(starless, copy=True)
178
+
179
+ if np.random.rand() < 0.5:
180
+ original = np.flip(original, axis=1)
181
+ starless = np.flip(starless, axis=1)
182
+ if np.random.rand() < 0.5:
183
+ original = np.flip(original, axis=0)
184
+ starless = np.flip(starless, axis=0)
185
+ if np.random.rand() < 0.5:
186
+ rotations = np.random.randint(1, 4)
187
+ original = np.rot90(original, rotations, axes=(1, 0))
188
+ starless = np.rot90(starless, rotations, axes=(1, 0))
189
+
190
+ if self.mode == "RGB":
191
+ if original.ndim != 3 or original.shape[2] < 3:
192
+ raise ValueError("RGB training images must contain at least three channels")
193
+ original = original[:, :, :3]
194
+ starless = starless[:, :, :3]
195
+ if np.random.rand() < 0.7:
196
+ channel = np.random.randint(3)
197
+ minimum = min(float(original.min()), float(starless.min()))
198
+ offset = np.random.rand() * 0.25 - np.random.rand() * minimum
199
+ original[:, :, channel] += offset * (1.0 - original[:, :, channel])
200
+ starless[:, :, channel] += offset * (1.0 - starless[:, :, channel])
201
+ if np.random.rand() < 0.7:
202
+ sequence = np.random.permutation(3)
203
+ original = original[:, :, sequence]
204
+ starless = starless[:, :, sequence]
205
+ else:
206
+ if np.random.rand() < 0.7:
207
+ minimum = min(float(original.min()), float(starless.min()))
208
+ offset = np.random.rand() * 0.25 - np.random.rand() * minimum
209
+ original += offset * (1.0 - original)
210
+ starless += offset * (1.0 - starless)
211
+ if original.ndim == 3:
212
+ channel = np.random.randint(min(3, original.shape[2]))
213
+ original = original[:, :, channel]
214
+ starless = starless[:, :, channel]
215
+ original = original[:, :, None]
216
+ starless = starless[:, :, None]
217
+
218
+ return (
219
+ np.ascontiguousarray(np.clip(original, 0.0, 1.0)),
220
+ np.ascontiguousarray(np.clip(starless, 0.0, 1.0)),
221
+ )
222
+
223
+ def generate_input(self, iterations=1, augmentation=False):
224
+ if not self.original:
225
+ raise RuntimeError("Training dataset was not loaded")
226
+
227
+ original_batch = None
228
+ starless_batch = None
229
+ for _ in range(iterations):
230
+ original_batch = np.zeros(
231
+ (
232
+ self.batch_size,
233
+ self.window_size,
234
+ self.window_size,
235
+ self.input_channels,
236
+ ),
237
+ dtype=np.float32,
238
+ )
239
+ starless_batch = np.zeros_like(original_batch)
240
+
241
+ for index in range(self.batch_size):
242
+ image_index = int(
243
+ np.random.choice(len(self.original), p=np.asarray(self.weights))
244
+ )
245
+ image_h, image_w = self.original[image_index].shape[:2]
246
+ top = np.random.randint(0, image_h - self.window_size + 1)
247
+ left = np.random.randint(0, image_w - self.window_size + 1)
248
+ original = self._get_sample(image_index, top, left, "original")
249
+ starless = self._get_sample(image_index, top, left, "starless")
250
+
251
+ if augmentation:
252
+ original, starless = self._augmentator(original, starless)
253
+ elif self.mode == "Greyscale":
254
+ if original.ndim == 3:
255
+ channel = np.random.randint(min(3, original.shape[2]))
256
+ original = original[:, :, channel]
257
+ starless = starless[:, :, channel]
258
+ original = original[:, :, None]
259
+ starless = starless[:, :, None]
260
+ else:
261
+ original = original[:, :, :3]
262
+ starless = starless[:, :, :3]
263
+
264
+ original_batch[index] = original
265
+ starless_batch[index] = starless
266
+
267
+ return original_batch, starless_batch
268
+
269
+ @staticmethod
270
+ def _set_requires_grad(module, enabled):
271
+ for parameter in module.parameters():
272
+ parameter.requires_grad_(enabled)
273
+
274
+ def _losses(self, original, target):
275
+ generated = self.G(original)
276
+ real_outputs = self.D(target)
277
+ fake_outputs = self.D(generated)
278
+
279
+ real_features = real_outputs[:-1]
280
+ fake_features = fake_outputs[:-1]
281
+ predict_real = real_outputs[-1]
282
+ predict_fake = fake_outputs[-1]
283
+
284
+ dis_loss = -(
285
+ torch.log(predict_real + 1e-8) + torch.log(1.0 - predict_fake + 1e-8)
286
+ ).mean()
287
+ gen_loss_gan = -torch.log(predict_fake + 1e-8).mean()
288
+ perceptual = [
289
+ torch.mean(torch.abs(fake - real))
290
+ for fake, real in zip(fake_features, real_features)
291
+ ]
292
+ gen_l1 = F.l1_loss(generated, target)
293
+ gen_loss = (
294
+ gen_loss_gan * 0.1
295
+ + perceptual[0] * 0.1
296
+ + sum(loss * 10.0 for loss in perceptual[1:])
297
+ + gen_l1 * 100.0
298
+ )
299
+
300
+ metrics = {
301
+ "dis_loss": dis_loss,
302
+ "gen_loss_GAN": gen_loss_gan,
303
+ "gen_p1": perceptual[0],
304
+ "gen_p2": perceptual[1],
305
+ "gen_p3": perceptual[2],
306
+ "gen_p4": perceptual[3],
307
+ "gen_p5": perceptual[4],
308
+ "gen_p6": perceptual[5],
309
+ "gen_p7": perceptual[6],
310
+ "gen_p8": perceptual[7],
311
+ "gen_L1": gen_l1 * 100.0,
312
+ "total": gen_loss,
313
+ }
314
+ return gen_loss, dis_loss, metrics
315
+
316
+ def _train_step(self, original, target):
317
+ self.G.train()
318
+ self.D.train()
319
+ self.gen_optimizer.zero_grad(set_to_none=True)
320
+ self.dis_optimizer.zero_grad(set_to_none=True)
321
+
322
+ gen_loss, dis_loss, metrics = self._losses(original, target)
323
+ generator_gradients = torch.autograd.grad(
324
+ gen_loss,
325
+ tuple(self.G.parameters()),
326
+ retain_graph=True,
327
+ )
328
+ discriminator_gradients = torch.autograd.grad(
329
+ dis_loss,
330
+ tuple(self.D.parameters()),
331
+ )
332
+
333
+ for parameter, gradient in zip(self.G.parameters(), generator_gradients):
334
+ parameter.grad = gradient
335
+ for parameter, gradient in zip(self.D.parameters(), discriminator_gradients):
336
+ parameter.grad = gradient
337
+
338
+ self.gen_optimizer.step()
339
+ self.dis_optimizer.step()
340
+ return {name: float(value.detach().cpu()) for name, value in metrics.items()}
341
+
342
+ def _update_history(self, metrics):
343
+ for name, value in metrics.items():
344
+ if name in self.history and self.history[name]:
345
+ value = value * (1.0 - self._ema) + self.history[name][-1] * self._ema
346
+ self.history[name].append(value)
347
+ else:
348
+ self.history[name] = [value]
349
+
350
+ def _plot_progress(self, original, target):
351
+ self.G.eval()
352
+ with torch.inference_mode():
353
+ generated = self.G(original)
354
+
355
+ original = ((original[0] + 1.0) / 2.0).detach().cpu().numpy()
356
+ generated = ((generated[0] + 1.0) / 2.0).detach().cpu().numpy()
357
+ target = ((target[0] + 1.0) / 2.0).detach().cpu().numpy()
358
+
359
+ plt.close()
360
+ _, axes = plt.subplots(1, 3, sharex=True, figsize=(16.5, 5.5))
361
+ titles = ("Original", "Starless", "Target")
362
+ for axis, title, image in zip(axes, titles, (original, generated, target)):
363
+ if self.mode == "RGB":
364
+ axis.imshow(np.transpose(image, (1, 2, 0)))
365
+ else:
366
+ axis.imshow(image[0], cmap="gray", vmin=0, vmax=1)
367
+ axis.set_title(title)
368
+ plt.pause(0.001)
369
+ self.G.train()
370
+
371
+ def train(
372
+ self,
373
+ epochs,
374
+ augmentation=True,
375
+ plot_progress=False,
376
+ plot_interval=50,
377
+ save_backups=True,
378
+ warm_up=False,
379
+ ):
380
+ if not self.original:
381
+ raise RuntimeError("Training dataset was not loaded; call load_training_dataset()")
382
+ if self.G is None or self.D is None:
383
+ self.initialize_model()
384
+
385
+ for epoch in range(epochs):
386
+ for iteration in range(self.iters_per_epoch):
387
+ original, target = self.generate_input(augmentation=augmentation)
388
+ original = torch.from_numpy(original.transpose(0, 3, 1, 2)).to(self.device)
389
+ target = torch.from_numpy(target.transpose(0, 3, 1, 2)).to(self.device)
390
+ original = original * 2.0 - 1.0
391
+ target = target * 2.0 - 1.0
392
+ if warm_up:
393
+ target = original
394
+
395
+ if plot_progress and iteration % plot_interval == 0:
396
+ self._plot_progress(original, target)
397
+
398
+ metrics = self._train_step(original, target)
399
+ self._update_history(metrics)
400
+ print(
401
+ "\rEpoch: %d. Iteration %d / %d Loss %f "
402
+ % (epoch, iteration, self.iters_per_epoch, self.history["total"][-1]),
403
+ end="",
404
+ )
405
+
406
+ print()
407
+ if save_backups:
408
+ suffix = "even" if epoch % 2 == 0 else "odd"
409
+ self._save_network(self.G, "./starnet_backup_G_%s.pth" % suffix)
410
+ self._save_network(self.D, "./starnet_backup_D_%s.pth" % suffix)
411
+
412
+ if plot_progress:
413
+ plt.close()
414
+ self.G.eval()
415
+ self.D.eval()
416
+ return self.history
417
+
418
+ def plot_history(self, last=None):
419
+ if not self.history:
420
+ raise RuntimeError("Empty training history, nothing to plot")
421
+
422
+ keys = list(self.history.keys())
423
+ columns = 3
424
+ rows = int(np.ceil(len(keys) / columns))
425
+ figure, axes = plt.subplots(rows, columns, sharex=True, figsize=(16, 3.5 * rows))
426
+ axes = np.atleast_1d(axes).reshape(-1)
427
+ for axis, key in zip(axes, keys):
428
+ values = self.history[key][-last:] if last else self.history[key]
429
+ axis.plot(values)
430
+ axis.set_title(key)
431
+ for axis in axes[len(keys):]:
432
+ axis.set_visible(False)
433
+ figure.tight_layout()
434
+ return figure, axes
435
+
436
+ @staticmethod
437
+ def _save_network(network, path):
438
+ path = Path(path)
439
+ path.parent.mkdir(parents=True, exist_ok=True)
440
+ torch.save({"state_dict": network.state_dict()}, path)
441
+
442
+ def save_model(self, weights_filename, history_filename=None):
443
+ if self.G is None or self.D is None:
444
+ raise RuntimeError("Model has not been initialized")
445
+
446
+ generator_path = Path(str(weights_filename) + "_G_" + self.mode + ".pth")
447
+ discriminator_path = Path(str(weights_filename) + "_D_" + self.mode + ".pth")
448
+ self._save_network(self.G, generator_path)
449
+ self._save_network(self.D, discriminator_path)
450
+
451
+ if history_filename:
452
+ history_path = Path(str(history_filename) + "_" + self.mode + ".pkl")
453
+ history_path.parent.mkdir(parents=True, exist_ok=True)
454
+ with history_path.open("wb") as handle:
455
+ pickle.dump(self.history, handle)
456
+ return generator_path, discriminator_path
457
+
458
+ def _read_image(self, input_path):
459
+ data = tiff.imread(input_path)
460
+ if data.ndim > 3:
461
+ layer = input("Tiff has %d layers, please enter layer to process: " % data.shape[0])
462
+ data = data[int(layer)]
463
+
464
+ if data.dtype == np.dtype("uint16"):
465
+ scale = 65535.0
466
+ elif data.dtype == np.dtype("uint8"):
467
+ scale = 255.0
468
+ else:
469
+ raise ValueError("Unknown image dtype: %s" % data.dtype)
470
+
471
+ image = (data / scale).astype(np.float32)
472
+ if self.mode == "Greyscale":
473
+ if image.ndim != 2:
474
+ raise ValueError("You loaded Greyscale model, but the image is RGB")
475
+ image = image[:, :, None]
476
+ else:
477
+ if image.ndim != 3:
478
+ raise ValueError("You loaded RGB model, but the image is Greyscale")
479
+ if image.shape[2] == 4:
480
+ print("Input image has 4 channels. Removing Alpha-Channel")
481
+ image = image[:, :, :3]
482
+ if image.shape[2] != 3:
483
+ raise ValueError("RGB input must have 3 or 4 channels")
484
+ return image, data.dtype, scale
485
+
486
+ @staticmethod
487
+ def _imwrite(path, image):
488
+ if hasattr(tiff, "imwrite"):
489
+ tiff.imwrite(path, image)
490
+ else:
491
+ tiff.imsave(path, image)
492
+
493
+ def transform(self, in_name, out_name):
494
+ if self.G is None:
495
+ raise RuntimeError("Call load_model() before transform()")
496
+
497
+ self.G.eval()
498
+ image, input_dtype, scale = self._read_image(in_name)
499
+ offset = (self.window_size - self.stride) // 2
500
+ h, w, _ = image.shape
501
+ ith = h // self.stride + 1
502
+ itw = w // self.stride + 1
503
+ dh = ith * self.stride - h
504
+ dw = itw * self.stride - w
505
+
506
+ image = np.concatenate((image, image[(h - dh):, :, :]), axis=0)
507
+ image = np.concatenate((image, image[:, (w - dw):, :]), axis=1)
508
+ padded_h, padded_w, _ = image.shape
509
+ image = np.concatenate((image, image[(padded_h - offset):, :, :]), axis=0)
510
+ image = np.concatenate((image[:offset, :, :], image), axis=0)
511
+ image = np.concatenate((image, image[:, (padded_w - offset):, :]), axis=1)
512
+ image = np.concatenate((image[:, :offset, :], image), axis=1)
513
+ image = image * 2.0 - 1.0
514
+ output = np.empty_like(image)
515
+
516
+ with torch.inference_mode():
517
+ for i in range(ith):
518
+ for j in range(itw):
519
+ x = self.stride * i
520
+ y = self.stride * j
521
+ tile = image[
522
+ x:x + self.window_size,
523
+ y:y + self.window_size,
524
+ :,
525
+ ]
526
+ tile = (
527
+ torch.from_numpy(tile.transpose(2, 0, 1))
528
+ .unsqueeze(0)
529
+ .to(self.device)
530
+ )
531
+ result = self.G(tile)
532
+ result = ((result[0] + 1.0) / 2.0).cpu().numpy().transpose(1, 2, 0)
533
+ output[
534
+ x + offset:x + offset + self.stride,
535
+ y + offset:y + offset + self.stride,
536
+ :,
537
+ ] = result[
538
+ offset:offset + self.stride,
539
+ offset:offset + self.stride,
540
+ :,
541
+ ]
542
+ progress = int((itw * i + j + 1) * 100 / (ith * itw))
543
+ print("Transforming input image... %d%%\r" % progress, end="")
544
+
545
+ print("Transforming input image... Done!")
546
+ output = np.clip(output, 0.0, 1.0)
547
+ output = output[offset:offset + h, offset:offset + w, :]
548
+ if self.mode == "Greyscale":
549
+ output = output[:, :, 0]
550
+
551
+ output_path = Path(out_name)
552
+ output_path.parent.mkdir(parents=True, exist_ok=True)
553
+ self._imwrite(output_path, (output * scale).astype(input_dtype))
554
+ return output_path
StarNetPyTorch/starnet_v1_pytorch_transform.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from starnet_v1_pytorch import StarNet
5
+
6
+
7
+ def main():
8
+
9
+ input_default = "rgb_test5.tif"
10
+ output_default = "rgb_test5_starless.tif"
11
+ weights_default = str(Path(__file__).resolve().parent / "models" / "weights")
12
+ mode_default = "RGB"
13
+
14
+ parser = argparse.ArgumentParser(description="PyTorch version of the TF2 StarNet transform demo")
15
+ parser.add_argument("input", nargs="?", default=input_default, help="Input TIFF image")
16
+ parser.add_argument(
17
+ "output",
18
+ nargs="?",
19
+ default=output_default,
20
+ help="Output TIFF image",
21
+ )
22
+ parser.add_argument(
23
+ "--weights",
24
+ default=weights_default,
25
+ help="Weight prefix or a direct .pth file path",
26
+ )
27
+ parser.add_argument("--mode", choices=("RGB", "Greyscale"), default=mode_default)
28
+ parser.add_argument("--window-size", type=int, default=512)
29
+ parser.add_argument("--stride", type=int, default=128)
30
+ parser.add_argument(
31
+ "--device",
32
+ default=None,
33
+ help="Torch device such as cuda, cuda:0, or cpu (default: auto-detect)",
34
+ )
35
+ args = parser.parse_args()
36
+
37
+ starnet = StarNet(
38
+ mode=args.mode,
39
+ window_size=args.window_size,
40
+ stride=args.stride,
41
+ device=args.device,
42
+ )
43
+ weights_path = starnet.load_model(args.weights)
44
+ print("Loaded weights: %s" % weights_path)
45
+
46
+ output_path = starnet.transform(args.input, args.output)
47
+ print("Saved result: %s" % Path(output_path).resolve())
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
StarNetPyTorch/transform.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+ import tifffile as tiff
5
+ import torch
6
+
7
+ from model import create_generator
8
+
9
+
10
+ WINDOW_SIZE = 256
11
+ DEFAULT_STRIDE = 64
12
+
13
+
14
+ def _read_tiff_image(image_name):
15
+ data = tiff.imread(image_name)
16
+ if len(data.shape) > 3:
17
+ layer = input("Tiff has %d layers, please enter layer to process: " % data.shape[0])
18
+ data = data[int(layer)]
19
+
20
+ input_dtype = data.dtype
21
+ if input_dtype == np.dtype("uint16"):
22
+ image = (data / 65535.0).astype("float32")
23
+ elif input_dtype == np.dtype("uint8"):
24
+ image = (data / 255.0).astype("float32")
25
+ else:
26
+ raise ValueError("Unknown image dtype: %s" % data.dtype)
27
+
28
+ if image.ndim != 3 or image.shape[2] < 3:
29
+ raise ValueError("Input image must be an RGB/RGBA TIFF image")
30
+
31
+ if image.shape[2] == 4:
32
+ print("Input image has 4 channels. Removing Alpha-Channel")
33
+ image = image[:, :, [0, 1, 2]]
34
+
35
+ return image, input_dtype
36
+
37
+
38
+ def _default_weights_path():
39
+ candidates = [
40
+ Path.cwd() / "starnet_generator.pth",
41
+ Path(__file__).resolve().parent / "starnet_generator.pth",
42
+ ]
43
+ for candidate in candidates:
44
+ if candidate.exists():
45
+ return candidate
46
+ return candidates[0]
47
+
48
+
49
+ def _save_tiff(path, array):
50
+ if hasattr(tiff, "imwrite"):
51
+ tiff.imwrite(path, array)
52
+ else:
53
+ tiff.imsave(path, array)
54
+
55
+
56
+ def transform(imageName, stride=DEFAULT_STRIDE, weights_path=None, device=None):
57
+ weights_path = Path(weights_path) if weights_path else _default_weights_path()
58
+ if not weights_path.exists():
59
+ raise FileNotFoundError(
60
+ "PyTorch weights were not found at %s. Convert a TensorFlow checkpoint first "
61
+ "with: python convert_tf_checkpoint.py --checkpoint ../starnet/model.ckpt "
62
+ "--output starnet_generator.pth" % weights_path
63
+ )
64
+
65
+ device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
66
+ print("Loading PyTorch model from %s..." % weights_path)
67
+ net = create_generator(weights_path=weights_path, device=device)
68
+ print("Done!")
69
+
70
+ print("Opening input image...")
71
+ image, input_dtype = _read_tiff_image(imageName)
72
+
73
+ offset = int((WINDOW_SIZE - stride) / 2)
74
+ h, w, _ = image.shape
75
+ ith = int(h / stride) + 1
76
+ itw = int(w / stride) + 1
77
+ dh = ith * stride - h
78
+ dw = itw * stride - w
79
+
80
+ image = np.concatenate((image, image[(h - dh):, :, :]), axis=0)
81
+ image = np.concatenate((image, image[:, (w - dw):, :]), axis=1)
82
+
83
+ h, w, _ = image.shape
84
+ image = np.concatenate((image, image[(h - offset):, :, :]), axis=0)
85
+ image = np.concatenate((image[:offset, :, :], image), axis=0)
86
+ image = np.concatenate((image, image[:, (w - offset):, :]), axis=1)
87
+ image = np.concatenate((image[:, :offset, :], image), axis=1)
88
+
89
+ output = np.copy(image)
90
+
91
+ with torch.inference_mode():
92
+ for i in range(ith):
93
+ for j in range(itw):
94
+ progress = int((itw * i + j + 1) * 100 / (ith * itw))
95
+ print("Transforming input image... %d%%\r" % progress, end="")
96
+
97
+ x = stride * i
98
+ y = stride * j
99
+ patch = image[x:x + WINDOW_SIZE, y:y + WINDOW_SIZE, :]
100
+ patch_tensor = torch.from_numpy(patch.transpose(2, 0, 1)).unsqueeze(0).to(device)
101
+
102
+ result = net(patch_tensor).squeeze(0).detach().cpu().numpy().transpose(1, 2, 0)
103
+ output[
104
+ x + offset:x + stride + offset,
105
+ y + offset:y + stride + offset,
106
+ :,
107
+ ] = result[offset:stride + offset, offset:stride + offset, :]
108
+
109
+ print("Transforming input image... Done!")
110
+
111
+ output = np.clip(output, 0, 1)
112
+ output = output[offset:-(offset + dh), offset:-(offset + dw), :]
113
+
114
+ output_path = Path(str(imageName) + "_starless.tif")
115
+ print("Saving output image...")
116
+ if input_dtype == np.dtype("uint8"):
117
+ _save_tiff(output_path, (output * 255).astype("uint8"))
118
+ else:
119
+ _save_tiff(output_path, (output * 65535).astype("uint16"))
120
+ print("Done!")
121
+ return output_path