Skip to content

Commit

Permalink
Added ability to save results and bin data.
Browse files Browse the repository at this point in the history
  • Loading branch information
jacione committed Jun 14, 2023
1 parent 3a705d8 commit affa265
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 85 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[//]: # (TODO: add some pictures to readme)
# Interactive CDI
An interactive applet that demonstrates principles of coherent diffraction imaging (CDI), specifically phase retrieval, in a hands-on environment.

Expand All @@ -21,7 +22,9 @@ While not designed for high performance, this approach has great educational val
* An experienced researcher might still gain new insights by watching phase retrieval occur in realtime.

## Installation
First, make sure that you have installed Python v3.9 or later. Then run the following (in a virtual environment, if desired):
If you are running on Windows, the simplest way to run Interactive CDI is to download and extract the official release, and run the `cdi_live.exe` executable within. As of right now, this software is only compiled for Windows.

If you're not on Windows, or if you just want to run Interactive CDI from its Python source code, download the appropriate files from GitHub. Make sure that you have installed Python v3.9 or later. Then run the following (in a virtual environment, if desired):

pip install numpy scipy scikit-image matplotlib Pillow

Expand All @@ -45,12 +48,14 @@ Depending on the state of the reconstruction and which tab is active, the app wi
### The "Data" tab
This is where you load and pre-process your diffraction pattern. It has the following features:

**Load data**: Opens a dialog to select one or more diffraction image files. If multiple images are selected, they will be summed pixel-wise. Some basic pre-processing is also applied at this stage: (1) color images are converted to grayscale, (2) the brightest point in the image is shifted to the center, (3) if the image is not a square, the long dimension is cropped to match the short dimension, (4) the image is resampled to 400x400 pixels (to keep computational times reasonable), and (5) the square root of the image is taken, which converts intensity to amplitude.
**Load data**: Opens a dialog to select one or more diffraction image files. If multiple images are selected, they will be summed pixel-wise. Some basic pre-processing is also applied at this stage: (1) color images are converted to grayscale, (2) the brightest point in the image is shifted to the center, (3) if the image is not a square, the long dimension is cropped to match the short dimension, (4) images larger than 1024x1024 pixels are downsampled to that size (to keep computational times reasonable), and (5) the square root of the image is taken, which converts intensity to amplitude.

**Load background**: Opens a dialog to select one or more background image files. These are (ideally) images measured under the exact same conditions as the diffraction data, but without the coherent light source. Such a measurement allows you to characterize the noise in your experiment, e.g. electrical noise in the camera or stray light in the room. The same pre-processing is applied to these images as to the diffraction images, with two exceptions. First, the shifting step re-uses the shift from the diffraction images (that is, it doesn't check for the brightest noise). Second, the background intensity is scaled to match the exposure of the diffraction; for example, if there are 10 summed diffraction images and only 5 background images, the background intensity will be scaled by a factor of 2. _Note that this requires the actual exposure levels to remain constant throughout the experiment._

**Subtract background**: Toggles whether the background is subtracted from the diffraction data. If no background is loaded, this does nothing. Background subtraction is probably the most important pre-processing feature in CDI, and should always be used when possible.

**Bin pixels**: Applies pixel binning (downsampling by summing adjacent pixels) to the image. For sufficiently oversampled data (many pixels per smallest diffraction fringe) this can drastically reduce the computational time of each iteration. However, if the fringes lose fidelity, the reconstruction will fail.

**Gaussian blur**: Applies gaussian blurring to the image. The slider below it adjusts the amount of blurring (gaussian sigma in pixels). This can be useful for smoothing out grainy data, though it is usually better to simply sum over more images.

**Threshold**: Applies thresholding to the image. The slider adjusts the threshold value; for a value of 0.25, the dimmest 25% of all pixels will be set to zero. This can sometimes be an effective way of removing noise in a dataset when long exposures and/or background subtraction are not options.
Expand Down Expand Up @@ -88,7 +93,7 @@ Iterative phase retrieval is, well, iterative. As such, it can get tedious to do

**Stop**: Completes the current iteration, then stops iterating.

**Stop w/ ER**: Completes the current iteration, performs a single iteration with the ER constraint replacing HIO, then stops iterating.
**Stop w/ ER**: Completes the current iteration, performs a single iteration with the ER constraint replacing HIO, then stops iterating. If pressed when the reconstruction is not iterating,

**Re-center**: Shifts the direct-space object so that the center-of-mass of the support region is centered on the image. If currently iterating, this will happen between iterations.

Expand All @@ -98,7 +103,7 @@ Iterative phase retrieval is, well, iterative. As such, it can get tedious to do

**Reset**: Resets the reconstruction to its initial state, with random phases in reciprocal space and a centered square support region half the size of the image. Also resets the SW and HIO parameters. If currently iterating, this will happen between iterations.

**Parameters**: These three sliders control the gaussian sigma and relative threshold for shrinkwrap as well as the beta coefficient for HIO. They are synced with those on the manual tab.
**Save results**: Opens a dialog to select a directory in which to save the current state of the reconstruction. You are strongly encouraged to create a new folder, as it will overwrite existing output files. Once a directory is selected, it will save seven files: an amplitude, phase, and composite image for direct space, the same for reciprocal space, and a raw numpy array file containing the actual complex values in direct space.

[^1]: Ware, M. and Peatross, J. (2015) ‘Fraunhofer Approximation’, sec 10.4 in _Physics of Light and Optics_, pp. 264–265. Available at: https://optics.byu.edu/textbook.

Expand Down
62 changes: 45 additions & 17 deletions src/cdi_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

import time
import tkinter as tk
from tkinter.messagebox import showerror
from tkinter.filedialog import asksaveasfilename
from tkinter.messagebox import showinfo
from tkinter.filedialog import askdirectory
import tkinter.ttk as ttk

import numpy as np
Expand All @@ -26,10 +26,6 @@
import src.diffraction as diffraction
import src.utils as ut

# TODO if actual data has been loaded into the app, disable all simulation functions to prevent overwriting
# TODO move the save reconstruction button to the reconstruction tabs


class App:
def __init__(self):
self.data = diffraction.LoadData()
Expand Down Expand Up @@ -65,6 +61,16 @@ def __init__(self):
ttk.Checkbutton(data_tab, text="Subtract background", variable=self.pre_bkgd).grid(row=r, column=0,
columnspan=3, **btn_kwargs)
r += 1
self.pre_binning = tk.BooleanVar(value=False)
self.pre_binfact = tk.IntVar(value=1)
ttk.Checkbutton(data_tab, text="Bin pixels", variable=self.pre_binning).grid(row=r, column=0, **btn_kwargs)
FormatLabel(data_tab, textvariable=self.pre_binfact, format="{:.0f}").grid(row=r, column=1, **btn_kwargs)
r += 1
ttk.Scale(data_tab, from_=1, to=10, orient="horizontal", variable=self.pre_binfact).grid(row=r, column=0,
columnspan=2,
**btn_kwargs)

r += 1
self.pre_gauss = tk.BooleanVar(value=False)
self.pre_sigma = tk.DoubleVar(value=0.0)
ttk.Checkbutton(data_tab, text="Gaussian blur", variable=self.pre_gauss).grid(row=r, column=0, **btn_kwargs)
Expand All @@ -78,7 +84,7 @@ def __init__(self):
self.pre_thresholding = tk.BooleanVar(value=False)
self.pre_thresh = tk.DoubleVar(value=0.0)
ttk.Checkbutton(data_tab, text="Threshold", variable=self.pre_thresholding).grid(row=r, column=0,
**btn_kwargs)
**btn_kwargs)
FormatLabel(data_tab, textvariable=self.pre_thresh, format="{:.2f}").grid(row=r, column=1, **btn_kwargs)
r += 1
ttk.Scale(data_tab, from_=0.0, to=1.0, orient="horizontal", variable=self.pre_thresh).grid(row=r, column=0,
Expand All @@ -95,8 +101,8 @@ def __init__(self):
columnspan=2,
**btn_kwargs)

for var in [self.pre_bkgd, self.pre_gauss, self.pre_sigma, self.pre_thresholding, self.pre_thresh,
self.pre_vignette, self.pre_vsigma]:
for var in [self.pre_bkgd, self.pre_binning, self.pre_binfact, self.pre_gauss, self.pre_sigma,
self.pre_thresholding, self.pre_thresh, self.pre_vignette, self.pre_vsigma]:
var.trace("w", self.preprocess)

# Manual controls #############################################################################################
Expand Down Expand Up @@ -153,6 +159,10 @@ def __init__(self):
btn.grid(row=r, column=0, columnspan=3, **btn_kwargs)
r += 1

self.save_msg = True
ttk.Button(live_tab, text="Save results", command=self.save_result).grid(row=r, column=0, columnspan=3,
**btn_kwargs)

# Parameter controls ##########################################################################################
self.sw_sigma = tk.DoubleVar(value=2.0)
self.sw_thresh = tk.DoubleVar(value=0.2)
Expand Down Expand Up @@ -188,8 +198,9 @@ def __init__(self):

# Finally, make the object itself. Start with random shapes.
impad = 2
self.img_left = ut.amp_to_photo_image(np.abs(self.solver.ds_image))
self.img_right = ut.phase_to_photo_image(self.solver.ds_image)
self.im_size = 450
self.img_left = ut.amp_to_photo_image(np.abs(self.solver.ds_image), size=self.im_size)
self.img_right = ut.phase_to_photo_image(self.solver.ds_image, size=self.im_size)

self.label_left = ttk.Label(self.root, text="Amplitude", font=("Arial", 20), justify=tk.CENTER)
self.label_left.grid(row=0, column=0)
Expand All @@ -208,7 +219,7 @@ def __init__(self):

def man_sw(self):
self.solver.shrinkwrap(self.sw_sigma.get(), self.sw_thresh.get())
self.img_left = ut.amp_to_photo_image(np.uint8(self.solver.support.array))
self.img_left = ut.amp_to_photo_image(np.uint8(self.solver.support.array), size=self.im_size)
self.disp_left.configure(image=self.img_left)
self.disp_left.image = self.img_left

Expand Down Expand Up @@ -262,15 +273,16 @@ def update_images(self, *_):
if not i == 2:
self.stop()
if self.fourier:
self.img_left = ut.amp_to_photo_image(np.sqrt(np.abs(self.solver.fs_image)))
self.img_right = ut.phase_to_photo_image(self.solver.fs_image)
self.img_left = ut.amp_to_photo_image(np.sqrt(np.abs(self.solver.fs_image)), size=self.im_size)
self.img_right = ut.phase_to_photo_image(self.solver.fs_image, size=self.im_size)
for button in self.ds_buttons:
button.state(["disabled"])
for button in self.fs_buttons:
button.state(["!disabled"])
else:
self.img_left = ut.amp_to_photo_image(np.abs(self.solver.ds_image), mask=self.solver.support.array)
self.img_right = ut.phase_to_photo_image(self.solver.ds_image)
self.img_left = ut.amp_to_photo_image(np.abs(self.solver.ds_image), mask=self.solver.support.array,
size=self.im_size)
self.img_right = ut.phase_to_photo_image(self.solver.ds_image, size=self.im_size)
for button in self.ds_buttons:
button.state(["!disabled"])
for button in self.fs_buttons:
Expand Down Expand Up @@ -304,6 +316,8 @@ def load_bkgd(self):

def preprocess(self, *_):
self.solver = phasing.Solver(self.data.preprocess(self.pre_bkgd.get(),
self.pre_binning.get(),
self.pre_binfact.get(),
self.pre_gauss.get(),
self.pre_sigma.get(),
self.pre_thresholding.get(),
Expand All @@ -315,7 +329,21 @@ def preprocess(self, *_):
self.update_images()

def save_result(self):
imsave(asksaveasfilename(defaultextension="png"), np.abs(self.solver.ds_image))
if self.save_msg:
# Show this message the first time only.
showinfo("Info", "Because there are multiple files to save, you will be asked to input a folder rather "
"than simply a file name. It is highly recommended that you create a new folder for "
"these files. If you select a folder that already contains output from this app, "
"the old files WILL be overwritten!")
self.save_msg = False
save_dir = askdirectory()
np.save(f"{save_dir}/ds_raw.npy", self.solver.ds_image)
imsave(f"{save_dir}/ds_amplitude.png", np.abs(self.solver.ds_image), cmap="gray")
imsave(f"{save_dir}/ds_phase.png", np.angle(self.solver.ds_image), cmap="hsv")
imsave(f"{save_dir}/ds_combined.png", ut.complex_composite_image(self.solver.ds_image, dark_background=True))
imsave(f"{save_dir}/fs_amplitude.png", np.abs(self.solver.fs_image), cmap="gray")
imsave(f"{save_dir}/fs_phase.png", np.angle(self.solver.fs_image), cmap="hsv")
imsave(f"{save_dir}/fs_combined.png", ut.complex_composite_image(self.solver.fs_image, dark_background=True))

def restart(self):
self.hio_beta.set(0.9)
Expand Down
Loading

0 comments on commit affa265

Please sign in to comment.