Image sharpness and finding the focus point#
This notebook illustrates how to find the focus point of an imaging detector by computing the image sharpness as a function of camera position.
[1]:
import scipp as sc
from ess import imaging as img
from ess.imaging import data
import scippnexus as sx
import numpy as np
Load the data#
We load the image data, and attach the camera stage position to the data array.
[2]:
dg = sx.load(data.get_tbl_orca_focussing_path())
da = dg["entry"]["instrument"]["orca_detector"]["data"]
da.coords["position"] = dg["entry"]["instrument"]["camera_stage"]["position_setpoint"][
"value"
].data
da = da.rename_dims(time="position")
da
Downloading file 'tbl-orca-focussing.hdf.zip' from 'https://public.esss.dk/groups/scipp/ess/imaging/1/tbl-orca-focussing.hdf.zip' to '/home/runner/.cache/essimaging/1'.
Unzipping contents of '/home/runner/.cache/essimaging/1/tbl-orca-focussing.hdf.zip' to '/home/runner/.cache/essimaging/1/tbl-orca-focussing.hdf.zip.unzip'
[2]:
- position: 61
- dim_1: 512
- dim_2: 512
- position(position)float64mm140.0, 141.0, ..., 199.0, 200.0
Values:
array([140., 141., 142., 143., 144., 145., 146., 147., 148., 149., 150., 151., 152., 153., 154., 155., 156., 157., 158., 159., 160., 161., 162., 163., 164., 165., 166., 167., 168., 169., 170., 171., 172., 173., 174., 175., 176., 177., 178., 179., 180., 181., 182., 183., 184., 185., 186., 187., 188., 189., 190., 191., 192., 193., 194., 195., 196., 197., 198., 199., 200.]) - time(position)datetime64ns2025-06-11T10:07:14.603685891, 2025-06-11T10:07:19.443681068, ..., 2025-06-11T10:12:15.939916263, 2025-06-11T10:12:20.779694055
Values:
array(['2025-06-11T10:07:14.603685891', '2025-06-11T10:07:19.443681068', '2025-06-11T10:07:24.291694836', '2025-06-11T10:07:29.135681029', '2025-06-11T10:07:36.387704881', '2025-06-11T10:07:41.239693346', '2025-06-11T10:07:46.111723518', '2025-06-11T10:07:50.687718363', '2025-06-11T10:07:55.555682998', '2025-06-11T10:08:00.435680492', '2025-06-11T10:08:05.283693959', '2025-06-11T10:08:10.023683844', '2025-06-11T10:08:14.879729086', '2025-06-11T10:08:19.735700903', '2025-06-11T10:08:24.587683636', '2025-06-11T10:08:29.439675066', '2025-06-11T10:08:33.991718005', '2025-06-11T10:08:38.851754030', '2025-06-11T10:08:43.699704502', '2025-06-11T10:08:48.555669860', '2025-06-11T10:08:53.403690390', '2025-06-11T10:08:58.247718146', '2025-06-11T10:09:03.099704735', '2025-06-11T10:09:07.951692452', '2025-06-11T10:09:12.787697950', '2025-06-11T10:09:20.067721706', '2025-06-11T10:09:24.919730518', '2025-06-11T10:09:29.763706276', '2025-06-11T10:09:34.619683746', '2025-06-11T10:09:39.467698542', '2025-06-11T10:09:46.723715712', '2025-06-11T10:09:51.387727129', '2025-06-11T10:09:56.155690987', '2025-06-11T10:10:00.983695240', '2025-06-11T10:10:05.835713180', '2025-06-11T10:10:10.679685428', '2025-06-11T10:10:17.947739370', '2025-06-11T10:10:22.803701747', '2025-06-11T10:10:27.671682040', '2025-06-11T10:10:34.923718875', '2025-06-11T10:10:39.475679906', '2025-06-11T10:10:44.327714300', '2025-06-11T10:10:49.179701241', '2025-06-11T10:10:54.047723775', '2025-06-11T10:10:58.907683420', '2025-06-11T10:11:03.775723131', '2025-06-11T10:11:08.639689393', '2025-06-11T10:11:13.515701420', '2025-06-11T10:11:18.375689596', '2025-06-11T10:11:23.247713489', '2025-06-11T10:11:28.123680303', '2025-06-11T10:11:32.959681624', '2025-06-11T10:11:37.719680831', '2025-06-11T10:11:42.255678954', '2025-06-11T10:11:49.539686860', '2025-06-11T10:11:54.379713851', '2025-06-11T10:11:59.231692792', '2025-06-11T10:12:06.499695867', '2025-06-11T10:12:11.347714105', '2025-06-11T10:12:15.939916263', '2025-06-11T10:12:20.779694055'], dtype='datetime64[ns]')
- (position, dim_1, dim_2)int32𝟙30213, 31248, ..., 7737, 7818
Values:
array([[[30213, 31248, 31972, ..., 21162, 20654, 19905], [30118, 31154, 31709, ..., 21594, 20768, 20430], [30200, 31051, 31501, ..., 20960, 20528, 20365], ..., [ 9970, 10262, 10097, ..., 7853, 7926, 8061], [ 9821, 9835, 10324, ..., 7721, 7617, 7816], [ 9373, 9988, 9827, ..., 7676, 7560, 7771]], [[30301, 31097, 31860, ..., 21286, 20226, 20102], [30427, 30812, 31838, ..., 21106, 20534, 20082], [29882, 31460, 31381, ..., 21053, 20535, 19926], ..., [ 9895, 10018, 9911, ..., 7965, 7917, 8088], [ 9624, 9767, 10171, ..., 7541, 7406, 7804], [ 9838, 9857, 10023, ..., 7556, 7388, 7705]], [[30044, 30657, 32174, ..., 20952, 20613, 20032], [29852, 31119, 31492, ..., 20668, 20736, 19924], [29956, 31202, 31819, ..., 21287, 20653, 20176], ..., [ 9786, 10056, 9942, ..., 7826, 7724, 8095], [ 9652, 9738, 10020, ..., 7573, 7685, 7855], [ 9637, 9689, 9995, ..., 7800, 7656, 7662]], ..., [[16267, 17469, 19609, ..., 15784, 15275, 14631], [16093, 17714, 19456, ..., 16212, 15494, 14393], [15566, 17114, 19024, ..., 16089, 15430, 14834], ..., [ 7755, 8114, 8318, ..., 7903, 8146, 7829], [ 7980, 7764, 8159, ..., 7680, 7954, 8030], [ 7802, 7929, 7885, ..., 7509, 7756, 7825]], [[15887, 17342, 19103, ..., 15943, 14901, 14436], [15416, 17493, 19189, ..., 16118, 15433, 14608], [15578, 17143, 19417, ..., 16151, 15445, 14567], ..., [ 7976, 8048, 8008, ..., 8038, 7926, 7682], [ 8040, 7800, 7903, ..., 7727, 8146, 7988], [ 7648, 7621, 8036, ..., 7444, 7760, 7836]], [[15713, 16996, 19101, ..., 15787, 15245, 14246], [15462, 17378, 18813, ..., 15957, 15037, 14255], [15247, 17023, 18774, ..., 16178, 15039, 14420], ..., [ 7876, 8267, 7935, ..., 8040, 8065, 7620], [ 7759, 7896, 7952, ..., 7567, 7853, 8073], [ 7553, 7772, 7796, ..., 7646, 7737, 7818]]], shape=(61, 512, 512), dtype=int32)
The data contains 61 images, one for each camera position.
We can plot the first image in the sequence to get an impression of what it looks like:
[3]:
da["position", 0].plot(aspect="equal")
[3]:
Computing image sharpness#
The ess.imaging.tools
module provides tools for image manipulation. The sharpness
function computes image sharpness over two spatial dimensions (that need to be specified). The remaining position
dimension will remain in the output.
[4]:
sharp = img.tools.sharpness(da, dims=["dim_1", "dim_2"])
sharp
[4]:
- position: 61
- position(position)float64mm140.0, 141.0, ..., 199.0, 200.0
Values:
array([140., 141., 142., 143., 144., 145., 146., 147., 148., 149., 150., 151., 152., 153., 154., 155., 156., 157., 158., 159., 160., 161., 162., 163., 164., 165., 166., 167., 168., 169., 170., 171., 172., 173., 174., 175., 176., 177., 178., 179., 180., 181., 182., 183., 184., 185., 186., 187., 188., 189., 190., 191., 192., 193., 194., 195., 196., 197., 198., 199., 200.]) - time(position)datetime64ns2025-06-11T10:07:14.603685891, 2025-06-11T10:07:19.443681068, ..., 2025-06-11T10:12:15.939916263, 2025-06-11T10:12:20.779694055
Values:
array(['2025-06-11T10:07:14.603685891', '2025-06-11T10:07:19.443681068', '2025-06-11T10:07:24.291694836', '2025-06-11T10:07:29.135681029', '2025-06-11T10:07:36.387704881', '2025-06-11T10:07:41.239693346', '2025-06-11T10:07:46.111723518', '2025-06-11T10:07:50.687718363', '2025-06-11T10:07:55.555682998', '2025-06-11T10:08:00.435680492', '2025-06-11T10:08:05.283693959', '2025-06-11T10:08:10.023683844', '2025-06-11T10:08:14.879729086', '2025-06-11T10:08:19.735700903', '2025-06-11T10:08:24.587683636', '2025-06-11T10:08:29.439675066', '2025-06-11T10:08:33.991718005', '2025-06-11T10:08:38.851754030', '2025-06-11T10:08:43.699704502', '2025-06-11T10:08:48.555669860', '2025-06-11T10:08:53.403690390', '2025-06-11T10:08:58.247718146', '2025-06-11T10:09:03.099704735', '2025-06-11T10:09:07.951692452', '2025-06-11T10:09:12.787697950', '2025-06-11T10:09:20.067721706', '2025-06-11T10:09:24.919730518', '2025-06-11T10:09:29.763706276', '2025-06-11T10:09:34.619683746', '2025-06-11T10:09:39.467698542', '2025-06-11T10:09:46.723715712', '2025-06-11T10:09:51.387727129', '2025-06-11T10:09:56.155690987', '2025-06-11T10:10:00.983695240', '2025-06-11T10:10:05.835713180', '2025-06-11T10:10:10.679685428', '2025-06-11T10:10:17.947739370', '2025-06-11T10:10:22.803701747', '2025-06-11T10:10:27.671682040', '2025-06-11T10:10:34.923718875', '2025-06-11T10:10:39.475679906', '2025-06-11T10:10:44.327714300', '2025-06-11T10:10:49.179701241', '2025-06-11T10:10:54.047723775', '2025-06-11T10:10:58.907683420', '2025-06-11T10:11:03.775723131', '2025-06-11T10:11:08.639689393', '2025-06-11T10:11:13.515701420', '2025-06-11T10:11:18.375689596', '2025-06-11T10:11:23.247713489', '2025-06-11T10:11:28.123680303', '2025-06-11T10:11:32.959681624', '2025-06-11T10:11:37.719680831', '2025-06-11T10:11:42.255678954', '2025-06-11T10:11:49.539686860', '2025-06-11T10:11:54.379713851', '2025-06-11T10:11:59.231692792', '2025-06-11T10:12:06.499695867', '2025-06-11T10:12:11.347714105', '2025-06-11T10:12:15.939916263', '2025-06-11T10:12:20.779694055'], dtype='datetime64[ns]')
- (position)float64𝟙1.664e+08, 1.737e+08, ..., 3.636e+08, 3.592e+08
Values:
array([1.66367877e+08, 1.73721491e+08, 1.80515730e+08, 1.87450301e+08, 1.95839357e+08, 2.03322584e+08, 2.11381659e+08, 2.21143653e+08, 2.29351388e+08, 2.37960144e+08, 2.46379092e+08, 2.54095879e+08, 2.62174234e+08, 2.69654120e+08, 2.77490212e+08, 2.87548060e+08, 2.97669093e+08, 3.04265665e+08, 3.12310314e+08, 3.24953189e+08, 3.32765238e+08, 3.38134294e+08, 3.46568909e+08, 3.53999035e+08, 3.61792635e+08, 3.70058742e+08, 3.74338346e+08, 3.79159907e+08, 3.85679708e+08, 3.89697049e+08, 3.91578219e+08, 3.98688639e+08, 4.02523118e+08, 4.03725026e+08, 4.08685857e+08, 4.11394311e+08, 4.10592086e+08, 4.13450995e+08, 4.17550265e+08, 4.20062414e+08, 4.18278889e+08, 4.18660099e+08, 4.19266356e+08, 4.18551455e+08, 4.16822756e+08, 4.14089584e+08, 4.15115642e+08, 4.14020582e+08, 4.10765746e+08, 4.08197241e+08, 4.04466194e+08, 4.00390526e+08, 3.97507864e+08, 3.96295749e+08, 3.91828606e+08, 3.83856874e+08, 3.78202144e+08, 3.74145507e+08, 3.69857673e+08, 3.63584695e+08, 3.59170710e+08])
A simple plot of the sharpness reveals a ‘best position’ where the sharpness peaks (around 180 mm):
[5]:
fig = sharp.plot()
fig
[5]:
Find the sharpest image#
To accurately find the sharpest image, and the associated camera stage position with the best focus, we filter outliers by applying a 75th percentile threshold to the data. We then compute the mean position weighted by the sharpness on the remaining points.
[6]:
threshold = sc.scalar(np.percentile(sharp.values, 75))
sel = sharp.data >= threshold
subset = sharp[sel]
subset
[6]:
- position: 16
- position(position)float64mm174.0, 175.0, ..., 188.0, 189.0
Values:
array([174., 175., 176., 177., 178., 179., 180., 181., 182., 183., 184., 185., 186., 187., 188., 189.]) - time(position)datetime64ns2025-06-11T10:10:05.835713180, 2025-06-11T10:10:10.679685428, ..., 2025-06-11T10:11:18.375689596, 2025-06-11T10:11:23.247713489
Values:
array(['2025-06-11T10:10:05.835713180', '2025-06-11T10:10:10.679685428', '2025-06-11T10:10:17.947739370', '2025-06-11T10:10:22.803701747', '2025-06-11T10:10:27.671682040', '2025-06-11T10:10:34.923718875', '2025-06-11T10:10:39.475679906', '2025-06-11T10:10:44.327714300', '2025-06-11T10:10:49.179701241', '2025-06-11T10:10:54.047723775', '2025-06-11T10:10:58.907683420', '2025-06-11T10:11:03.775723131', '2025-06-11T10:11:08.639689393', '2025-06-11T10:11:13.515701420', '2025-06-11T10:11:18.375689596', '2025-06-11T10:11:23.247713489'], dtype='datetime64[ns]')
- (position)float64𝟙4.087e+08, 4.114e+08, ..., 4.108e+08, 4.082e+08
Values:
array([4.08685857e+08, 4.11394311e+08, 4.10592086e+08, 4.13450995e+08, 4.17550265e+08, 4.20062414e+08, 4.18278889e+08, 4.18660099e+08, 4.19266356e+08, 4.18551455e+08, 4.16822756e+08, 4.14089584e+08, 4.15115642e+08, 4.14020582e+08, 4.10765746e+08, 4.08197241e+08])
[7]:
# Compute weighted mean of selected points
pos = (subset.coords["position"] * subset.data).sum() / subset.data.sum()
pos
[7]:
- ()float64mm181.49986403473855
Values:
array(181.49986403)
[8]:
fig.ax.axhline(threshold.value, color="lightgray", label="threshold")
fig.ax.axvline(pos.value, color="k", label="focus point")
fig.ax.legend()
fig
[8]:
Image comparison#
Finally, we can quickly compare the first image in the stack to the sharpest image determined above.
[9]:
# Show a smaller region to highlight details
dx = 100
xslice = ("dim_2", slice(363, 363 + dx))
yslice = ("dim_1", slice(90, 90 + dx))
# Find the closest position to the focus point
closest = subset.coords["position"][
np.argmin(sc.abs(subset.coords["position"] - pos).values)
]
da["position", 0][xslice][yslice].plot(aspect="equal", title="First image") + da[
"position", closest
][xslice][yslice].plot(aspect="equal", title="Sharpest image")
[9]: