├── 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 | --------------------------------------------------------------------------------