├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── examples.ipynb ├── microscPSF ├── __init__.py ├── microscPSF.py └── test │ └── test.py ├── setup.cfg └── setup.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install pytest numpy scipy 31 | python -m pip install -e . 32 | - name: Test with pytest 33 | run: | 34 | pytest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############# 2 | ## Python 3 | ############# 4 | 5 | *.py[co] 6 | *.egg-info 7 | build/ 8 | *.eggs 9 | dist/ 10 | .cache/ 11 | .DS_STORE 12 | storm_analysis/test/output/ 13 | *__pycache__* 14 | 15 | ############# 16 | ## Emacs 17 | ############# 18 | 19 | *~ 20 | 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | # Use containers. 3 | sudo: false 4 | 5 | dist: xenial 6 | 7 | language: python 8 | 9 | python: 10 | - 2.7 11 | - 3.6 12 | 13 | install: 14 | - pip install --upgrade pip 15 | - pip install numpy 16 | - pip install pytest 17 | - pip install scipy 18 | 19 | script: 20 | - python setup.py install 21 | - pytest 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Kyle Douglass and Hazen Babcock 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include microscPSF *.md 2 | recursive-include microscPSF *.py 3 | recursive-include microscPSF *.txt 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MicroscPSF-Py ## 2 | 3 | This is a Python implementation of the fast microscope PSF generation tool (using the Gibson-Lanni model). 4 | Technical details can be found [here](http://jizhou.li/project/microsc_psf). 5 | 6 | [![PyPI version](https://badge.fury.io/py/MicroscPSF-Py.svg)](https://badge.fury.io/py/MicroscPSF-Py) 7 | 8 | ### Install ### 9 | 10 | #### PyPI #### 11 | 12 | ``` 13 | $ python -m pip install MicroscPSF-Py 14 | ```` 15 | 16 | #### Source #### 17 | 18 | ``` 19 | $ git clone https://github.com/MicroscPSF/MicroscPSF-Py.git 20 | $ cd MicroscPSF-Py 21 | $ python setup.py install 22 | ``` 23 | 24 | ### Usage ### 25 | 26 | Please see the [examples.ipynb](https://github.com/MicroscPSF/MicroscPSF-Py/blob/master/examples.ipynb) Jupyter notebook. 27 | 28 | ### Acknowledgements ### 29 | 30 | - Original algorithm Li, J., Xue, F., & Blu, T. (2017). Fast and accurate three-dimensional point spread function computation for fluorescence microscopy. JOSA A, 34(6), 1029-1034. [link](https://doi.org/10.1364/JOSAA.34.001029). 31 | 32 | - Python implementation by Kyle Douglass and Hazen Babcock. 33 | -------------------------------------------------------------------------------- /examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Examples of how to use the microscPSF package.\n", 8 | "\n", 9 | "Notes:\n", 10 | "* All units are microns.\n", 11 | "* Particle z position is a positive value relative to the coverslip surface (z = 0.0).\n", 12 | "* Focusing into the sample is a negative value relative to the coverslip surface (the distance between the objective and the coverslip is reduced).\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import inspect\n", 22 | "import numpy\n", 23 | "import matplotlib.pyplot as pyplot\n", 24 | "\n", 25 | "import microscPSF.microscPSF as msPSF\n" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "### Microscope parameters\n", 33 | "\n", 34 | "All distance / length parameters are in units of microns." 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "# Load and print the default microscope parameters.\n", 44 | "for key in sorted(msPSF.m_params):\n", 45 | " print(key, msPSF.m_params[key])\n", 46 | "print()\n", 47 | "\n", 48 | "# You can find more information about what these are in this file:\n", 49 | "print(inspect.getfile(msPSF))\n" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "# We'll use this for drawing PSFs.\n", 59 | "#\n", 60 | "# Note that we display the sqrt of the PSF.\n", 61 | "#\n", 62 | "def psfSlicePics(psf, sxy, sz, zvals, pixel_size = 0.05):\n", 63 | " ex = pixel_size * 0.5 * psf.shape[1]\n", 64 | "\n", 65 | " fig = pyplot.figure(figsize = (12,4))\n", 66 | " ax1 = fig.add_subplot(1,3,1)\n", 67 | " ax1.imshow(numpy.sqrt(psf[sz,:,:]),\n", 68 | " interpolation = 'none', \n", 69 | " extent = [-ex, ex, -ex, ex],\n", 70 | " cmap = \"gray\")\n", 71 | " ax1.set_title(\"PSF XY slice\")\n", 72 | " ax1.set_xlabel(r'x, $\\mu m$')\n", 73 | " ax1.set_ylabel(r'y, $\\mu m$')\n", 74 | "\n", 75 | " ax2 = fig.add_subplot(1,3,2)\n", 76 | " ax2.imshow(numpy.sqrt(psf[:,:,sxy]),\n", 77 | " interpolation = 'none',\n", 78 | " extent = [-ex, ex, zvals.max(), zvals.min()],\n", 79 | " cmap = \"gray\")\n", 80 | " ax2.set_title(\"PSF YZ slice\")\n", 81 | " ax2.set_xlabel(r'y, $\\mu m$')\n", 82 | " ax2.set_ylabel(r'z, $\\mu m$')\n", 83 | "\n", 84 | " ax3 = fig.add_subplot(1,3,3)\n", 85 | " ax3.imshow(numpy.sqrt(psf[:,sxy,:]), \n", 86 | " interpolation = 'none',\n", 87 | " extent = [-ex, ex, zvals.max(), zvals.min()],\n", 88 | " cmap = \"gray\")\n", 89 | " ax3.set_title(\"PSF XZ slice\")\n", 90 | " ax3.set_xlabel(r'x, $\\mu m$')\n", 91 | " ax3.set_ylabel(r'z, $\\mu m$')\n", 92 | "\n", 93 | " pyplot.show()" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "### Focus scan PSF\n", 101 | "\n", 102 | "The GL PSF for a fixed particle, scanning the microscope focus.\n", 103 | "\n", 104 | "Note that we're showing the sqrt of the PSF in all the PSF pictures." 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "# Radial PSF\n", 114 | "mp = msPSF.m_params\n", 115 | "pixel_size = 0.05\n", 116 | "rv = numpy.arange(0.0, 3.01, pixel_size)\n", 117 | "zv = numpy.arange(-1.5, 1.51, pixel_size)\n", 118 | "\n", 119 | "psf_zr = msPSF.gLZRFocalScan(mp, rv, zv, \n", 120 | " pz = 0.1, # Particle 0.1um above the surface.\n", 121 | " wvl = 0.7, # Detection wavelength.\n", 122 | " zd = mp[\"zd0\"]) # Detector exactly at the tube length of the microscope.\n" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "fig, ax = pyplot.subplots()\n", 132 | "\n", 133 | "ax.imshow(numpy.sqrt(psf_zr),\n", 134 | " extent=(rv.min(), rv.max(), zv.max(), zv.min()),\n", 135 | " cmap = 'gray')\n", 136 | "ax.set_xlabel(r'r, $\\mu m$')\n", 137 | "ax.set_ylabel(r'z, $\\mu m$')\n", 138 | "\n", 139 | "pyplot.show()" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": null, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "# XYZ PSF\n", 149 | "psf_xyz = msPSF.gLXYZFocalScan(mp, pixel_size, 31, zv, pz = 0.1)\n" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": null, 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "psfSlicePics(psf_xyz, 15, 30, zv)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "### Particle scan PSF\n", 166 | "\n", 167 | "The GL PSF for a particle scanned through a fixed focus.\n" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "# Radial PSF\n", 177 | "mp = msPSF.m_params\n", 178 | "pixel_size = 0.05\n", 179 | "rv = numpy.arange(0.0, 3.01, pixel_size)\n", 180 | "pv = numpy.arange(0.0, 3.01, pixel_size) # Particle distance above coverslip in microns.\n", 181 | "\n", 182 | "psf_zr = msPSF.gLZRParticleScan(mp, rv, pv, \n", 183 | " wvl = 0.7, # Detection wavelength.\n", 184 | " zd = mp[\"zd0\"], # Detector exactly at the tube length of the microscope.\n", 185 | " zv = -1.5) # Microscope focused 1.5um above the coverslip.\n" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "fig, ax = pyplot.subplots()\n", 195 | "\n", 196 | "ax.imshow(numpy.sqrt(psf_zr),\n", 197 | " extent=(rv.min(), rv.max(), pv.max(), pv.min()),\n", 198 | " cmap = 'gray')\n", 199 | "ax.set_xlabel(r'r, $\\mu m$')\n", 200 | "ax.set_ylabel(r'z, $\\mu m$')\n", 201 | "\n", 202 | "pyplot.show()" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": null, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "# XYZ PSF\n", 212 | "psf_xyz = msPSF.gLXYZParticleScan(mp, pixel_size, 31, pv, zv = -1.5)\n" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "psfSlicePics(psf_xyz, 12, 30, pv)" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [] 230 | } 231 | ], 232 | "metadata": { 233 | "kernelspec": { 234 | "display_name": "Python 3", 235 | "language": "python", 236 | "name": "python3" 237 | }, 238 | "language_info": { 239 | "codemirror_mode": { 240 | "name": "ipython", 241 | "version": 3 242 | }, 243 | "file_extension": ".py", 244 | "mimetype": "text/x-python", 245 | "name": "python", 246 | "nbconvert_exporter": "python", 247 | "pygments_lexer": "ipython3", 248 | "version": "3.6.7" 249 | } 250 | }, 251 | "nbformat": 4, 252 | "nbformat_minor": 2 253 | } 254 | -------------------------------------------------------------------------------- /microscPSF/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroscPSF/MicroscPSF-Py/df9b820150ac346a81ba9b8ccce811b17f35a520/microscPSF/__init__.py -------------------------------------------------------------------------------- /microscPSF/microscPSF.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generate a PSF using the Gibson and Lanni model. 4 | 5 | Note: All distance units are microns. 6 | 7 | This is slightly reworked version of the Python code provided by Kyle 8 | Douglass, "Implementing a fast Gibson-Lanni PSF solver in Python". 9 | 10 | http://kmdouglass.github.io/posts/implementing-a-fast-gibson-lanni-psf-solver-in-python.html 11 | 12 | 13 | References: 14 | 15 | 1. Li et al, "Fast and accurate three-dimensional point spread function computation 16 | for fluorescence microscopy", JOSA, 2017. 17 | 18 | 2. Gibson, S. & Lanni, F. "Experimental test of an analytical model of 19 | aberration in an oil-immersion objective lens used in three-dimensional 20 | light microscopy", J. Opt. Soc. Am. A 9, 154-166 (1992), [Originally 21 | published in J. Opt. Soc. Am. A 8, 1601-1613 (1991)]. 22 | 23 | 3. Kirshner et al, "3-D PSF fitting for fluorescence microscopy: implementation 24 | and localization application", Journal of Microscopy, 2012. 25 | 26 | Hazen 04/18 27 | """ 28 | import cmath 29 | import math 30 | import numpy 31 | import scipy 32 | import scipy.integrate 33 | import scipy.interpolate 34 | import scipy.special 35 | 36 | 37 | # Internal constants. 38 | num_basis = 100 # Number of rescaled Bessels that approximate the phase function. 39 | rho_samples = 1000 # Number of pupil sample along the radial direction. 40 | 41 | # Microscope parameters. 42 | m_params = {"M" : 100.0, # magnification 43 | "NA" : 1.4, # numerical aperture 44 | "ng0" : 1.515, # coverslip RI design value 45 | "ng" : 1.515, # coverslip RI experimental value 46 | "ni0" : 1.515, # immersion medium RI design value 47 | "ni" : 1.515, # immersion medium RI experimental value 48 | "ns" : 1.33, # specimen refractive index (RI) 49 | "ti0" : 150, # microns, working distance (immersion medium thickness) design value 50 | "tg" : 170, # microns, coverslip thickness experimental value 51 | "tg0" : 170, # microns, coverslip thickness design value 52 | "zd0" : 200.0 * 1.0e+3} # microscope tube length (in microns). 53 | 54 | 55 | def calcRv(dxy, xy_size, sampling=2): 56 | """ 57 | Calculate rv vector, this is 2x up-sampled. 58 | """ 59 | rv_max = math.sqrt(0.5 * xy_size * xy_size) + 1 60 | return numpy.arange(0, rv_max * dxy, dxy / sampling) 61 | 62 | 63 | def configure(mp, wvl): 64 | # Scaling factors for the Fourier-Bessel series expansion 65 | min_wavelength = 0.436 # microns 66 | scaling_factor = mp["NA"] * (3 * numpy.arange(1, num_basis + 1) - 2) * min_wavelength / wvl 67 | 68 | # Not sure this is completely correct for the case where the axial 69 | # location of the flourophore is 0.0. 70 | # 71 | max_rho = min([mp["NA"], mp["ng0"], mp["ng"], mp["ni0"], mp["ni"], mp["ns"]]) / mp["NA"] 72 | 73 | return [scaling_factor, max_rho] 74 | 75 | 76 | def deltaFocus(mp, zd): 77 | """ 78 | Return focal offset needed to compensate for the camera being at zd. 79 | 80 | mp - The microscope parameters dictionary. 81 | zd - Actual camera position in microns. 82 | """ 83 | a = mp["NA"] * mp["zd0"] / mp["M"] # Aperture radius at the back focal plane. 84 | return a*a*(mp["zd0"] - zd)/(2.0*mp["zd0"]*zd) 85 | 86 | 87 | def gLXYZCameraScan(mp, dxy, xy_size, zd, normalize = True, pz = 0.0, wvl = 0.6, zv = 0.0): 88 | """ 89 | NOTE: Does not work! 90 | 91 | Calculate 3D G-L PSF. This is models the PSF you would measure by scanning the 92 | camera position (changing the microscope tube length). 93 | 94 | This will return a numpy array with of size (zv.size, xy_size, xy_size). Note that z 95 | is the zeroth dimension of the PSF. 96 | 97 | mp - The microscope parameters dictionary. 98 | dxy - Step size in the XY plane. 99 | xy_size - Number of pixels in X/Y. 100 | zd - A numpy array containing the camera positions in microns. 101 | 102 | normalize - Normalize the PSF to unit height. 103 | pz - Particle z position above the coverslip (positive values only). 104 | wvl - Light wavelength in microns. 105 | zv - The (relative) z offset value of the coverslip (negative is closer to the objective). 106 | """ 107 | # Calculate rv vector, this is 2x up-sampled. 108 | rv = calcRv(dxy, xy_size) 109 | 110 | # Calculate radial/Z PSF. 111 | PSF_rz = gLZRCameraScan(mp, rv, zd, normalize = normalize, pz = pz, wvl = wvl, zv = zv) 112 | 113 | # Create XYZ PSF by interpolation. 114 | return psfRZToPSFXYZ(dxy, xy_size, rv, PSF_rz) 115 | 116 | 117 | def gLXYZFocalScan(mp, dxy, xy_size, zv, normalize = True, pz = 0.0, wvl = 0.6, zd = None): 118 | """ 119 | Calculate 3D G-L PSF. This is models the PSF you would measure by scanning the microscopes 120 | focus. 121 | 122 | This will return a numpy array with of size (zv.size, xy_size, xy_size). Note that z 123 | is the zeroth dimension of the PSF. 124 | 125 | mp - The microscope parameters dictionary. 126 | dxy - Step size in the XY plane. 127 | xy_size - Number of pixels in X/Y. 128 | zv - A numpy array containing the (relative) z offset values of the coverslip (negative is closer to the objective). 129 | 130 | normalize - Normalize the PSF to unit height. 131 | pz - Particle z position above the coverslip (positive values only). 132 | wvl - Light wavelength in microns. 133 | zd - Actual camera position in microns. If not specified the microscope tube length is used. 134 | """ 135 | # Calculate rv vector, this is 2x up-sampled. 136 | rv = calcRv(dxy, xy_size) 137 | 138 | # Calculate radial/Z PSF. 139 | PSF_rz = gLZRFocalScan(mp, rv, zv, normalize = normalize, pz = pz, wvl = wvl, zd = zd) 140 | 141 | # Create XYZ PSF by interpolation. 142 | return psfRZToPSFXYZ(dxy, xy_size, rv, PSF_rz) 143 | 144 | 145 | def gLXYZParticleScan(mp, dxy, xy_size, pz, normalize = True, wvl = 0.6, zd = None, zv = 0.0): 146 | """ 147 | Calculate 3D G-L PSF. This is models the PSF you would measure by scanning a particle 148 | through the microscopes focus. 149 | 150 | This will return a numpy array with of size (zv.size, xy_size, xy_size). Note that z 151 | is the zeroth dimension of the PSF. 152 | 153 | mp - The microscope parameters dictionary. 154 | dxy - Step size in the XY plane. 155 | xy_size - Number of pixels in X/Y. 156 | pz - A numpy array containing the particle z position above the coverslip (positive values only) 157 | in microns. 158 | 159 | normalize - Normalize the PSF to unit height. 160 | wvl - Light wavelength in microns. 161 | zd - Actual camera position in microns. If not specified the microscope tube length is used. 162 | zv - The (relative) z offset value of the coverslip (negative is closer to the objective). 163 | """ 164 | # Calculate rv vector, this is 2x up-sampled. 165 | rv = calcRv(dxy, xy_size) 166 | 167 | # Calculate radial/Z PSF. 168 | PSF_rz = gLZRParticleScan(mp, rv, pz, normalize = normalize, wvl = wvl, zd = zd, zv = zv) 169 | 170 | # Create XYZ PSF by interpolation. 171 | return psfRZToPSFXYZ(dxy, xy_size, rv, PSF_rz) 172 | 173 | 174 | def gLZRScan(mp, pz, rv, zd, zv, normalize = True, wvl = 0.6): 175 | """ 176 | Calculate radial G-L at specified radius. This function is primarily designed 177 | for internal use. Note that only one pz, zd and zv should be a numpy array 178 | with more than one element. You can simulate scanning the focus, the particle 179 | or the camera but not 2 or 3 of these values at the same time. 180 | 181 | mp - The microscope parameters dictionary. 182 | pz - A numpy array containing the particle z position above the coverslip (positive values only). 183 | rv - A numpy array containing the radius values. 184 | zd - A numpy array containing the actual camera position in microns. 185 | zv - A numpy array containing the relative z offset value of the coverslip (negative is closer to the objective). 186 | 187 | normalize - Normalize the PSF to unit height. 188 | wvl - Light wavelength in microns. 189 | """ 190 | [scaling_factor, max_rho] = configure(mp, wvl) 191 | rho = numpy.linspace(0.0, max_rho, rho_samples) 192 | 193 | a = mp["NA"] * mp["zd0"] / math.sqrt(mp["M"]*mp["M"] + mp["NA"]*mp["NA"]) # Aperture radius at the back focal plane. 194 | k = 2.0 * numpy.pi/wvl 195 | 196 | ti = zv.reshape(-1,1) + mp["ti0"] 197 | pz = pz.reshape(-1,1) 198 | zd = zd.reshape(-1,1) 199 | 200 | opdt = OPD(mp, rho, ti, pz, wvl, zd) 201 | 202 | # Sample the phase 203 | #phase = numpy.cos(opdt) + 1j * numpy.sin(opdt) 204 | phase = numpy.exp(1j * opdt) 205 | 206 | # Define the basis of Bessel functions 207 | # Shape is (number of basis functions by number of rho samples) 208 | J = scipy.special.jv(0, scaling_factor.reshape(-1, 1) * rho) 209 | 210 | # Compute the approximation to the sampled pupil phase by finding the least squares 211 | # solution to the complex coefficients of the Fourier-Bessel expansion. 212 | # Shape of C is (number of basis functions by number of z samples). 213 | # Note the matrix transposes to get the dimensions correct. 214 | C, residuals, _, _ = numpy.linalg.lstsq(J.T, phase.T) 215 | 216 | rv = rv*mp["M"] 217 | b = k * a * rv.reshape(-1, 1)/zd 218 | 219 | # Convenience functions for J0 and J1 Bessel functions 220 | J0 = lambda x: scipy.special.jv(0, x) 221 | J1 = lambda x: scipy.special.jv(1, x) 222 | 223 | # See equation 5 in Li, Xue, and Blu 224 | denom = scaling_factor * scaling_factor - b * b 225 | R = (scaling_factor * J1(scaling_factor * max_rho) * J0(b * max_rho) * max_rho - b * J0(scaling_factor * max_rho) * J1(b * max_rho) * max_rho) 226 | R /= denom 227 | 228 | # The transpose places the axial direction along the first dimension of the array, i.e. rows 229 | # This is only for convenience. 230 | PSF_rz = (numpy.abs(R.dot(C))**2).T 231 | 232 | # Normalize to the maximum value 233 | if normalize: 234 | PSF_rz /= numpy.max(PSF_rz) 235 | 236 | return PSF_rz 237 | 238 | 239 | def gLZRCameraScan(mp, rv, zd, normalize = True, pz = 0.0, wvl = 0.6, zv = 0.0): 240 | """ 241 | NOTE: Does not work! 242 | 243 | Calculate radial G-L at specified radius and z values. This is models the PSF 244 | you would measure by scanning the camera position (changing the microscope 245 | tube length). 246 | 247 | mp - The microscope parameters dictionary. 248 | rv - A numpy array containing the radius values. 249 | zd - A numpy array containing the camera positions in microns. 250 | 251 | normalize - Normalize the PSF to unit height. 252 | pz - Particle z position above the coverslip (positive values only). 253 | wvl - Light wavelength in microns. 254 | zv - The (relative) z offset value of the coverslip (negative is closer to the objective). 255 | """ 256 | pz = numpy.array([pz]) 257 | zv = numpy.array([zv]) 258 | 259 | return gLZRScan(mp, pz, rv, zd, zv, normalize = normalize, wvl = wvl) 260 | 261 | 262 | def gLZRFocalScan(mp, rv, zv, normalize = True, pz = 0.0, wvl = 0.6, zd = None): 263 | """ 264 | Calculate radial G-L at specified radius and z values. This is models the PSF 265 | you would measure by scanning the microscopes focus. 266 | 267 | mp - The microscope parameters dictionary. 268 | rv - A numpy array containing the radius values. 269 | zv - A numpy array containing the (relative) z offset values of the coverslip (negative is 270 | closer to the objective) in microns. 271 | 272 | normalize - Normalize the PSF to unit height. 273 | pz - Particle z position above the coverslip (positive values only). 274 | wvl - Light wavelength in microns. 275 | zd - Actual camera position in microns. If not specified the microscope tube length is used. 276 | """ 277 | if zd is None: 278 | zd = mp["zd0"] 279 | 280 | pz = numpy.array([pz]) 281 | zd = numpy.array([zd]) 282 | 283 | return gLZRScan(mp, pz, rv, zd, zv, normalize = normalize, wvl = wvl) 284 | 285 | 286 | def gLZRParticleScan(mp, rv, pz, normalize = True, wvl = 0.6, zd = None, zv = 0.0): 287 | """ 288 | Calculate radial G-L at specified radius and z values. This is models the PSF 289 | you would measure by scanning the particle relative to the microscopes focus. 290 | 291 | mp - The microscope parameters dictionary. 292 | rv - A numpy array containing the radius values. 293 | pz - A numpy array containing the particle z position above the coverslip (positive values only) 294 | in microns. 295 | 296 | normalize - Normalize the PSF to unit height. 297 | wvl - Light wavelength in microns. 298 | zd - Actual camera position in microns. If not specified the microscope tube length is used. 299 | zv - The (relative) z offset value of the coverslip (negative is closer to the objective). 300 | """ 301 | if zd is None: 302 | zd = mp["zd0"] 303 | 304 | zd = numpy.array([zd]) 305 | zv = numpy.array([zv]) 306 | 307 | return gLZRScan(mp, pz, rv, zd, zv, normalize = normalize, wvl = wvl) 308 | 309 | 310 | def OPD(mp, rho, ti, pz, wvl, zd): 311 | """ 312 | Calculate phase aberration term. 313 | 314 | mp - The microscope parameters dictionary. 315 | rho - Rho term. 316 | ti - Coverslip z offset in microns. 317 | pz - Particle z position above the coverslip in microns. 318 | wvl - Light wavelength in microns. 319 | zd - Actual camera position in microns. 320 | """ 321 | NA = mp["NA"] 322 | ns = mp["ns"] 323 | ng0 = mp["ng0"] 324 | ng = mp["ng"] 325 | ni0 = mp["ni0"] 326 | ni = mp["ni"] 327 | ti0 = mp["ti0"] 328 | tg = mp["tg"] 329 | tg0 = mp["tg0"] 330 | zd0 = mp["zd0"] 331 | 332 | a = NA * zd0 / mp["M"] # Aperture radius at the back focal plane. 333 | k = 2.0 * numpy.pi/wvl # Wave number of emitted light. 334 | 335 | OPDs = pz * numpy.sqrt(ns * ns - NA * NA * rho * rho) # OPD in the sample. 336 | OPDi = ti * numpy.sqrt(ni * ni - NA * NA * rho * rho) - ti0 * numpy.sqrt(ni0 * ni0 - NA * NA * rho * rho) # OPD in the immersion medium. 337 | OPDg = tg * numpy.sqrt(ng * ng - NA * NA * rho * rho) - tg0 * numpy.sqrt(ng0 * ng0 - NA * NA * rho * rho) # OPD in the coverslip. 338 | OPDt = a * a * (zd0 - zd) * rho * rho / (2.0 * zd0 * zd) # OPD in camera position. 339 | 340 | return k * (OPDs + OPDi + OPDg + OPDt) 341 | 342 | 343 | def psfRZToPSFXYZ(dxy, xy_size, rv, PSF_rz): 344 | """ 345 | Use interpolation to create a 3D XYZ PSF from a 2D ZR PSF. 346 | """ 347 | # Create XY grid of radius values. 348 | c_xy = float(xy_size) * 0.5 349 | xy = numpy.mgrid[0:xy_size, 0:xy_size] + 0.5 350 | r_pixel = dxy * numpy.sqrt((xy[1] - c_xy) * (xy[1] - c_xy) + (xy[0] - c_xy) * (xy[0] - c_xy)) 351 | 352 | # Create XYZ PSF by interpolation. 353 | PSF_xyz = numpy.zeros((PSF_rz.shape[0], xy_size, xy_size)) 354 | for i in range(PSF_rz.shape[0]): 355 | psf_rz_interp = scipy.interpolate.interp1d(rv, PSF_rz[i,:]) 356 | PSF_xyz[i,:,:] = psf_rz_interp(r_pixel.ravel()).reshape(xy_size, xy_size) 357 | 358 | return PSF_xyz 359 | 360 | 361 | def slowGL(mp, max_rho, rv, zv, pz, wvl, zd): 362 | """ 363 | Calculate a single point in the G-L PSF using integration. This 364 | is primarily provided for testing / reference purposes. As the 365 | function name implies, this is going to be slow. 366 | 367 | mp - The microscope parameters dictionary. 368 | max_rho - The maximum rho value. 369 | rv - A radius value in microns. 370 | zv - A z offset value (of the coverslip) in microns. 371 | pz - Particle z position above the coverslip in microns. 372 | wvl - Light wavelength in microns. 373 | zd - Actual camera position in microns. 374 | """ 375 | a = mp["NA"] * mp["zd0"] / math.sqrt(mp["M"]*mp["M"] + mp["NA"]*mp["NA"]) # Aperture radius at the back focal plane. 376 | k = 2.0 * numpy.pi/wvl 377 | ti = zv + mp["ti0"] 378 | 379 | rv = rv*mp["M"] 380 | 381 | def integral_fn_imag(rho): 382 | t1 = k * a * rho * rv/zd 383 | t2 = scipy.special.jv(0, t1) 384 | t3 = t2*cmath.exp(1j*OPD(mp, rho, ti, pz, wvl, zd))*rho 385 | return t3.imag 386 | 387 | def integral_fn_real(rho): 388 | t1 = k * a * rho * rv/zd 389 | t2 = scipy.special.jv(0, t1) 390 | t3 = t2*cmath.exp(1j*OPD(mp, rho, ti, pz, wvl, zd))*rho 391 | return t3.real 392 | 393 | int_i = scipy.integrate.quad(lambda x: integral_fn_imag(x), 0.0, max_rho)[0] 394 | int_r = scipy.integrate.quad(lambda x: integral_fn_real(x), 0.0, max_rho)[0] 395 | 396 | t1 = k * a * a / (zd * zd) 397 | return t1 * (int_r * int_r + int_i * int_i) 398 | 399 | 400 | def gLZRFocalScanSlow(mp, rv, zv, normalize = True, pz = 0.0, wvl = 0.6, zd = None): 401 | """ 402 | This is the integration version of gLZRFocalScan. 403 | 404 | mp - The microscope parameters dictionary. 405 | rv - A numpy array containing the radius values. 406 | zv - A numpy array containing the (relative) z offset values of the coverslip (negative is closer to the objective). 407 | 408 | normalize - Normalize the PSF to unit height. 409 | pz - Particle z position above the coverslip (positive values only). 410 | wvl - Light wavelength in microns. 411 | zd - Actual camera position in microns. If not specified the microscope tube length is used. 412 | """ 413 | if zd is None: 414 | zd = mp["zd0"] 415 | 416 | [scaling_factor, max_rho] = configure(mp, wvl) 417 | rho = numpy.linspace(0.0, max_rho, rho_samples) 418 | 419 | psf_rz = numpy.zeros((zv.size, rv.size)) 420 | for i in range(zv.size): 421 | for j in range(rv.size): 422 | psf_rz[i,j] = slowGL(mp, max_rho, rv[j], zv[i], pz, wvl, zd) 423 | 424 | if normalize: 425 | psf_rz = psf_rz/numpy.max(psf_rz) 426 | 427 | return psf_rz 428 | 429 | 430 | def gLZRParticleScanSlow(mp, rv, pz, normalize = True, wvl = 0.6, zd = None, zv = 0.0): 431 | """ 432 | This is the integration version of gLZRParticleScan. 433 | 434 | mp - The microscope parameters dictionary. 435 | rv - A numpy array containing the radius values. 436 | pz - A numpy array containing the particle z position above the coverslip (positive values only) 437 | in microns. 438 | 439 | normalize - Normalize the PSF to unit height. 440 | wvl - Light wavelength in microns. 441 | zd - Actual camera position in microns. If not specified the microscope tube length is used. 442 | zv - The (relative) z offset value of the coverslip (negative is closer to the objective). 443 | """ 444 | if zd is None: 445 | zd = mp["zd0"] 446 | 447 | [scaling_factor, max_rho] = configure(mp, wvl) 448 | rho = numpy.linspace(0.0, max_rho, rho_samples) 449 | 450 | psf_rz = numpy.zeros((pz.size, rv.size)) 451 | for i in range(pz.size): 452 | for j in range(rv.size): 453 | psf_rz[i,j] = slowGL(mp, max_rho, rv[j], zv, pz[i], wvl, zd) 454 | 455 | if normalize: 456 | psf_rz = psf_rz/numpy.max(psf_rz) 457 | 458 | return psf_rz 459 | -------------------------------------------------------------------------------- /microscPSF/test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Test focal scan G-L PSF. 4 | """ 5 | import numpy 6 | 7 | import microscPSF 8 | import microscPSF.microscPSF as msPSF 9 | 10 | def test_01(): 11 | """ 12 | Particle on surface. 13 | """ 14 | mp = msPSF.m_params 15 | rv = numpy.arange(0.0, 1.01, 0.1) 16 | zv = numpy.arange(-1.0, 1.01, 0.2) 17 | 18 | fast_rz = msPSF.gLZRFocalScan(mp, rv, zv) 19 | slow_rz = msPSF.gLZRFocalScanSlow(mp, rv, zv) 20 | 21 | assert (numpy.allclose(fast_rz, slow_rz)) 22 | 23 | 24 | def test_02(): 25 | """ 26 | Particle above surface. 27 | """ 28 | mp = msPSF.m_params 29 | rv = numpy.arange(0.0, 1.01, 0.1) 30 | zv = numpy.arange(-1.0, 1.01, 0.2) 31 | 32 | fast_rz = msPSF.gLZRFocalScan(mp, rv, zv, pz = 0.5) 33 | slow_rz = msPSF.gLZRFocalScanSlow(mp, rv, zv, pz = 0.5) 34 | 35 | assert (numpy.allclose(fast_rz, slow_rz, atol = 1.0e-4, rtol = 1.0e-4)) 36 | 37 | 38 | def test_03(): 39 | """ 40 | Detector offset. 41 | """ 42 | mp = msPSF.m_params 43 | rv = numpy.arange(0.0, 1.01, 0.1) 44 | zv = numpy.arange(-1.0, 1.01, 0.2) 45 | 46 | zd = mp["zd0"] + 1000 47 | fast_rz = msPSF.gLZRFocalScan(mp, rv, zv, zd = zd) 48 | slow_rz = msPSF.gLZRFocalScanSlow(mp, rv, zv, zd = zd) 49 | 50 | assert (numpy.allclose(fast_rz, slow_rz)) 51 | 52 | 53 | def test_04(): 54 | """ 55 | Particle scan. 56 | """ 57 | mp = msPSF.m_params 58 | rv = numpy.arange(0.0, 1.01, 0.1) 59 | pv = numpy.arange(0.0, 2.01, 0.1) 60 | 61 | fast_rz = msPSF.gLZRParticleScan(mp, rv, pv) 62 | slow_rz = msPSF.gLZRParticleScanSlow(mp, rv, pv) 63 | 64 | assert (numpy.allclose(fast_rz, slow_rz, rtol = 1.0e-4, atol = 1.0e-4)) 65 | 66 | 67 | def test_05(): 68 | """ 69 | Particle scan, focus offset. 70 | """ 71 | mp = msPSF.m_params 72 | rv = numpy.arange(0.0, 1.01, 0.1) 73 | pv = numpy.arange(1.0, 3.01, 0.2) 74 | 75 | fast_rz = msPSF.gLZRParticleScan(mp, rv, pv, zv = -2.0) 76 | slow_rz = msPSF.gLZRParticleScanSlow(mp, rv, pv, zv = -2.0) 77 | 78 | assert (numpy.allclose(fast_rz, slow_rz, rtol = 1.0e-3, atol = 1.0e-3)) 79 | 80 | 81 | if (__name__ == "__main__"): 82 | test_01() 83 | test_02() 84 | test_03() 85 | test_04() 86 | test_05() 87 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | [metadata] 8 | license_file = LICENSE.txt 9 | 10 | [tool:pytest] 11 | python_files = test*.py 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | from setuptools import setup, find_packages 5 | 6 | # Get the long description from the relevant file 7 | with open('README.md') as f: 8 | readme = f.read() 9 | 10 | setup( 11 | name='MicroscPSF-Py', 12 | version=0.2, 13 | description='Gibson-Lanni PSF calculation code.', 14 | long_description=readme, 15 | long_description_content_type='text/markdown', 16 | author='Kyle Douglass, Hazen Babcock', 17 | author_email='hbabcock@mac.com', 18 | url='https://github.com/MicroscPSF/MicroscPSF-Py', 19 | 20 | zip_safe=False, 21 | packages=find_packages(), 22 | 23 | package_data={}, 24 | exclude_package_data={}, 25 | include_package_data=True, 26 | 27 | requires=[], 28 | 29 | setup_requires=['pytest-runner'], 30 | tests_require=['pytest'], 31 | 32 | license='MIT', 33 | keywords='PSF,microscopy', 34 | classifiers=[ 35 | 'Development Status :: 3 - Alpha', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: MIT License', 38 | "Programming Language :: C", 39 | "Programming Language :: Python :: 2", 40 | 'Programming Language :: Python :: 2.7', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.5', 43 | 'Programming Language :: Python :: 3.6', 44 | ], 45 | ) 46 | --------------------------------------------------------------------------------