├── README.md
├── assets
└── unet.png
├── dataset
├── psf_0.fits
├── psf_1.fits
├── psf_2.fits
├── psf_3.fits
└── psf_4.fits
├── examples
├── data.ipynb
├── evaluation.ipynb
└── training.ipynb
└── src
├── NVCC_monitoring.py
├── algorithms
├── Gerchberg–Saxton.py
├── Input-Output.py
├── animation.py
└── utils.py
├── generation
├── generator.py
├── plots.py
├── psf.yaml
└── radial.py
├── processing
├── plot3D.py
└── zoom.py
└── pytorch
├── criterion.py
├── dataset.py
├── lr_analyzer.py
├── models
├── Densenet.py
├── InceptionV3.py
├── Resnet.py
├── Unet.py
├── Unet_PP.py
├── VGG.py
└── __pycache__
│ └── Unet.cpython-36.pyc
├── train.py
├── utils.py
├── utils_model.py
└── utils_visdom.py
/README.md:
--------------------------------------------------------------------------------
1 | # Machine learning for image-based wavefront sensing
2 |
3 | Astronomical images are often degraded by the disturbance of the Earth’s atmosphere. This thesis proposes to improve image-based wavefront sensing techniques using machine learning algorithms. Deep convolutional neural networks (CNN) have thus been trained to estimate the wavefront using one or multiple intensity measurements.
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Getting Started
11 |
12 | ### Prerequisites
13 |
14 | First, make sure the following python libraries are installed.
15 |
16 | ```
17 | Aotools
18 | Astropy
19 | Soapy
20 | Scipy
21 | Pytorch
22 | Visdom
23 | ```
24 | ### Examples
25 |
26 | The dataset generation can be run using. The dataset size and other parameters can be set in the same file.
27 |
28 | ```
29 | python src/generation/generator.py
30 | ```
31 |
32 | Some notebooks to highlights the networks and the dataset.
33 |
34 | - [Overview of the dataset](examples/data.ipynb)
35 | - [Network Training](examples/training.ipynb)
36 | - [Network evaluation](examples/evaluation.ipynb)
37 |
38 | Finally some classical algorithms (Gerchberg–Saxton) can be directly tested on the dataset.
39 |
40 | ```
41 | python src/algorithms/Gerchberg–Saxton.py
42 | ```
43 |
--------------------------------------------------------------------------------
/assets/unet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/assets/unet.png
--------------------------------------------------------------------------------
/dataset/psf_0.fits:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/dataset/psf_0.fits
--------------------------------------------------------------------------------
/dataset/psf_1.fits:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/dataset/psf_1.fits
--------------------------------------------------------------------------------
/dataset/psf_2.fits:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/dataset/psf_2.fits
--------------------------------------------------------------------------------
/dataset/psf_3.fits:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/dataset/psf_3.fits
--------------------------------------------------------------------------------
/dataset/psf_4.fits:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/dataset/psf_4.fits
--------------------------------------------------------------------------------
/examples/training.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "# Example of network training using Pytorch (1.1) and Cuda (9).\n",
10 | "\n",
11 | "# NB: This code use real time monitoring based on Visdom\n",
12 | "# an open source webserver allowing real time monitoring\n",
13 | "#\n",
14 | "# https://github.com/facebookresearch/visdom\n",
15 | "#\n",
16 | "# Start the webserver using:\n",
17 | "# python -m visdom.server\n",
18 | "#\n",
19 | "# Access it on: (by default)\n",
20 | "# http://localhost:8097\n",
21 | "\n",
22 | "# Global import\n",
23 | "import sys\n",
24 | "import torch\n",
25 | "import torch.nn as nn\n",
26 | "import torch.nn.functional as F\n",
27 | "import torch.optim as optim\n",
28 | "from torchvision import transforms\n",
29 | "from collections import OrderedDict\n",
30 | "\n",
31 | "# Local import\n",
32 | "sys.path.insert(0, '../src/pytorch/models/')\n",
33 | "from Unet import UNet\n",
34 | "\n",
35 | "sys.path.insert(0, '../src/pytorch/')\n",
36 | "from dataset import *\n",
37 | "from train import *\n",
38 | "from lr_analyzer import *\n",
39 | "from criterion import *"
40 | ]
41 | },
42 | {
43 | "cell_type": "code",
44 | "execution_count": 2,
45 | "metadata": {},
46 | "outputs": [
47 | {
48 | "data": {
49 | "image/png": "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\n",
50 | "text/plain": [
51 | ""
52 | ]
53 | },
54 | "metadata": {
55 | "needs_background": "light"
56 | },
57 | "output_type": "display_data"
58 | }
59 | ],
60 | "source": [
61 | "# Load dataset\n",
62 | "\n",
63 | "data_dir = '../dataset/'\n",
64 | "dataset_size = 100000\n",
65 | "dataset = psf_dataset(root_dir = data_dir, \n",
66 | " size = dataset_size,\n",
67 | " transform = transforms.Compose([Noise(), Normalize(), ToTensor()]))\n",
68 | "\n",
69 | "# Check everything works as expected\n",
70 | "import matplotlib.pyplot as plt\n",
71 | "\n",
72 | "id = 0\n",
73 | "sample = dataset[id]\n",
74 | "phase = sample['phase']\n",
75 | "image_in = sample['image'][0]\n",
76 | "image_out = sample['image'][1]\n",
77 | "\n",
78 | "f, axarr = plt.subplots(1, 3, figsize=(15, 10))\n",
79 | "im1 = axarr[0].imshow(phase, cmap=plt.cm.jet)\n",
80 | "im1.set_clim(-np.pi, np.pi)\n",
81 | "axarr[0].set_title(\"Phase\")\n",
82 | "plt.colorbar(im1, ax = axarr[0], fraction=0.046)\n",
83 | "im2 = axarr[1].imshow(image_in, cmap=plt.cm.jet)\n",
84 | "axarr[1].set_title(\"In\")\n",
85 | "plt.colorbar(im2, ax = axarr[1], fraction=0.046)\n",
86 | "im3 = axarr[2].imshow(image_out, cmap=plt.cm.jet)\n",
87 | "axarr[2].set_title(\"Out\")\n",
88 | "plt.colorbar(im3, ax = axarr[2], fraction=0.046)\n",
89 | "plt.show()"
90 | ]
91 | },
92 | {
93 | "cell_type": "code",
94 | "execution_count": 3,
95 | "metadata": {},
96 | "outputs": [],
97 | "source": [
98 | "# Load model architecture, in this example: Unet \n",
99 | "\n",
100 | "model = UNet(2, 1)\n",
101 | "criterion = RMSELoss()\n",
102 | "optimizer = optim.Adam(model.parameters(), lr=1e-3)\n",
103 | "\n",
104 | "# Move Network to GPU\n",
105 | "\n",
106 | "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
107 | "if torch.cuda.device_count() > 1:\n",
108 | " model = nn.DataParallel(model)\n",
109 | " model.cuda()\n",
110 | "\n",
111 | "# Eventually load existing weights\n",
112 | "\n",
113 | "#model_dir = 'ADAM_it2/model.pth'\n",
114 | "#state_dict = torch.load(model_dir)\n",
115 | "#new_state_dict = OrderedDict()\n",
116 | "#for k, v in state_dict.items():\n",
117 | "# name = k[7:] # remove module.\n",
118 | "# new_state_dict[name] = v\n",
119 | "#model.load_state_dict(state_dict)"
120 | ]
121 | },
122 | {
123 | "cell_type": "code",
124 | "execution_count": null,
125 | "metadata": {
126 | "scrolled": true
127 | },
128 | "outputs": [],
129 | "source": [
130 | "# Launch training script. The network weights are automatically saved \n",
131 | "# at the end of an epoch (if the test error is reduced). The metrics are also\n",
132 | "# saved at the end of each epoch in JSON format. All outputs are also stored in a \n",
133 | "# log file.\n",
134 | "#\n",
135 | "# - model = network to train\n",
136 | "# - dataset = dataset object\n",
137 | "# - optimizer = gradient descent optimizer (Adam, SGD, RMSProp)\n",
138 | "# - criterion = loss function\n",
139 | "# - split[x, 1-x] = Division train/test. 'x' is the proportion of the test set.\n",
140 | "# - batch_size = batch size\n",
141 | "# - n_epochs = number of epochs\n",
142 | "# - model_dir = where to save the results\n",
143 | "# - visdom = enable real time monitoring\n",
144 | "\n",
145 | "train(model, \n",
146 | " dataset, \n",
147 | " optimizer, \n",
148 | " criterion,\n",
149 | " split = [0.50, 0.50],\n",
150 | " batch_size = 64,\n",
151 | " n_epochs = 500,\n",
152 | " model_dir = './',\n",
153 | " visdom = True)"
154 | ]
155 | }
156 | ],
157 | "metadata": {
158 | "kernelspec": {
159 | "display_name": "Python (myenv)",
160 | "language": "python",
161 | "name": "myenv"
162 | },
163 | "language_info": {
164 | "codemirror_mode": {
165 | "name": "ipython",
166 | "version": 3
167 | },
168 | "file_extension": ".py",
169 | "mimetype": "text/x-python",
170 | "name": "python",
171 | "nbconvert_exporter": "python",
172 | "pygments_lexer": "ipython3",
173 | "version": "3.6.7"
174 | }
175 | },
176 | "nbformat": 4,
177 | "nbformat_minor": 2
178 | }
179 |
--------------------------------------------------------------------------------
/src/NVCC_monitoring.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import json
4 | from threading import Timer
5 | from pynvml import *
6 | import matplotlib.pyplot as plt
7 |
8 | # Small codes dedicated to the monitoring of nvidia GPUs.
9 |
10 | def getGPUMetrics():
11 | # Metrics accuracy within 1%, see docs:
12 | # docs.nvidia.com/deploy/nvml-api
13 | try:
14 | nvmlInit()
15 | except err:
16 | print("Failed to initialize NVML: ", err)
17 | os.exit(1)
18 |
19 | deviceCount = nvmlDeviceGetCount()
20 | GPUs = [nvmlDeviceGetHandleByIndex(i) for i in range(deviceCount)]
21 |
22 | temperatures = []
23 | fanSpeed = []
24 | power = []
25 | memory = []
26 |
27 | for i in range(deviceCount):
28 | temperatures.append(nvmlDeviceGetTemperature(GPUs[i], NVML_TEMPERATURE_GPU))
29 | memory.append(nvmlDeviceGetMemoryInfo(GPUs[i]).used)
30 | fanSpeed.append(nvmlDeviceGetFanSpeed(GPUs[i]))
31 | power.append(nvmlDeviceGetPowerUsage(GPUs[i]) / 1000) # Miliwatt to watt
32 |
33 | try:
34 | nvmlShutdown()
35 | except err:
36 | print("Error shutting down NVML:", err)
37 | os.exit(1)
38 |
39 | metrics = { 'gpu%i'%i: {
40 | 'temperatures': temperatures[i],
41 | 'fanSpeed': fanSpeed[i],
42 | 'memory': memory[i],
43 | 'power': power[i],
44 | 'time': time.strftime("%H:%M:%S")
45 | } for i in range(deviceCount)
46 | }
47 |
48 | return metrics
49 |
50 |
51 | def saveGPUMetrics(metrics, saving_dir='', name='monitoring_metrics.json', deviceCount=2):
52 | # Save to json GPU metrics
53 |
54 | json_path = os.path.join(saving_dir, name)
55 | if os.path.exists(json_path):
56 | # Load existing metrics and add news
57 | with open(json_path, 'r') as f:
58 | old_metrics = json.load(f)
59 | for i in range(deviceCount):
60 | for key in old_metrics['gpu%i'%i]:
61 | old_metrics['gpu%i'%i][key].append(metrics['gpu%i'%i][key])
62 | with open(json_path, 'w') as f:
63 | json.dump(old_metrics, f, indent=4)
64 | else:
65 | # If does not exist
66 | with open(json_path, 'w') as f:
67 | for i in range(deviceCount):
68 | for key in metrics['gpu%i'%i]:
69 | metrics['gpu%i'%i][key] = [metrics['gpu%i'%i][key]]
70 | json.dump(metrics, f, indent=4)
71 |
72 | def plotMetrics(json_path, key_name, limit=93):
73 |
74 | with open(json_path, 'r') as f:
75 | metrics = json.load(f)
76 |
77 | plt.plot(metrics['gpu0'][key_name], label='GPU 0')
78 | plt.plot(metrics['gpu1'][key_name], label='GPU 1')
79 | plt.hlines(limit,0, len(metrics['gpu0'][key_name]), color='red', linestyle='--')
80 | plt.grid()
81 | plt.legend()
82 | plt.title(key_name)
83 | plt.show()
84 |
85 | # How to use:
86 | # monitor = monitoring.monitoringGPU(30) # autostart, time in seconds
87 | # - Do fancy stuffs
88 | # monitor.stop()
89 | class monitoringGPU(object):
90 | def __init__(self, interval, *args, **kwargs):
91 | self._timer = None
92 | self.interval = interval
93 | self.args = args
94 | self.kwargs = kwargs
95 | self.is_running = False
96 | self.start()
97 |
98 | def _run(self):
99 | self.is_running = False
100 | self.start()
101 | metrics = getGPUMetrics()
102 | saveGPUMetrics(metrics, *self.args, **self.kwargs)
103 |
104 | def start(self):
105 | if not self.is_running:
106 | self._timer = Timer(self.interval, self._run)
107 | self._timer.start()
108 | self.is_running = True
109 |
110 | def stop(self):
111 | self._timer.cancel()
112 | self.is_running = False
113 |
114 |
--------------------------------------------------------------------------------
/src/algorithms/Gerchberg–Saxton.py:
--------------------------------------------------------------------------------
1 | import aotools
2 | from astropy.io import fits
3 | import numpy as np
4 | import utils
5 | from animation import *
6 | from time import time
7 |
8 | def GerchbergSaxton(target, source, phase, n_max=200, animation=True):
9 | '''
10 | Phase retrieval, Gerchberg-Saxton algorithm.
11 |
12 | [1] R. W. Gerchberg and W. O. Saxton, “A practical algorithm
13 | for the determination of the phase from image and diffraction
14 | plane pictures,” Optik 35, 237 (1972)
15 |
16 | [2] J. R. Fienup, "Phase retrieval algorithms: a comparison,"
17 | Appl. Opt. 21, 2758-2769 (1982)
18 |
19 | :param target:
20 | :param source:
21 | :param phase: Algorithm goal, provided for visualization and metrics
22 | :param n_max: Maximum number of iteration
23 | :param animation:
24 | :return:
25 | '''
26 |
27 | # Add padding
28 | target = utils.addPadding(np.sqrt(target))
29 | source = utils.addPadding(source)
30 |
31 | # Metrics: tuple -> (time, error)
32 | metrics = []
33 |
34 | # Initialize animation
35 | if animation:
36 | f, axarr = initAnimation()
37 |
38 | # Timer
39 | timer = 0.0
40 |
41 | # Random initializer
42 | A = source * np.exp(1j * 0.0 * np.pi * (np.random.rand(source.shape[0], source.shape[1])*2-1))
43 |
44 | for n in range(n_max):
45 | t0 = time()
46 | B = np.absolute(source) * np.exp(1j * np.angle(A))
47 | C = utils.fft(B)
48 | D = np.absolute(target) * np.exp(1j * np.angle(C))
49 | A = utils.ifft(D)
50 |
51 | t1 = time()
52 | timer += t1-t0
53 |
54 | phaseEst = source * np.angle(A)
55 | #phaseEst = np.rot90(np.rot90(-phaseEst))
56 | error = utils.rootMeanSquaredError(phase, utils.removePadding(phaseEst), mask=True)
57 | #error = utils.rootMeanSquaredError(C, D, mask=True)
58 |
59 | metrics.append((timer, error))
60 |
61 | if animation:
62 | H = utils.addPadding(mask) * np.exp(1j * (phaseEst-utils.addPadding(phase)))
63 | h = utils.fft(H)
64 | psf = utils.removePadding(np.abs(h) ** 2)
65 | updateAnimation(f, axarr, metrics, phase, utils.removePadding(phaseEst), psf, timer)
66 |
67 | return metrics
68 |
69 | if __name__ == '__main__':
70 |
71 | # Files
72 | #reference_file = 'references.fits'
73 | psf_file = '../../dataset/psf_1.fits'
74 |
75 | # Data
76 | wavelength = 2200 * (10**(-9)) #[m]
77 | n=20
78 | z_basis = aotools.zernikeArray(n+1, 128, norm='rms') #[rad]
79 | mask = aotools.circle(64, 128)
80 |
81 | #rv_HDU = fits.open(reference_file)
82 | #mask = rv_HDU[0].data # [0-1] function defining entrance pupil
83 | #psf_reference = rv_HDU[1].data # diffraction limited point spread function
84 |
85 | HDU = fits.open(psf_file)
86 | phase = utils.meterToRadian(HDU[1].data, wavelength* (10**(9)))
87 |
88 | H = utils.addPadding(mask) * np.exp(1j * utils.addPadding(phase))
89 | h = utils.fft(H)
90 | psf_test = utils.removePadding(np.abs(h)**2)
91 |
92 | metrics = GerchbergSaxton(psf_test, mask, phase, n_max=200, animation=True)
93 |
94 |
--------------------------------------------------------------------------------
/src/algorithms/Input-Output.py:
--------------------------------------------------------------------------------
1 | import aotools
2 | from astropy.io import fits
3 | import numpy as np
4 | import utils
5 | from animation import *
6 | from time import time
7 |
8 | def HybridInputOutput(target, source, phase, n_max=200, animation=True):
9 | '''
10 |
11 | [1] E. Osherovich, Numerical methods for phase retrieval, 2012,
12 | https://arxiv.org/abs/1203.4756
13 | [2] J. R. Fienup, Phase retrieval algorithms: a comparison, 1982,
14 | https://www.osapublishing.org/ao/abstract.cfm?uri=ao-21-15-2758
15 |
16 | :param target:
17 | :param source:
18 | :param phase: Algorithm goal, provided for visualization and metrics
19 | :param n_max: Maximum number of iteration
20 | :param animation:
21 | :return:
22 | '''
23 |
24 | # Add padding
25 | target = utils.addPadding(np.sqrt(target))
26 | source = utils.addPadding(source)
27 |
28 | # Metrics: tuple -> (time, rmse)
29 | metrics = []
30 |
31 | # Initialize animation
32 | if animation:
33 | f, axarr = initAnimation()
34 |
35 | # Timer
36 | timer = 0.0
37 |
38 | # Random initializer
39 | g_k_prime = np.exp(1j * 0.0 * np.pi * (np.random.rand(source.shape[0], source.shape[1])*2-1))
40 |
41 |
42 | # Previous iteration
43 | g_k_previous = None
44 |
45 | for n in range(n_max):
46 | t0 = time()
47 |
48 | g_k = source * np.exp(1j * np.angle(g_k_prime))
49 | G_k= utils.fft(g_k)
50 | G_k_prime = np.absolute(target) * np.exp(1j * np.angle(G_k))
51 | g_k_prime = utils.ifft(G_k_prime)
52 |
53 |
54 | if g_k_previous is None:
55 | g_k_previous = g_k_prime
56 | else:
57 | g_k_previous = g_k
58 |
59 | indices = np.logical_or(np.logical_and(g_k < 0, source), np.logical_not(source))
60 |
61 | g_k[indices] = g_k_previous[indices] - 0.9 * np.real(g_k_prime[indices])
62 |
63 | t1 = time()
64 | timer += t1-t0
65 |
66 | phaseEst = source * np.angle(g_k)
67 | #phaseEst = np.rot90(np.rot90(-phaseEst))
68 | error = utils.rootMeanSquaredError(phase, utils.removePadding(phaseEst), mask=True)
69 | #error = utils.rootMeanSquaredError(G_k, G_k_prime, mask=True)
70 |
71 | metrics.append((timer, error))
72 |
73 | if animation:
74 | H = utils.addPadding(mask) * np.exp(1j * (phaseEst-utils.addPadding(phase)))
75 | h = utils.fft(H)
76 | psf = utils.removePadding(np.abs(h) ** 2)
77 | updateAnimation(f, axarr, metrics, phase, utils.removePadding(phaseEst), psf, timer)
78 |
79 | return metrics
80 |
81 | if __name__ == '__main__':
82 |
83 | # Files
84 | reference_file = 'references.fits'
85 | psf_file = 'psf_1.fits'
86 |
87 | # Data
88 | wavelength = 2200 * (10**(-9)) #[m]
89 | n=20
90 | z_basis = aotools.zernikeArray(n+1, 128, norm='rms') #[rad]
91 |
92 | rv_HDU = fits.open(reference_file)
93 | mask = rv_HDU[0].data # [0-1] function defining entrance pupil
94 | psf_reference = rv_HDU[1].data # diffraction limited point spread function
95 |
96 | HDU = fits.open(psf_file)
97 | phase = utils.meterToRadian(HDU[1].data, wavelength* (10**(9)))
98 |
99 | H = utils.addPadding(mask) * np.exp(1j * utils.addPadding(phase))
100 | h = utils.fft(H)
101 | psf_test = utils.removePadding(np.abs(h)**2)
102 |
103 | metrics = HybridInputOutput(psf_test, mask, phase, n_max=300, animation=True)
104 | metrics = np.array(metrics)
105 |
--------------------------------------------------------------------------------
/src/algorithms/animation.py:
--------------------------------------------------------------------------------
1 | import aotools
2 | import numpy as np
3 | import matplotlib.pyplot as plt
4 | import matplotlib as mpl
5 | import utils
6 |
7 |
8 | def initAnimation():
9 | mpl.style.use('default')
10 | f, axarr = plt.subplots(2, 2, figsize=(10, 10))
11 | return f, axarr
12 |
13 | def updateAnimation(f, axarr, error, phase, phaseEst, psf, timer):
14 |
15 | f.suptitle('Algorithm time: {0:.5}s'.format(timer))
16 | cmap = plt.cm.jet
17 | error = np.array(error)
18 | im1 = axarr[0, 0].plot(error[:, 1], linewidth=2.5)
19 | axarr[0, 0].grid(color='lightgrey', linestyle='--')
20 | axarr[0, 0].set_title("Wavefront error")
21 | axarr[0, 0].set_xlabel('iterations')
22 | axarr[0, 0].set_ylabel('RMSE')
23 | im2 = axarr[0, 1].imshow(psf**(1/3), cmap=cmap)
24 | cb2 = plt.colorbar(im2, ax=axarr[0, 1], fraction=0.046)
25 | axarr[0, 1].set_title("Point Spread function (strehl={0:.5f})".format(utils.strehl(phase-phaseEst)))
26 | axarr[0, 1].set_axis_off()
27 | mask=aotools.circle(64, 128).astype(np.float64)
28 | phase[mask<0.1]=None
29 | phaseEst[mask<0.1]=None
30 | im3 = axarr[1, 0].imshow(phase, cmap=cmap)
31 | im3.set_clim(-np.pi,np.pi)
32 | cb3 = plt.colorbar(im3, ax=axarr[1, 0], fraction=0.046)
33 | axarr[1, 0].set_title("Exact Phase")
34 | axarr[1, 0].set_axis_off()
35 | im4 = axarr[1, 1].imshow(phaseEst, cmap=cmap)
36 | im4.set_clim(-np.pi, np.pi)
37 | axarr[1, 1].set_title("Recovered phase")
38 | axarr[1, 1].set_axis_off()
39 | cb4 = plt.colorbar(im4, ax=axarr[1, 1], fraction=0.046)
40 | plt.pause(1e-5)
41 | axarr[0, 0].cla()
42 | cb2.remove()
43 | cb3.remove()
44 | cb4.remove()
45 | phase[mask<0.1]=0
46 | phaseEst[mask<0.1]=0
47 |
--------------------------------------------------------------------------------
/src/algorithms/utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import numpy.fft as FFT
3 | import aotools
4 |
5 | def meterToRadian(array, wavelength):
6 | '''
7 | Convert array from meter to radian
8 |
9 | :param array: [nm]
10 | :param wavelength: [nm]
11 | :return: [rad]
12 | '''
13 | array_rad = (array / wavelength) * (2*np.pi)
14 | return array_rad
15 |
16 | def getPhase(z_coeffs, z_basis):
17 | '''
18 | Compute phase from Zernike basis and Zernike coeffs
19 |
20 | :param z_coeffs: [rad]
21 | :param z_basis: [rad]
22 | :return: [rad]
23 | '''
24 | phase = z_coeffs[:, None, None] * z_basis[:, :, :]
25 | phase = np.sum(phase, axis=0)
26 | phase = np.squeeze(phase)
27 | return phase
28 |
29 | def fft(array):
30 | '''
31 | Compute discrete fast fourier transform
32 |
33 | :param array:
34 | :return:
35 | '''
36 | fft_array = FFT.fftshift(FFT.fft2(FFT.fftshift(array)))
37 | return fft_array
38 |
39 | def ifft(array):
40 | '''
41 | Compute discrete inverse fast fourier transform
42 |
43 | :param array:
44 | :return:
45 | '''
46 | fft_array = FFT.ifftshift(FFT.ifft2(FFT.ifftshift(array)))
47 | return fft_array
48 |
49 | def pad_with(vector, pad_width, iaxis, kwargs):
50 | '''
51 | Padding utils
52 |
53 | :param vector:
54 | :param pad_width:
55 | :param iaxis:
56 | :param kwargs:
57 | :return:
58 | '''
59 | pad_value = kwargs.get('padder', 10)
60 | vector[:pad_width[0]] = pad_value
61 | vector[-pad_width[1]:] = pad_value
62 | return vector
63 |
64 | def addPadding(array, padding=2):
65 | '''
66 | Add padding to array
67 |
68 | :param array:
69 | :param padding:
70 | :return:
71 | '''
72 | size = array.shape[1]
73 | padded_array = np.pad(array, padding*size, pad_with, padder = 0)
74 | return padded_array
75 |
76 | def removePadding(array, padding=2):
77 | '''
78 | Remove padding from array
79 |
80 | :param array:
81 | :param padding:
82 | :return:
83 | '''
84 | size = array.shape[1] // (2*padding + 1)
85 | rmPixel = padding*size
86 | return array[rmPixel:size+rmPixel,rmPixel:size+rmPixel]
87 |
88 |
89 | def rootMeanSquaredError(array1, array2, mask=True):
90 | '''
91 | RMSE error between array1 and array2
92 | if mask=True computed over circle
93 |
94 | :param array:
95 | :return:
96 | '''
97 | if mask is True:
98 | size = array1.shape[1]
99 | center = size//2
100 | radius = size//2
101 |
102 | n = 0
103 | error = 0.0
104 | for x in range(size):
105 | for y in range(size):
106 | if (x-center)**2 + (y-center)**2 <= radius:
107 | n += 1
108 | error += (array1[x, y]-array2[x, y])**2
109 | rms_error = np.sqrt((1/n)*(error))
110 | else:
111 | rms_error = np.sqrt(((array1 - array2) ** 2).mean())
112 | return rms_error
113 |
114 | def strehl(phase):
115 | mask = aotools.circle(64, 128)
116 | N= 0.0
117 | phase_mean = 0.0
118 | for i in range(128):
119 | for j in range(128):
120 | if(mask[i,j]>= 0.001):
121 | N += 1
122 | phase_mean += phase[i, j]
123 | phase_mean = phase_mean / N
124 | strehl = np.abs(np.mean(np.exp(1j*(phase-phase_mean))))**2
125 | return strehl
--------------------------------------------------------------------------------
/src/generation/generator.py:
--------------------------------------------------------------------------------
1 | import time
2 | import aotools
3 | from radial import radial_data
4 | import numpy as np
5 | from scipy import fftpack
6 | from astropy.io import fits
7 | from soapy import SCI, confParse
8 | from matplotlib import pyplot as plt
9 |
10 | # ------------------------------------------------------------------------
11 | # Generate Point Spread functions from randomly drawn non-common path
12 | # aberrations. The aberration follows a 1/f^2 law.
13 | # One PSF in focus as well as a PSF out of focus are saved in FITS format
14 | # (see astropy docs). The corresponding phase and Zernike Coefficient
15 | # are also saved.
16 | # ------------------------------------------------------------------------
17 |
18 | np.random.seed(seed=0)
19 |
20 | SOAPY_CONF = "psf.yaml" # Soapy config
21 | gridsize = 128 # Pixel size of science camera
22 | wavelength = 2.2e-6 # Observational wavelength
23 | diameter = 10 # Telescope diameter
24 | pixelScale = 0.01 # [''/px]s
25 |
26 | n_psfs = 5 # Number of PSFs
27 | n_zernike = 100 # Number of Zernike polynomials
28 | i_zernike = np.arange(2, n_zernike + 2) # Zernike polynomial indices (piston excluded)
29 | o_zernike= [] # Zernike polynomial radial Order, see J. Noll paper :
30 | for i in range(1,n_zernike): # "Zernike polynomials and atmospheric turbulence", 1975
31 | for j in range(i+1):
32 | if len(o_zernike) < n_zernike:
33 | o_zernike.append(i)
34 |
35 | # Generate randomly Zernike coefficient. By dividing the value
36 | # by its radial order we produce a distribution following
37 | # the expected 1/f^-2 law.
38 | c_zernike = 2 * np.random.random((n_psfs, n_zernike)) - 1
39 | for j in range(n_psfs):
40 | for i in range(n_zernike):
41 | c_zernike[j, i] = c_zernike[j, i] / o_zernike[i]
42 | c_zernike = np.array([c_zernike[k, :] / np.abs(c_zernike[k, :]).sum()
43 | * wavelength*(10**9) for k in range(n_psfs)])
44 |
45 | # Update scientific camera parameters
46 | config = confParse.loadSoapyConfig(SOAPY_CONF)
47 | config.scis[0].pxlScale = pixelScale
48 | config.tel.telDiam = diameter
49 | config.calcParams()
50 |
51 | mask = aotools.circle(config.sim.pupilSize / 2., config.sim.simSize).astype(np.float64)
52 | zernike_basis = aotools.zernikeArray(n_zernike + 1, config.sim.pupilSize, norm='rms')
53 |
54 | psfObj = SCI.PSF(config, nSci=0, mask=mask)
55 |
56 | psfs_in = np.zeros((n_psfs, psfObj.detector.shape[0], psfObj.detector.shape[1]))
57 | psfs_out = np.zeros((n_psfs, psfObj.detector.shape[0], psfObj.detector.shape[1]))
58 |
59 | defocus = (wavelength / 4) * (10 ** 9) * zernike_basis[3, :, :]
60 |
61 | t0 = time.time()
62 | n_fail = 0
63 |
64 | for i in range(n_psfs):
65 |
66 | aberrations_in = np.squeeze(np.sum(c_zernike[i, :, None, None] * zernike_basis[1:, :, :], axis=0))
67 | psfs_in[i, :, :] = np.copy(psfObj.frame(aberrations_in.astype(np.float64)))
68 |
69 | aberations_out = np.squeeze(aberrations_in) + defocus
70 | psfs_out[i, :, :] = np.copy(psfObj.frame(aberations_out.astype(np.float64)))
71 |
72 | # psfs_in[i, :, :] = np.random.poisson(lam=100000*psfs_in[i, :, :], size=None)
73 | # psfs_out[i, :, :] = np.random.poisson(lam=100000*psfs_out[i, :, :], size=None)
74 |
75 | # Save
76 | outfile = "psf_" + str(i) + ".fits"
77 | hdu_primary = fits.PrimaryHDU(c_zernike[i, :].astype(np.float32))
78 | hdu_phase = fits.ImageHDU(aberrations_in.astype(np.float32), name='PHASE')
79 | hdu_In = fits.ImageHDU(psfs_in[i, :, :].astype(np.float32), name='INFOCUS')
80 | hdu_Out = fits.ImageHDU(psfs_out[i, :, :].astype(np.float32), name='OUTFOCUS')
81 | hdu = fits.HDUList([hdu_primary, hdu_phase, hdu_In, hdu_Out])
82 | hdu.writeto(outfile, overwrite=True)
83 |
84 | t_soapy = time.time() - t0
85 | print('Propagation and saving finished in {0:2f}s'.format(t_soapy))
86 | print('Failed: {0:2f}'.format(n_fail))
87 |
--------------------------------------------------------------------------------
/src/generation/plots.py:
--------------------------------------------------------------------------------
1 | import time
2 | import aotools
3 | from radial import radial_data
4 | import numpy as np
5 | from scipy import fftpack
6 | from astropy.io import fits
7 | from soapy import SCI, confParse
8 | from matplotlib import pyplot as plt
9 |
10 | id = 0
11 | phase = np.squeeze(np.sum(c_zernike[id, :, None, None] * zernike_basis[1:, :, :], axis=0))
12 | F1 = fftpack.fft2(phase)
13 | F2 = fftpack.fftshift( F1 )
14 | psd2D = np.abs( F2 )**2
15 |
16 | plt.imshow(np.sqrt(psfs_in[id,:,:]), cmap=plt.cm.jet)
17 | plt.axis('off')
18 | plt.savefig('psf_in.pdf')
19 | plt.imshow(np.sqrt(psfs_out[id,:,:]), cmap=plt.cm.jet)
20 | plt.axis('off')
21 | plt.savefig('psf_out.pdf')
22 | plt.imshow(phase, cmap=plt.cm.jet)
23 | plt.axis('off')
24 | plt.savefig('phase_in.pdf')
25 |
26 | fig, ax = plt.subplots(figsize=(15, 5))
27 | width = 0.4
28 | plt.bar(i_zernike[:100], np.abs(c_zernike[id]/2200*2*np.pi)[:100], color='#32526e', width=width, zorder=3)
29 | #plt.title('Zernike coefficient distribution', fontsize=19)
30 | plt.xlabel('zernike coefficients', fontsize=16)
31 | plt.ylabel('magnitude [rad]', fontsize=16)
32 | plt.xticks(fontsize=16)
33 | plt.yticks(fontsize=16)
34 | ax.spines['right'].set_visible(False)
35 | ax.spines['top'].set_visible(False)
36 | plt.grid(zorder=0, color='lightgray', linestyle='--')
37 | plt.ylim(0,0.4)
38 | plt.savefig('z_distrib.pdf')
39 | plt.show()
40 |
41 | rad_obj = radial_data(psd2D, rmax=64)
42 | fig, ax = plt.subplots()
43 | plt.xlabel('Spatial frequency (cycles/pupil)', fontsize=13)
44 | plt.ylabel('PSF (nm²nm²)', fontsize=13)
45 | plt.loglog(rad_obj.r[1:], psd2D[65:128, 64])
46 | ax.spines['right'].set_visible(False)
47 | ax.spines['top'].set_visible(False)
48 | plt.grid(zorder=0, color='lightgray', linestyle='--')
49 | start, end = ax.get_xlim()
50 | plt.xticks(np.logspace(np.log10(start), np.log10(end), num=9, base=10),('10⁰','','','','10¹','','','','',''))
51 | plt.savefig('PSD_rad.pdf')
52 | plt.show()
53 |
54 | fig, ax = plt.subplots()
55 | #plt.title('1D PSD avg', fontsize=15)
56 | plt.xlabel('Spatial frequency (cycles/pupil)', fontsize=13)
57 | plt.ylabel('PSF (nm²nm²)', fontsize=13)
58 | plt.loglog(rad_obj.r[1:],rad_obj.mean[1:])
59 | ax.spines['right'].set_visible(False)
60 | ax.spines['top'].set_visible(False)
61 | plt.grid(zorder=0, color='lightgray', linestyle='--')
62 | start, end = ax.get_xlim()
63 | plt.xticks(np.logspace(np.log10(start), np.log10(end), num=9, base=10),('10⁰','','','','10¹','','','','',''))
64 | plt.savefig('PSD_rad_avg.pdf')
65 | plt.show()
66 |
--------------------------------------------------------------------------------
/src/generation/psf.yaml:
--------------------------------------------------------------------------------
1 | simName:
2 | pupilSize: 128
3 |
4 | nSci: 1
5 | nIters: 5000
6 | loopTime: 0.0025
7 | threads: 4
8 |
9 | verbosity: 2
10 |
11 |
12 | Atmosphere:
13 | scrnNo: 1
14 | scrnHeights: [0]
15 | scrnStrengths: [1]
16 | windDirs: [0]
17 | windSpeeds: [5]
18 | wholeScrnSize: 2048
19 | r0: 0.1
20 | L0: [100]
21 | infinite: True
22 |
23 | Telescope:
24 | telDiam: 10
25 | obsDiam: 0
26 | mask: circle
27 |
28 | Reconstructor:
29 | type: MVM
30 |
31 |
32 | Science:
33 | 0:
34 | position: [0, 0]
35 | FOV: 10.0
36 | #pxlScale: 0.2
37 | wavelength: 2.2e-6
38 | pxls: 128
39 | fftOversamp: 2
40 | fftwThreads: 0
41 |
42 | fftwFlag: "FFTW_MEASURE"
43 |
44 |
--------------------------------------------------------------------------------
/src/generation/radial.py:
--------------------------------------------------------------------------------
1 | def radial_data(data,annulus_width=1,working_mask=None,x=None,y=None,rmax=None):
2 | """
3 | r = radial_data(data,annulus_width,working_mask,x,y)
4 |
5 | A function to reduce an image to a radial cross-section.
6 |
7 | INPUT:
8 | ------
9 | data - whatever data you are radially averaging. Data is
10 | binned into a series of annuli of width 'annulus_width'
11 | pixels.
12 | annulus_width - width of each annulus. Default is 1.
13 | working_mask - array of same size as 'data', with zeros at
14 | whichever 'data' points you don't want included
15 | in the radial data computations.
16 | x,y - coordinate system in which the data exists (used to set
17 | the center of the data). By default, these are set to
18 | integer meshgrids
19 | rmax -- maximum radial value over which to compute statistics
20 |
21 | OUTPUT:
22 | -------
23 | r - a data structure containing the following
24 | statistics, computed across each annulus:
25 | .r - the radial coordinate used (outer edge of annulus)
26 | .mean - mean of the data in the annulus
27 | .std - standard deviation of the data in the annulus
28 | .median - median value in the annulus
29 | .max - maximum value in the annulus
30 | .min - minimum value in the annulus
31 | .numel - number of elements in the annulus
32 | """
33 |
34 | # 2010-03-10 19:22 IJC: Ported to python from Matlab
35 | # 2005/12/19 Added 'working_region' option (IJC)
36 | # 2005/12/15 Switched order of outputs (IJC)
37 | # 2005/12/12 IJC: Removed decifact, changed name, wrote comments.
38 | # 2005/11/04 by Ian Crossfield at the Jet Propulsion Laboratory
39 |
40 | import numpy as ny
41 |
42 | class radialDat:
43 | """Empty object container.
44 | """
45 | def __init__(self):
46 | self.mean = None
47 | self.std = None
48 | self.median = None
49 | self.numel = None
50 | self.max = None
51 | self.min = None
52 | self.r = None
53 |
54 | #---------------------
55 | # Set up input parameters
56 | #---------------------
57 | data = ny.array(data)
58 |
59 | if working_mask==None:
60 | working_mask = ny.ones(data.shape,bool)
61 |
62 | npix, npiy = data.shape
63 | if x==None or y==None:
64 | x1 = ny.arange(-npix/2.,npix/2.)
65 | y1 = ny.arange(-npiy/2.,npiy/2.)
66 | x,y = ny.meshgrid(y1,x1)
67 |
68 | r = abs(x+1j*y)
69 |
70 | if rmax==None:
71 | rmax = r[working_mask].max()
72 |
73 | #---------------------
74 | # Prepare the data container
75 | #---------------------
76 | dr = ny.abs([x[0,0] - x[0,1]]) * annulus_width
77 | radial = ny.arange(rmax/dr)*dr + dr/2.
78 | nrad = len(radial)
79 | radialdata = radialDat()
80 | radialdata.mean = ny.zeros(nrad)
81 | radialdata.std = ny.zeros(nrad)
82 | radialdata.median = ny.zeros(nrad)
83 | radialdata.numel = ny.zeros(nrad)
84 | radialdata.max = ny.zeros(nrad)
85 | radialdata.min = ny.zeros(nrad)
86 | radialdata.r = radial
87 |
88 | #---------------------
89 | # Loop through the bins
90 | #---------------------
91 | for irad in range(nrad): #= 1:numel(radial)
92 | minrad = irad*dr
93 | maxrad = minrad + dr
94 | thisindex = (r>=minrad) * (r None:
39 | # pylint: disable=invalid-name
40 | self.T_max = T_max
41 | self.eta_min = eta_min
42 | self.factor = factor
43 | self._last_restart: int = 0
44 | self._cycle_counter: int = 0
45 | self._cycle_factor: float = 1.
46 | self._updated_cycle_len: int = T_max
47 | self._initialized: bool = False
48 | super(CosineWithRestarts, self).__init__(optimizer, last_epoch)
49 |
50 | def get_lr(self):
51 | """Get updated learning rate."""
52 | # HACK: We need to check if this is the first time get_lr() was called, since
53 | # we want to start with step = 0, but _LRScheduler calls get_lr with
54 | # last_epoch + 1 when initialized.
55 | if not self._initialized:
56 | self._initialized = True
57 | return self.base_lrs
58 |
59 | step = self.last_epoch + 1
60 | self._cycle_counter = step - self._last_restart
61 |
62 | lrs = [
63 | (
64 | self.eta_min + ((lr - self.eta_min) / 2) *
65 | (
66 | np.cos(
67 | np.pi *
68 | ((self._cycle_counter) % self._updated_cycle_len) /
69 | self._updated_cycle_len
70 | ) + 1
71 | )
72 | ) for lr in self.base_lrs
73 | ]
74 |
75 | if self._cycle_counter % self._updated_cycle_len == 0:
76 | # Adjust the cycle length.
77 | self._cycle_factor *= self.factor
78 | self._cycle_counter = 0
79 | self._updated_cycle_len = int(self._cycle_factor * self.T_max)
80 | self._last_restart = step
81 |
82 | return lrs
83 |
--------------------------------------------------------------------------------
/src/pytorch/dataset.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from astropy.io import fits
3 | from torch.utils.data import Dataset, DataLoader, SubsetRandomSampler
4 | from astropy.visualization import SqrtStretch, MinMaxInterval
5 | import numpy as np
6 |
7 | class psf_dataset(Dataset):
8 |
9 | def __init__(self, root_dir, size, transform=None):
10 | self.size = size
11 | self.root_dir = root_dir
12 | self.transform = transform
13 |
14 | def __len__(self):
15 | return self.size
16 |
17 | def __getitem__(self, id):
18 |
19 | if id >= self.size:
20 | raise ValueError('[Dataset] Index out of bounds')
21 | return None
22 |
23 | sample_name = self.root_dir + 'psf_' + str(int(id)) + '.fits'
24 | sample_hdu = fits.open(sample_name)
25 |
26 | image = np.stack((sample_hdu[2].data, sample_hdu[3].data)).astype(np.float32)
27 |
28 | phase = sample_hdu[1].data.astype(np.float32)
29 |
30 | sample = {'phase': phase, 'image': image}
31 |
32 | if self.transform:
33 | sample = self.transform(sample)
34 |
35 | return sample
36 |
37 |
38 | class Normalize(object):
39 | def __call__(self, sample):
40 | phase, image = sample['phase'], sample['image']
41 |
42 | image[0] = minmax(np.sqrt(image[0]))
43 | image[1] = minmax(np.sqrt(image[1]))
44 |
45 | phase = (phase/2200.)*2*np.pi
46 |
47 | return {'phase': phase, 'image': image}
48 |
49 |
50 | def minmax(array):
51 | a_min = np.min(array)
52 | a_max = np.max(array)
53 | return (array-a_min)/(a_max-a_min)
54 |
55 | class ToTensor(object):
56 | def __call__(self, sample):
57 | phase, image = sample['phase'], sample['image']
58 |
59 | return {'phase': torch.from_numpy(phase), 'image': torch.from_numpy(image)}
60 |
61 | class Noise(object):
62 | def __call__(self, sample):
63 | phase, image = sample['phase'], sample['image']
64 |
65 | noise_intensity = 1000
66 | image[0] = minmax(image[0])
67 | image[1] = minmax(image[1])
68 | image[0] = np.random.poisson(lam=noise_intensity*image[0], size=None)
69 | image[1] = np.random.poisson(lam=noise_intensity*image[1], size=None)
70 |
71 | return {'phase': phase, 'image': image}
72 |
73 |
74 | def splitDataLoader(dataset, split=[0.9, 0.1], batch_size=32, random_seed=None, shuffle=True):
75 | indices = list(range(len(dataset)))
76 | s = int(np.floor(split[1] * len(dataset)))
77 | if shuffle:
78 | np.random.seed(random_seed)
79 | np.random.shuffle(indices)
80 | train_indices, val_indices = indices[s:], indices[:s]
81 |
82 | train_sampler, val_sampler = SubsetRandomSampler(train_indices), SubsetRandomSampler(val_indices)
83 |
84 | train_dataloader = DataLoader(dataset, batch_size=batch_size, num_workers=4, sampler=train_sampler)
85 | val_dataloader = DataLoader(dataset, batch_size=batch_size, num_workers=4, sampler=val_sampler)
86 |
87 | return train_dataloader, val_dataloader
88 |
--------------------------------------------------------------------------------
/src/pytorch/lr_analyzer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import utils
4 | import json
5 | import logging
6 | import torch
7 | import torch.nn as nn
8 | import torch.nn.functional as F
9 | import torch.optim as optim
10 | import numpy as np
11 | from torchvision import transforms
12 | from dataset import psf_dataset, splitDataLoader, ToTensor, Normalize
13 | from utils_visdom import VisdomWebServer
14 | import aotools
15 |
16 | def lr_analyzer(model, dataset, optimizer, criterion, split=[0.9, 0.1], batch_size=64, lr=[1e-5, 1e-1]):
17 |
18 | for p in optimizer.param_groups:
19 | p['lr'] = lr[0]
20 |
21 | lr_log = np.geomspace(lr[0], lr[1], 100)
22 |
23 | # Dataset
24 | dataloaders, _ = splitDataLoader(dataset, split=split, batch_size=batch_size)
25 |
26 | # Device
27 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
28 |
29 | losses = []
30 | lrs = []
31 |
32 | running_loss = 0.0
33 | it = 0
34 |
35 | for _, sample in enumerate(dataloaders):
36 | # GPU support
37 | inputs = sample['image'].to(device)
38 | phase_0 = sample['phase'].to(device)
39 |
40 | # zero the parameter gradients
41 | optimizer.zero_grad()
42 |
43 | # forward: track history if only in train
44 | with torch.set_grad_enabled(True):
45 |
46 | # Network return phase and zernike coeffs
47 | phase_estimation = model(inputs)
48 | loss = criterion(torch.squeeze(phase_estimation), phase_0)
49 | loss.backward()
50 | optimizer.step()
51 |
52 | losses.append(loss.item())
53 | lrs.append(get_lr(optimizer))
54 |
55 | if it == 100:
56 | break
57 |
58 | #update lr
59 | for p in optimizer.param_groups:
60 | p['lr'] = lr_log[it]
61 |
62 | it +=1
63 |
64 | return losses, lrs
65 |
66 | def get_lr(optimizer):
67 | for p in optimizer.param_groups:
68 | lr = p['lr']
69 | return lr
70 |
--------------------------------------------------------------------------------
/src/pytorch/models/Densenet.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import numpy as np
3 | import aotools
4 | import torch.nn as nn
5 | import torch.nn.functional as F
6 | import torchvision.models as models
7 |
8 | class Net(nn.Module):
9 |
10 | def __init__(self):
11 | super(Net, self).__init__()
12 |
13 | self.densenet = models.densenet161(pretrained=True)
14 |
15 | for param in self.densenet.parameters():
16 | param.requires_grad = True
17 |
18 | # Input reshape
19 | first_conv_layer = [nn.Conv2d(2, 3, kernel_size=1, stride=1, bias=True),
20 | nn.AdaptiveMaxPool2d(224),
21 | self.densenet.features.conv0]
22 | self.densenet.features.conv0 = nn.Sequential(*first_conv_layer)
23 |
24 | # Classifier
25 | self.densenet.classifier = nn.Sequential(
26 | nn.Linear(2208, 20, bias=True),
27 | #nn.ReLU(inplace=True),
28 | #nn.BatchNorm1d(1024),
29 | #nn.Linear(1024, 1024, bias=True),
30 | #nn.ReLU(inplace=True),
31 | #nn.BatchNorm1d(1024),
32 | #nn.Linear(1024, 20, bias=True)
33 | )
34 |
35 | self.phase2dlayer = Phase2DLayer(20,128)
36 |
37 | def forward(self, x):
38 | # 128x128x2
39 | z = self.densenet(x)
40 | phase = self.phase2dlayer(z)
41 | return phase, z
42 |
43 | class Phase2D(torch.autograd.Function):
44 |
45 | @staticmethod
46 | def forward(ctx, input, z_basis):
47 | ctx.z_basis = z_basis.cpu()#.cuda()
48 | output = input[:,:, None, None] * ctx.z_basis[None, 1:,:,:]
49 | return torch.sum(output, dim=1)
50 |
51 | @staticmethod
52 | def backward(ctx, grad_output):
53 | dL_dy = grad_output.unsqueeze(1)
54 | dy_dz = ctx.z_basis[1:,:,:].unsqueeze(0)
55 | grad_input = torch.sum(dL_dy * dy_dz, dim=(2,3))
56 | return grad_input, None
57 |
58 | class Phase2DLayer(nn.Module):
59 | def __init__(self, input_features, output_features):
60 | super(Phase2DLayer, self).__init__()
61 | self.input_features = input_features
62 | self.output_features = output_features
63 | self.z_basis = aotools.zernikeArray(input_features+1, output_features, norm='rms')
64 | self.z_basis = torch.as_tensor(self.z_basis, dtype=torch.float32)
65 |
66 | def forward(self, input):
67 | return Phase2D.apply(input, self.z_basis)
68 |
69 |
70 | class BasicConv2d(nn.Module):
71 |
72 | def __init__(self, in_channels, out_channels, **kwargs):
73 | super(BasicConv2d, self).__init__()
74 | self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
75 | self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
76 |
77 | def forward(self, x):
78 | x = self.conv(x)
79 | x = self.bn(x)
80 | return F.relu(x, inplace=True)
--------------------------------------------------------------------------------
/src/pytorch/models/InceptionV3.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import numpy as np
3 | import aotools
4 | import torch.nn as nn
5 | import torch.nn.functional as F
6 | import torchvision.models as models
7 |
8 | class Net(nn.Module):
9 |
10 | def __init__(self):
11 | super(Net, self).__init__()
12 |
13 | self.inception = models.inception_v3(pretrained=True, transform_input=False)
14 |
15 | for param in self.inception.parameters():
16 | param.requires_grad = True
17 |
18 | # Input size
19 | first_conv_layer = [nn.Conv2d(2, 3, kernel_size=1, stride=1, bias=True),
20 | nn.AdaptiveMaxPool2d(299),
21 | self.inception.Conv2d_1a_3x3]
22 | self.inception.Conv2d_1a_3x3= nn.Sequential(*first_conv_layer)
23 |
24 | # Fit classifier
25 | self.inception.fc = nn.Sequential(
26 | nn.Linear(2048, 20),
27 | #nn.ReLU(inplace=True),
28 | #nn.BatchNorm1d(2048),
29 | #nn.Linear(2048, 1024),
30 | #nn.ReLU(inplace=True),
31 | #nn.BatchNorm1d(2048),
32 | #nn.Linear(1024, 20)
33 | )
34 |
35 | self.phase2dlayer = Phase2DLayer(20,128)
36 |
37 | def forward(self, x):
38 | if self.inception.training:
39 | z, _ = self.inception(x)
40 | else:
41 | z = self.inception(x)
42 | phase = self.phase2dlayer(z)
43 | return phase, z
44 |
45 | class Phase2D(torch.autograd.Function):
46 |
47 | @staticmethod
48 | def forward(ctx, input, z_basis):
49 | ctx.z_basis = z_basis.cpu()#.cuda()
50 | output = input[:,:, None, None] * ctx.z_basis[None, 1:,:,:]
51 | return torch.sum(output, dim=1)
52 |
53 | @staticmethod
54 | def backward(ctx, grad_output):
55 | dL_dy = grad_output.unsqueeze(1)
56 | dy_dz = ctx.z_basis[1:,:,:].unsqueeze(0)
57 | grad_input = torch.sum(dL_dy * dy_dz, dim=(2,3))
58 | return grad_input, None
59 |
60 | class Phase2DLayer(nn.Module):
61 | def __init__(self, input_features, output_features):
62 | super(Phase2DLayer, self).__init__()
63 | self.input_features = input_features
64 | self.output_features = output_features
65 | self.z_basis = aotools.zernikeArray(input_features+1, output_features, norm='rms')
66 | self.z_basis = torch.as_tensor(self.z_basis, dtype=torch.float32)
67 |
68 | def forward(self, input):
69 | return Phase2D.apply(input, self.z_basis)
70 |
71 |
72 | class BasicConv2d(nn.Module):
73 |
74 | def __init__(self, in_channels, out_channels, **kwargs):
75 | super(BasicConv2d, self).__init__()
76 | self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
77 | self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
78 |
79 | def forward(self, x):
80 | x = self.conv(x)
81 | x = self.bn(x)
82 | return F.relu(x, inplace=True)
--------------------------------------------------------------------------------
/src/pytorch/models/Resnet.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import numpy as np
3 | import aotools
4 | import torch.nn as nn
5 | import torch.nn.functional as F
6 | import torchvision.models as models
7 |
8 | class Net(nn.Module):
9 |
10 | def __init__(self):
11 | super(Net, self).__init__()
12 |
13 | self.resnet = models.resnet50(pretrained=True)
14 |
15 | for param in self.resnet.parameters():
16 | param.requires_grad = True
17 |
18 | # Input size 2x128x128 -> 2x224x224
19 | first_conv_layer = [nn.Conv2d(2, 3, kernel_size=1, stride=1, bias=True),
20 | nn.AdaptiveMaxPool2d(224),
21 | self.resnet.conv1]
22 | self.resnet.conv1= nn.Sequential(*first_conv_layer)
23 |
24 | # Fit classifier
25 | self.resnet.fc = nn.Sequential(
26 | nn.Linear(2048, 100),
27 | #nn.ReLU(inplace=True),
28 | #nn.BatchNorm1d(1024),
29 | #nn.Linear(1024, 1024),
30 | #nn.ReLU(inplace=True),
31 | #nn.BatchNorm1d(1024),
32 | #nn.Linear(1024, 20)
33 | )
34 |
35 | self.phase2dlayer = Phase2DLayer(100,128)
36 |
37 | def forward(self, x):
38 | # 128x128x2
39 | z = self.resnet(x)
40 | phase = self.phase2dlayer(z)
41 | return phase
42 |
43 | class Phase2D(torch.autograd.Function):
44 |
45 | @staticmethod
46 | def forward(ctx, input, z_basis):
47 | ctx.z_basis = z_basis.cuda()
48 | output = input[:,:, None, None] * ctx.z_basis[None, 1:,:,:]
49 | return torch.sum(output, dim=1)
50 |
51 | @staticmethod
52 | def backward(ctx, grad_output):
53 | dL_dy = grad_output.unsqueeze(1)
54 | dy_dz = ctx.z_basis[1:,:,:].unsqueeze(0)
55 | grad_input = torch.sum(dL_dy * dy_dz, dim=(2,3))
56 | return grad_input, None
57 |
58 | class Phase2DLayer(nn.Module):
59 | def __init__(self, input_features, output_features):
60 | super(Phase2DLayer, self).__init__()
61 | self.input_features = input_features
62 | self.output_features = output_features
63 | self.z_basis = aotools.zernikeArray(input_features+1, output_features, norm='rms')
64 | self.z_basis = torch.as_tensor(self.z_basis, dtype=torch.float32)
65 |
66 | def forward(self, input):
67 | return Phase2D.apply(input, self.z_basis)
68 |
--------------------------------------------------------------------------------
/src/pytorch/models/Unet.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | import torch.nn.functional as F
4 |
5 | class UNet(nn.Module):
6 | def __init__(self, n_channels_in, n_channels_out):
7 | super(UNet, self).__init__()
8 | self.inc = inconv(n_channels_in, 64)
9 | self.down1 = down(64, 128)
10 | self.down2 = down(128, 256)
11 | self.down3 = down(256, 512)
12 | self.down4 = down(512, 512)
13 | self.up1 = up(1024, 256, bilinear=True)
14 | self.up2 = up(512, 128, bilinear=True)
15 | self.up3 = up(256, 64, bilinear=True)
16 | self.up4 = up(128, 64, bilinear=True)
17 | self.outc = outconv(64, n_channels_out)
18 |
19 | def forward(self, x):
20 | x1 = self.inc(x)
21 | x2 = self.down1(x1)
22 | x3 = self.down2(x2)
23 | x4 = self.down3(x3)
24 | x5 = self.down4(x4)
25 | x = self.up1(x5, x4)
26 | x = self.up2(x, x3)
27 | x = self.up3(x, x2)
28 | x = self.up4(x, x1)
29 | x = self.outc(x)
30 | return x
31 |
32 | class double_conv(nn.Module):
33 | '''(conv => BN => ReLU) * 2'''
34 | def __init__(self, in_ch, out_ch):
35 | super(double_conv, self).__init__()
36 | self.conv = nn.Sequential(
37 | nn.Conv2d(in_ch, out_ch, 3, padding=1),
38 | nn.BatchNorm2d(out_ch),
39 | nn.ReLU(inplace=True),
40 | nn.Conv2d(out_ch, out_ch, 3, padding=1),
41 | nn.BatchNorm2d(out_ch),
42 | nn.ReLU(inplace=True)
43 | )
44 |
45 | def forward(self, x):
46 | x = self.conv(x)
47 | return x
48 |
49 | class inconv(nn.Module):
50 | def __init__(self, in_ch, out_ch):
51 | super(inconv, self).__init__()
52 | self.conv = double_conv(in_ch, out_ch)
53 |
54 | def forward(self, x):
55 | x = self.conv(x)
56 | return x
57 |
58 | class down(nn.Module):
59 | def __init__(self, in_ch, out_ch):
60 | super(down, self).__init__()
61 | self.mpconv = nn.Sequential(
62 | nn.MaxPool2d(2),
63 | double_conv(in_ch, out_ch)
64 | )
65 |
66 | def forward(self, x):
67 | x = self.mpconv(x)
68 | return x
69 |
70 | class up(nn.Module):
71 | def __init__(self, in_ch, out_ch, bilinear=True):
72 | super(up, self).__init__()
73 |
74 | # would be a nice idea if the upsampling could be learned too,
75 | # but my machine do not have enough memory to handle all those weights
76 | if bilinear:
77 | self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
78 | else:
79 | self.up = nn.ConvTranspose2d(in_ch//2, in_ch//2, 2, stride=2)
80 |
81 | self.conv = double_conv(in_ch, out_ch)
82 |
83 | def forward(self, x1, x2):
84 | x1 = self.up(x1)
85 |
86 | # input is CHW
87 | diffY = x2.size()[2] - x1.size()[2]
88 | diffX = x2.size()[3] - x1.size()[3]
89 |
90 | x1 = F.pad(x1, (diffX // 2, diffX - diffX//2,
91 | diffY // 2, diffY - diffY//2))
92 |
93 | x = torch.cat([x2, x1], dim=1)
94 | x = self.conv(x)
95 | return x
96 |
97 | class outconv(nn.Module):
98 | def __init__(self, in_ch, out_ch):
99 | super(outconv, self).__init__()
100 | self.conv = nn.Conv2d(in_ch, out_ch, 3, padding=1)
101 |
102 | def forward(self, x):
103 | x = self.conv(x)
104 | return x
105 |
106 |
--------------------------------------------------------------------------------
/src/pytorch/models/Unet_PP.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import numpy as np
3 |
4 | from torch import nn
5 | from torch.nn import functional as F
6 | import torch
7 | from torchvision import models
8 | import torchvision
9 |
10 |
11 | class VGGBlock(nn.Module):
12 | def __init__(self, in_channels, middle_channels, out_channels, act_func=nn.ReLU(inplace=True)):
13 | super(VGGBlock, self).__init__()
14 | self.act_func = act_func
15 | self.conv1 = nn.Conv2d(in_channels, middle_channels, 3, padding=1)
16 | self.bn1 = nn.BatchNorm2d(middle_channels)
17 | self.conv2 = nn.Conv2d(middle_channels, out_channels, 3, padding=1)
18 | self.bn2 = nn.BatchNorm2d(out_channels)
19 |
20 | def forward(self, x):
21 | out = self.conv1(x)
22 | out = self.bn1(out)
23 | out = self.act_func(out)
24 |
25 | out = self.conv2(out)
26 | out = self.bn2(out)
27 | out = self.act_func(out)
28 |
29 | return out
30 |
31 |
32 | class UNet(nn.Module):
33 | def __init__(self, args):
34 | super().__init__()
35 |
36 | self.args = args
37 |
38 | nb_filter = [32, 64, 128, 256, 512]
39 |
40 | self.pool = nn.MaxPool2d(2, 2)
41 | self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
42 |
43 | self.conv0_0 = VGGBlock(args.input_channels, nb_filter[0], nb_filter[0])
44 | self.conv1_0 = VGGBlock(nb_filter[0], nb_filter[1], nb_filter[1])
45 | self.conv2_0 = VGGBlock(nb_filter[1], nb_filter[2], nb_filter[2])
46 | self.conv3_0 = VGGBlock(nb_filter[2], nb_filter[3], nb_filter[3])
47 | self.conv4_0 = VGGBlock(nb_filter[3], nb_filter[4], nb_filter[4])
48 |
49 | self.conv3_1 = VGGBlock(nb_filter[3]+nb_filter[4], nb_filter[3], nb_filter[3])
50 | self.conv2_2 = VGGBlock(nb_filter[2]+nb_filter[3], nb_filter[2], nb_filter[2])
51 | self.conv1_3 = VGGBlock(nb_filter[1]+nb_filter[2], nb_filter[1], nb_filter[1])
52 | self.conv0_4 = VGGBlock(nb_filter[0]+nb_filter[1], nb_filter[0], nb_filter[0])
53 |
54 | self.final = nn.Conv2d(nb_filter[0], 1, kernel_size=1)
55 |
56 |
57 | def forward(self, input):
58 | x0_0 = self.conv0_0(input)
59 | x1_0 = self.conv1_0(self.pool(x0_0))
60 | x2_0 = self.conv2_0(self.pool(x1_0))
61 | x3_0 = self.conv3_0(self.pool(x2_0))
62 | x4_0 = self.conv4_0(self.pool(x3_0))
63 |
64 | x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))
65 | x2_2 = self.conv2_2(torch.cat([x2_0, self.up(x3_1)], 1))
66 | x1_3 = self.conv1_3(torch.cat([x1_0, self.up(x2_2)], 1))
67 | x0_4 = self.conv0_4(torch.cat([x0_0, self.up(x1_3)], 1))
68 |
69 | output = self.final(x0_4)
70 | return output
71 |
72 |
73 | class NestedUNet(nn.Module):
74 | def __init__(self):
75 | super().__init__()
76 |
77 | #self.args = args
78 |
79 | nb_filter = [32, 64, 96, 128, 256]
80 |
81 | self.pool = nn.MaxPool2d(2, 2)
82 | self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
83 |
84 | self.conv0_0 = VGGBlock(2, nb_filter[0], nb_filter[0])
85 | self.conv1_0 = VGGBlock(nb_filter[0], nb_filter[1], nb_filter[1])
86 | self.conv2_0 = VGGBlock(nb_filter[1], nb_filter[2], nb_filter[2])
87 | self.conv3_0 = VGGBlock(nb_filter[2], nb_filter[3], nb_filter[3])
88 | self.conv4_0 = VGGBlock(nb_filter[3], nb_filter[4], nb_filter[4])
89 |
90 | self.conv0_1 = VGGBlock(nb_filter[0]+nb_filter[1], nb_filter[0], nb_filter[0])
91 | self.conv1_1 = VGGBlock(nb_filter[1]+nb_filter[2], nb_filter[1], nb_filter[1])
92 | self.conv2_1 = VGGBlock(nb_filter[2]+nb_filter[3], nb_filter[2], nb_filter[2])
93 | self.conv3_1 = VGGBlock(nb_filter[3]+nb_filter[4], nb_filter[3], nb_filter[3])
94 |
95 | self.conv0_2 = VGGBlock(nb_filter[0]*2+nb_filter[1], nb_filter[0], nb_filter[0])
96 | self.conv1_2 = VGGBlock(nb_filter[1]*2+nb_filter[2], nb_filter[1], nb_filter[1])
97 | self.conv2_2 = VGGBlock(nb_filter[2]*2+nb_filter[3], nb_filter[2], nb_filter[2])
98 |
99 | self.conv0_3 = VGGBlock(nb_filter[0]*3+nb_filter[1], nb_filter[0], nb_filter[0])
100 | self.conv1_3 = VGGBlock(nb_filter[1]*3+nb_filter[2], nb_filter[1], nb_filter[1])
101 |
102 | self.conv0_4 = VGGBlock(nb_filter[0]*4+nb_filter[1], nb_filter[0], nb_filter[0])
103 |
104 | #if self.args.deepsupervision:
105 | # self.final1 = nn.Conv2d(nb_filter[0], 1, kernel_size=1)
106 | # self.final2 = nn.Conv2d(nb_filter[0], 1, kernel_size=1)
107 | # self.final3 = nn.Conv2d(nb_filter[0], 1, kernel_size=1)
108 | # self.final4 = nn.Conv2d(nb_filter[0], 1, kernel_size=1)
109 | #else:
110 | self.final = nn.Conv2d(nb_filter[0], 1, kernel_size=1)
111 |
112 |
113 | def forward(self, input):
114 | x0_0 = self.conv0_0(input)
115 | x1_0 = self.conv1_0(self.pool(x0_0))
116 | x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1))
117 |
118 | x2_0 = self.conv2_0(self.pool(x1_0))
119 | x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1))
120 | x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1))
121 |
122 | x3_0 = self.conv3_0(self.pool(x2_0))
123 | x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1))
124 | x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1))
125 | x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1))
126 |
127 | x4_0 = self.conv4_0(self.pool(x3_0))
128 | x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))
129 | x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1))
130 | x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1))
131 | x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1))
132 | '''
133 | if self.args.deepsupervision:
134 | output1 = self.final1(x0_1)
135 | output2 = self.final2(x0_2)
136 | output3 = self.final3(x0_3)
137 | output4 = self.final4(x0_4)
138 | return [output1, output2, output3, output4]
139 |
140 | else:
141 | '''
142 | output = self.final(x0_4)
143 | return output
144 |
--------------------------------------------------------------------------------
/src/pytorch/models/VGG.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import numpy as np
3 | import aotools
4 | import torch.nn as nn
5 | import torch.nn.functional as F
6 |
7 | class Net(nn.Module):
8 |
9 | def __init__(self):
10 | super(Net, self).__init__()
11 |
12 | self.conv_a1 = BasicConv2d(2, 32, kernel_size=3, stride=1, padding=1)
13 | self.conv_a2 = BasicConv2d(32, 32, kernel_size=3, stride=1, padding=1)
14 |
15 | self.conv_b1 = BasicConv2d(32, 64, kernel_size=3, stride=1, padding=1)
16 | self.conv_b2 = BasicConv2d(64, 64, kernel_size=3, stride=1, padding=1)
17 |
18 | self.conv_c1 = BasicConv2d(64, 128, kernel_size=3, stride=1, padding=1)
19 | self.conv_c2 = BasicConv2d(128, 128, kernel_size=3, stride=1, padding=1)
20 |
21 | self.conv_d1 = BasicConv2d(128, 256, kernel_size=3, stride=1, padding=1)
22 | self.conv_d2 = BasicConv2d(256, 256, kernel_size=3, stride=1, padding=1)
23 |
24 | self.conv_e1 = BasicConv2d(256, 512, kernel_size=3, stride=1, padding=1)
25 | self.conv_e2 = BasicConv2d(512, 512, kernel_size=3, stride=1, padding=1)
26 |
27 | self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
28 |
29 | self.fc1 = torch.nn.Linear(4*4*512, 1024)
30 | #self.fc1_bn = nn.BatchNorm1d(1024)
31 | self.fc2 = torch.nn.Linear(1024, 1024)
32 | #self.fc2_bn = nn.BatchNorm1d(1024)
33 | self.fc3 = torch.nn.Linear(1024, 20)
34 |
35 | self.phase2dlayer = Phase2DLayer(20,128)
36 |
37 | def forward(self, x):
38 | # 128x128x2
39 | x = self.conv_a1(x) # 128x128x16
40 | x = self.conv_a2(x)
41 | x = self.pool(x)
42 | x = self.conv_b1(x) # 64x64x32
43 | x = self.conv_b2(x)
44 | x = self.pool(x)
45 | x = self.conv_c1(x) # 32x32x64
46 | x = self.conv_c2(x)
47 | x = self.pool(x)
48 | x = self.conv_d1(x) # 16x16x128
49 | x = self.conv_d2(x)
50 | x = self.pool(x)
51 | x = self.conv_e1(x) # 8x8x512
52 | x = self.conv_e2(x) # 8x8x512
53 | x = self.pool(x)
54 | x = x.view(-1, 4*4*512)
55 | x = F.relu(self.fc1(x))
56 | x = F.relu(self.fc2(x))
57 | z_coeffs = self.fc3(x)
58 | phase = self.phase2dlayer(z_coeffs)
59 | return phase, z_coeffs
60 |
61 |
62 | class Phase2D(torch.autograd.Function):
63 |
64 | @staticmethod
65 | def forward(ctx, input, z_basis):
66 | ctx.z_basis = z_basis.cpu() #.cuda()
67 | output = input[:,:, None, None] * ctx.z_basis[None, 1:,:,:]
68 | return torch.sum(output, dim=1)
69 |
70 | @staticmethod
71 | def backward(ctx, grad_output):
72 | dL_dy = grad_output.unsqueeze(1)
73 | dy_dz = ctx.z_basis[1:,:,:].unsqueeze(0)
74 | grad_input = torch.sum(dL_dy * dy_dz, dim=(2,3))
75 | return grad_input, None
76 |
77 | class Phase2DLayer(nn.Module):
78 | def __init__(self, input_features, output_features):
79 | super(Phase2DLayer, self).__init__()
80 | self.input_features = input_features
81 | self.output_features = output_features
82 | self.z_basis = aotools.zernikeArray(input_features+1, output_features, norm='rms')
83 | self.z_basis = torch.as_tensor(self.z_basis, dtype=torch.float32)
84 |
85 | def forward(self, input):
86 | return Phase2D.apply(input, self.z_basis)
87 |
88 |
89 | class BasicConv2d(nn.Module):
90 |
91 | def __init__(self, in_channels, out_channels, **kwargs):
92 | super(BasicConv2d, self).__init__()
93 | self.conv = nn.Conv2d(in_channels, out_channels, bias=True, **kwargs)
94 | self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
95 |
96 | def forward(self, x):
97 | x = self.conv(x)
98 | x = self.bn(x)
99 | return F.relu(x, inplace=True)
--------------------------------------------------------------------------------
/src/pytorch/models/__pycache__/Unet.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/povanberg/Machine-learning-for-image-based-wavefront-sensing/a687f422d822a7c1db76375a0a4a67a60b03721d/src/pytorch/models/__pycache__/Unet.cpython-36.pyc
--------------------------------------------------------------------------------
/src/pytorch/train.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import utils
4 | import json
5 | import logging
6 | import torch
7 | import torch.nn as nn
8 | import torch.nn.functional as F
9 | import torch.optim as optim
10 | from torchvision import transforms
11 | from dataset import psf_dataset, splitDataLoader, ToTensor, Normalize
12 | from utils_visdom import VisdomWebServer
13 | import aotools
14 | from criterion import *
15 |
16 | def train(model, dataset, optimizer, criterion, split=[0.9, 0.1], batch_size=32,
17 | n_epochs=1, model_dir='./', random_seed=None, visdom=False):
18 |
19 | # Create directory if doesn't exist
20 | if not os.path.exists(model_dir):
21 | os.makedirs(model_dir)
22 |
23 | # Logging
24 | log_path = os.path.join(model_dir, 'logs.log')
25 | utils.set_logger(log_path)
26 |
27 | # Visdom support
28 | if visdom:
29 | vis = VisdomWebServer()
30 |
31 | # Dataset
32 | dataloaders = {}
33 | dataloaders['train'], dataloaders['val'] = splitDataLoader(dataset, split=split,
34 | batch_size=batch_size, random_seed=random_seed)
35 |
36 | # ---
37 | scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=150, gamma=0.1)
38 | #scheduler = CosineWithRestarts(optimizer, T_max=40, eta_min=1e-7, last_epoch=-1)
39 | #scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-7, last_epoch=-1)
40 |
41 | # Metrics
42 | metrics_path = os.path.join(model_dir, 'metrics.json')
43 |
44 | metrics = {
45 | 'model': model_dir,
46 | 'optimizer': optimizer.__class__.__name__,
47 | 'criterion': criterion.__class__.__name__,
48 | 'scheduler': scheduler.__class__.__name__,
49 | 'dataset_size': int(len(dataset)),
50 | 'train_size': int(split[0]*len(dataset)),
51 | 'test_size': int(split[1]*len(dataset)),
52 | 'n_epoch': n_epochs,
53 | 'batch_size': batch_size,
54 | 'learning_rate': [],
55 | 'train_loss': [],
56 | 'val_loss': [],
57 | 'zernike_train_loss': [],
58 | 'zernike_val_loss': []
59 | }
60 |
61 | # Zernike basis
62 | z_basis = torch.as_tensor(aotools.zernikeArray(100+1, 128, norm='rms'), dtype=torch.float32)
63 |
64 | # Device
65 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
66 |
67 | # Training
68 | since = time.time()
69 | dataset_size = {
70 | 'train':int(split[0]*len(dataset)),
71 | 'val':int(split[1]*len(dataset))
72 | }
73 |
74 |
75 | best_loss = 0.0
76 |
77 | for epoch in range(n_epochs):
78 |
79 | logging.info('-'*30)
80 | epoch_time = time.time()
81 |
82 | # Each epoch has a training and validation phase
83 | for phase in ['train', 'val']:
84 | if phase == 'train':
85 | model.train() # Set model to training mode
86 | else:
87 | model.eval() # Set model to evaluate mode
88 |
89 | running_loss = 0.0
90 | zernike_loss =0.0
91 |
92 | for _, sample in enumerate(dataloaders[phase]):
93 | # GPU support
94 | inputs = sample['image'].to(device)
95 | phase_0 = sample['phase'].to(device)
96 |
97 | # zero the parameter gradients
98 | optimizer.zero_grad()
99 |
100 | # forward: track history if only in train
101 | with torch.set_grad_enabled(phase == 'train'):
102 |
103 | # Network return phase and zernike coeffs
104 | phase_estimation = model(inputs)
105 | loss = criterion(torch.squeeze(phase_estimation), phase_0)
106 |
107 | # backward
108 | if phase == 'train':
109 | loss.backward()
110 | optimizer.step()
111 |
112 | running_loss += 1 * loss.item() * inputs.size(0)
113 |
114 | logging.info('[%i/%i] %s loss: %f' % (epoch+1, n_epochs, phase, running_loss / dataset_size[phase]))
115 |
116 | # Update metrics
117 | metrics[phase+'_loss'].append(running_loss / dataset_size[phase])
118 | #metrics['zernike_'+phase+'_loss'].append(zernike_loss / dataset_size[phase])
119 | if phase=='train':
120 | metrics['learning_rate'].append(get_lr(optimizer))
121 |
122 | # Adaptive learning rate
123 | if phase == 'val':
124 | scheduler.step()
125 | # Save weigths
126 | if epoch == 0 or running_loss < best_loss:
127 | best_loss = running_loss
128 | model_path = os.path.join(model_dir, 'model.pth')
129 | torch.save(model.state_dict(), model_path)
130 | # Save metrics
131 | with open(metrics_path, 'w') as f:
132 | json.dump(metrics, f, indent=4)
133 | # Visdom update
134 | if visdom:
135 | vis.update(metrics)
136 |
137 | logging.info('[%i/%i] Time: %f s' % (epoch + 1, n_epochs, time.time()-epoch_time))
138 |
139 | time_elapsed = time.time() - since
140 | logging.info('[-----] All epochs completed in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
141 |
142 |
143 |
144 | def get_lr(optimizer):
145 | for p in optimizer.param_groups:
146 | lr = p['lr']
147 | return lr
148 |
--------------------------------------------------------------------------------
/src/pytorch/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import json
3 | import os
4 | import matplotlib.pyplot as plt
5 |
6 | def set_logger(log_path):
7 | """
8 | Set the logger to log info in terminal and file `log_path`.
9 |
10 | Args:
11 | log_path: (string) where to log
12 | """
13 | logger = logging.getLogger()
14 | logger.setLevel(logging.INFO)
15 |
16 | if not logger.handlers:
17 | # Logging to a file
18 | file_handler = logging.FileHandler(log_path)
19 | file_handler.setFormatter(logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s'))
20 | logger.addHandler(file_handler)
21 |
22 | # Logging to console
23 | stream_handler = logging.StreamHandler()
24 | stream_handler.setFormatter(logging.Formatter('%(message)s'))
25 | logger.addHandler(stream_handler)
26 |
27 |
28 |
29 |
30 | class Params():
31 | """
32 | Class that loads hyperparameters from a json file.
33 |
34 | params = Params(json_path)
35 | print(params.learning_rate)
36 | params.learning_rate = 0.5 # change the value of learning_rate in params
37 | """
38 |
39 | def __init__(self, json_path):
40 |
41 | if not os.path.exists(json_path):
42 | with open(json_path, 'w') as f:
43 | data = {}
44 | json.dump(data, f, indent=4)
45 |
46 | with open(json_path) as f:
47 | params = json.load(f)
48 | self.__dict__.update(params)
49 |
50 | def save(self, json_path):
51 | with open(json_path, 'w') as f:
52 | json.dump(self.__dict__, f, indent=4)
53 |
54 | def update(self, json_path):
55 | """Loads parameters from json file"""
56 | with open(json_path) as f:
57 | params = json.load(f)
58 | self.__dict__.update(params)
59 |
60 | def hasKey(self, json_path, key_name):
61 | bool_key = False
62 | with open(json_path) as f:
63 | params = json.load(f)
64 | if key_name in params:
65 | bool_key = True
66 |
67 | return bool_key
68 |
69 | @property
70 | def dict(self):
71 | """Gives dict-like access to Params instance by `params.dict['learning_rate']"""
72 | return self.__dict__
73 |
74 |
75 | def plot_learningcurve(metrics, save=True, show=True, name='lrcurve.pdf',
76 | xlim=[None,None], ylim=[None,None], zernike=False):
77 | import numpy as np
78 | plt.figure()
79 | #x = np.arange(200)
80 | #plt.plot(x, np.array(metrics['train_loss' if not zernike else 'zernike_train_loss'])[x]/(0.8*np.log(x)), label='Training loss', color='blue')
81 | plt.plot(metrics['train_loss' if not zernike else 'zernike_train_loss'][:], label='Training loss', color='blue')
82 | plt.plot(metrics['val_loss' if not zernike else 'zernike_val_loss'][:], label='Validation loss', color='red')
83 | plt.legend()
84 | plt.grid()
85 | plt.xlim(xlim[0], xlim[1])
86 | plt.ylim(ylim[0], ylim[1])
87 | plt.xlabel('epochs')
88 | plt.ylabel('loss')
89 | if save: plt.savefig(name)
90 | if show: plt.show()
91 |
92 |
93 | def get_metrics(model_dir=''):
94 |
95 | metrics_path = os.path.join(model_dir, 'metrics.json')
96 |
97 | with open(metrics_path) as f:
98 | metrics = json.load(f)
99 | return metrics
100 |
101 | return None
102 |
--------------------------------------------------------------------------------
/src/pytorch/utils_model.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | import torch.nn.functional as F
4 |
5 | class BasicConv2d(nn.Module):
6 | def __init__(self, in_channels, out_channels, **kwargs):
7 | super(BasicConv2d, self).__init__()
8 | self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
9 | self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
10 |
11 | def forward(self, x):
12 | x = self.conv(x)
13 | x = self.bn(x)
14 | return F.relu(x, inplace=True)
15 |
16 |
17 | class Phase2D(torch.autograd.Function):
18 |
19 | @staticmethod
20 | def forward(ctx, input, z_basis):
21 | ctx.z_basis = z_basis.cuda()
22 | output = input[:,:, None, None] * ctx.z_basis[None, 1:,:,:]
23 | return torch.sum(output, dim=1)
24 |
25 | @staticmethod
26 | def backward(ctx, grad_output):
27 | dL_dy = grad_output.unsqueeze(1)
28 | dy_dz = ctx.z_basis[1:,:,:].unsqueeze(0)
29 | grad_input = torch.sum(dL_dy * dy_dz, dim=(2,3))
30 | return grad_input, None
31 |
32 | class Phase2DLayer(nn.Module):
33 | def __init__(self, input_features, output_features):
34 | super(Phase2DLayer, self).__init__()
35 | self.input_features = input_features
36 | self.output_features = output_features
37 | self.z_basis = aotools.zernikeArray(input_features+1, output_features, norm='rms')
38 | self.z_basis = torch.as_tensor(self.z_basis, dtype=torch.float32)
39 |
40 | def forward(self, input):
41 | return Phase2D.apply(input, self.z_basis)
42 |
--------------------------------------------------------------------------------
/src/pytorch/utils_visdom.py:
--------------------------------------------------------------------------------
1 | from visdom import Visdom
2 | import numpy as np
3 | import matplotlib.pyplot as plt
4 | import json
5 | from utils import plot_learningcurve
6 |
7 | # Start web server with: python -m visdom.server
8 |
9 | class VisdomWebServer(object):
10 |
11 | def __init__(self):
12 |
13 | DEFAULT_PORT = 8097
14 | DEFAULT_HOSTNAME = "http://localhost"
15 |
16 | self.vis = Visdom(port=DEFAULT_PORT, server=DEFAULT_HOSTNAME)
17 |
18 | def update(self, metrics):
19 |
20 | if not self.vis.check_connection():
21 | 'No connection could be formed quickly'
22 | return
23 |
24 | # Learning curve
25 | try:
26 | fig, ax = plt.subplots()
27 | plt.plot(metrics['train_loss'], label='Training loss', color='#32526e')
28 | plt.plot(metrics['val_loss'], label='Validation loss', color='#ff6b57')
29 | plt.legend()
30 | ax.spines['right'].set_visible(False)
31 | ax.spines['top'].set_visible(False)
32 | plt.grid(zorder=0, color='lightgray', linestyle='--')
33 | self.vis.matplot(plt, win='lrcurve')
34 | plt.close()
35 | plt.clf()
36 |
37 | fig, ax = plt.subplots()
38 | plt.plot(metrics['learning_rate'], color='#32526e')
39 | ax.spines['right'].set_visible(False)
40 | ax.spines['top'].set_visible(False)
41 | plt.grid(zorder=0, color='lightgray', linestyle='--')
42 | self.vis.matplot(plt, win='lr_rate')
43 | plt.close()
44 | plt.clf()
45 |
46 | #plt.figure()
47 | #plt.plot(metrics['zernike_train_loss'], label='Zernike train loss', color='blue')
48 | #plt.plot(metrics['zernike_val_loss'], label='Zernike val loss', color='red')
49 | #plt.legend()
50 | #plt.grid()
51 | #self.vis.matplot(plt, win='lrcurve_z')
52 | #plt.close()
53 | #plt.clf()
54 | except BaseException as err:
55 | print('Skipped matplotlib example')
56 | print('Error message: ', err)
57 |
58 |
59 |
60 | if __name__ == "__main__":
61 |
62 | from utils import get_metrics
63 |
64 | metrics = get_metrics('experiments/example')
65 |
66 | visdom = VisdomWebServer()
67 | visdom.update(metrics)
68 |
69 |
--------------------------------------------------------------------------------