In [1]:

```
%matplotlib notebook
import matplotlib.pyplot as plt
import tifffile
import numpy as np
import os
```

This notebook shows an example of how to calibrate an EMCCD camera to covert the measured AD converted numbers into number of photons measured. The method is approximate and does not take into account factors such as the quantum efficiency of the camera and readout noise. In summary, the procedure consists of two steps, 1) Determine gain factors to convert signals measured in EM mode to signals measured in normal CCD more 2) Determine conversion factor of measured signal to photons for normal CCD more.

The data can be found online here: https://doi.org/10.5281/zenodo.3765412

In [2]:

```
data_dir = r'data'
```

In [24]:

```
# Function to read all files in a folder and combine them to a stack
def folder_read(path):
return np.stack([tifffile.imread(os.path.join(path, f)) for f in os.listdir(path) if '.tif' in f]).squeeze()
```

For the first part, we need to relate EMCCD signals to CCD signals. For this we need to illuminate the camera with a constant light source and then tweak the light intensity at the desired EM gain level at low exposure times such that the measured signals are at typical levels for normal measurements. For 16 bit images this would be around 20-40k. Then, while keeping the light intensity fixed, switch to normal CCD mode and adjust exporure time so that the signal is at the level measured before.

The example datasets has measurements at 13 different EM gain levels but only measurments at EM gain levels which you acutally use are required. The same measurement should be done without any light exposure to obtain the dark counts which should be subtracted.

To generate lists of the measurement folders:

In [4]:

```
gain_list = sorted(os.listdir(os.path.join(data_dir, 'gain')), key=lambda x: x.split('_')[1])
dark_list = sorted(os.listdir(os.path.join(data_dir, 'gain_dark')), key=lambda x: x.split('_')[1])
for pair in zip(gain_list, dark_list):
print(pair)
```

The last set of measurements here is the normal CCD which is used to convert to.

To load this dataset:

In [25]:

```
gain_normal = folder_read(os.path.join(data_dir, 'gain', gain_list[-1]))
dark_normal = folder_read(os.path.join(data_dir, 'gain_dark', dark_list[-1]))
normal = gain_normal.mean(axis=0) - dark_normal.mean(axis=0)
normal.mean(), normal.shape
```

Out[25]:

Then we iterate over the other conditions and calculate the gain factor by using the fact that signal is linear with exposure:

$$\frac{Signal_{EM} Exposure_{CCD}}{Signal_{CCD} Exposure_{EM}}$$

In [26]:

```
for gain_p, dark_p in zip(gain_list, dark_list):
gain = folder_read(os.path.join(data_dir, 'gain', gain_p))
dark = folder_read(os.path.join(data_dir, 'gain_dark', dark_p))
current = gain.mean(axis=0) - dark.mean(axis=0)
expt = int(gain_p.split('_')[2][:-2])
gain = (current / normal).mean() * (4600 / expt)
print(gain_p, gain)
```

These numbers can now be used to calculate signals in CCD mode from signals in EM mode. For example, if a signal of 30k was measured with 1200 EM gain, this would have given a signal of 30000/112.89 = 265. Thus we can use these factors to convert to CCD signals.

Next, we want to know how many photons were detected. For this we use the fact that for a Poisson distribution the mean of the population is equal to its mean. We assume that each photon creates an electron in the CCD which means that these electrons, which eventually give the AD signal, are also poissonian. Therefore there should be a linear relation between the variance and mean of the signal measured, and the slope tells us the conversion factor from signal to photoelectrons.

To do so we need to record short movies at different light exposure levels, subtract dark counts and calculate mean and variance for these measurements.

The available folders are:

In [13]:

```
folders = [f for f in os.listdir(os.path.join(data_dir, 'std_vs_r')) if not 'darkfield' in f]
folders
```

Out[13]:

Loading the dark counts:

In [28]:

```
df = tifffile.imread(os.path.join(data_dir, 'std_vs_r', '10_darkfield_1', '10_darkfield_1_MMStack_Pos0.ome.tif'))
```

In [31]:

```
df_mean = df.mean(axis=0)
df_mean.mean()
```

Out[31]:

We then iterate over all folders and calculate for each stack of 200 frames the variance and mean values for every pixel. These values are then appended to two lists which we'll combine later.

In [32]:

```
var_list = []
mean_list = []
for f in folders:
print(f)
arr = folder_read(os.path.join(data_dir, 'std_vs_r', f)).astype(float)
arr -= df_mean
print(arr.shape)
stds = np.var(arr, axis=0)
means = np.mean(arr, axis=0)
b1 = np.abs(stds - stds.mean()) <3*np.std(stds) # Remove outliers with love
var_list.append(stds[b1])
mean_list.append(means[b1])
```

In [33]:

```
all_var = np.concatenate(var_list).flatten()
all_mean = np.concatenate(mean_list).flatten()
```

To obtain the slope we'll use numpy's polyfit to fit the data to a straight line:

In [20]:

```
a, b = np.polyfit(all_mean, all_var, 1)
```

In [34]:

```
x_plot = np.linspace(0, 1.05*np.max(all_mean), num=200)
plt.figure()
plt.plot(all_mean, all_var, '.')
plt.plot(x_plot, a*x_plot + b)
plt.xlabel('Mean')
plt.ylabel('Variance')
```

Out[34]:

The final value for the slope is:

In [35]:

```
a
```

Out[35]:

This number means that for a normal CCD (which is what we used to get the graph above) we get 0.5 counts per electron. So when you measure for example 20000 counts this resulted from 40000 photons.

To get the number of photons for the desired EM level you should multiply this number with the gain factor above.

For gain 1200 this number is 112.897. This number will decrease over time due to EM gain ageing as a result of high light exposure. It possible to correct this by adjusting the voltage in the EM gain register. Consult your camera manufacturer on how to do so.

The factor to convert from AD counts to photons is then for gain 1200:

In [91]:

```
0.5002699721794224*112.897
```

Out[91]:

Divide your measured AD counts by this number to get photons.