Summary
When tomocupy recon_steps --reconstruction-type try (or tomocupy recon --reconstruction-type try) is invoked together with --rotation-axis-auto auto --rotation-axis-method vo (or sift), the per-TIFF filenames written to disk are silently mis-anchored at the image centre nx/2, while the image content inside each TIFF is reconstructed using the centre returned by the auto-detection method.
The label printed in the filename therefore does not correspond to the rotation axis position that was actually used to render that slice. The offset is exactly nx/2 − auto_detected_center.
This makes the common workflow
"run a try-recon, visually pick the best slice, pass that slice's centre to a full recon"
silently incorrect by nx/2 − auto_center pixels for any dataset where the rotation axis is not at the geometric image centre. The full recon ends up at the wrong axis without any warning.
Affected versions
Reproduced on tomocupy-1.1.0 (CUDA/cupy backend, default save-format=h5nolinks). Reading the dataio/reader.py source suggests the bug exists in current main as well.
Affected flag combination
--reconstruction-type try (or try_lamino)
- AND
--rotation-axis-auto auto
- AND
--rotation-axis-method {vo, sift}
A try-recon run without --rotation-axis-auto (i.e. with an explicit --rotation-axis X) is not affected — that is presumably why the bug has gone unnoticed: most users manually specify the sweep centre when running try.
The --rotation-axis-method ai path is not affected: when method == 'ai', _check_use_ai() returns True and the if args.rotation_axis_auto == 'auto' and not use_ai: gate in run_recsteps / run_rec skips _find_center() entirely. params.centeri therefore stays at the default nx/2, save_centers is built against that same nx/2, and the label-to-content relationship save_center == nx/2 - shift_array[i] == actual_axis_ used holds correctly. The bug is specific to the two methods (vo, sift) that flow through _find_center() (and would affect any future auto-detect method added to that branch).
Minimal reproduction
The same dataset, reconstructed two ways. Both runs are valid invocations of tomocupy recon_steps. (CaCO3temp_049.h5 is one of our in-situ scans; the only assumption used below is that nx = 2048 and vo's auto-detected centre for this dataset is 1016.)
# RUN 1 — manual --rotation-axis (correct labels)
tomocupy recon_steps --reconstruction-type try \
--rotation-axis 1016 \
--center-search-width 50 --center-search-step 0.5 \
--file-name CaCO3temp_049.h5 \
--pixel-size 0.69 --energy 30 --propagation-distance 120 \
--retrieve-phase-method paganin --fbp-filter shepp --retrieve-phase-alpha 0.0007 \
--dark-file-name CaCO3roompreT2_006.h5 --flat-file-name CaCO3roompreT2_006.h5 \
--remove-stripe-method fw \
--out-path-name /tmp/run_manual
# RUN 2 — --rotation-axis-auto auto --rotation-axis-method vo (mislabeled)
tomocupy recon_steps --reconstruction-type try \
--rotation-axis-auto auto --rotation-axis-method vo \
--center-search-width 50 --center-search-step 0.5 \
--file-name CaCO3temp_049.h5 \
--pixel-size 0.69 --energy 30 --propagation-distance 120 \
--retrieve-phase-method paganin --fbp-filter shepp --retrieve-phase-alpha 0.0007 \
--dark-file-name CaCO3roompreT2_006.h5 --flat-file-name CaCO3roompreT2_006.h5 \
--remove-stripe-method fw \
--out-path-name /tmp/run_auto
Observed behaviour
/tmp/run_manual/ TIFF range (filename labels): 966.50 → 1066.00 (centred on 1016, vo's centre)
/tmp/run_auto / TIFF range (filename labels): 974.50 → 1074.00 (centred on 1024, nx/2 — wrong)
Pixel-level proof that the labels are off by nx/2 − vo_center = 8
diff( /tmp/run_manual/recon_slice1023_center1016.00.tiff ,
/tmp/run_auto /recon_slice1023_center1024.00.tiff )
max|diff| = 8.3e-10 mean|diff| = 7.3e-11 (i.e. bit-identical)
The TIFF labeled 1016 in the manual run and the TIFF labeled 1024 in the auto run contain the same reconstruction — same axis, same pixels, same numerics. The auto run just put the wrong number in the filename.
For symmetry, a separate full-mode test confirms the actual centre used by the auto run is 1016 (vo's value), not 1024:
diff( manual `--rotation-axis 1016` full recon ,
manual `--rotation-axis 1016` try-recon TIFF labeled 1016 )
max|diff| = 1.0e-6 (bit-identical, expected)
So nx/2 − auto_center (here, 8) is exactly the discrepancy between the saved filename and the centre actually used inside the file.
Real-world impact (how we discovered this)
We ran a 305-sample auto-vo try-recon and used the resulting TIFFs as input to an external AI center-finder. The AI correctly identified the visually-best TIFF in each case, returned that TIFF's filename label, and we then re-launched ~13 TB of full reconstructions passing those labels as --rotation-axis. Every full reconstruction ended up at the wrong axis by nx/2 − vo_center. We caught it only when one of the team noticed that slice 110
3 of an "AI-centered" recon was visibly worse than the equivalent slice of the original (vo-auto, full) recon. Several hours of diagnosis later, we traced it to the ordering bug below.
If a user runs an auto-vo try, eyeballs the TIFFs, picks the visually-best one, and re-runs --reconstruction-type full --rotation-axis <that_filename_label>, they get a misaligned full recon. The error is silent unless the sample is large enough or the bias direction is unfortunate enough to produce visible artifacts at off-mid slices.
Root cause
In tomocupy/dataio/reader.py, init_sizes_try() is called from the reader's __init__ (line 78):
def __init__(self, args):
...
self.init_sizes()
if args.reconstruction_type[:3] == 'try':
self.init_sizes_try() # <-- save_centers built here using the *default* centeri
...
At this point args.rotation_axis == -1 (its default), so init_sizes() resolves centeri = ni/2, and init_sizes_try() then computes
save_centers = (params.centeri - shift_array) * 2**args.binning + params.st_n
# ^^^^^^^^^^^^^^
# = ni/2 for any auto-detection invocation
The reader is then passed to _find_center() (in tomocupy/__main__.py):
def _find_center(cl_reader):
clrotthandle = FindCenter(cl_reader)
args.rotation_axis = clrotthandle.find_center() # vo / sift runs here, returns e.g. 1016
params.center = args.rotation_axis
params.centeri = args.rotation_axis # centeri NOW updated to 1016
log.warning(f'set rotation axis {args.rotation_axis}')
params.centeri is now correct (1016) and is consequently used by the filter (backproj_functions.py:103) during reconstruction. But params.save_centers is never re-anchored — it is still the array computed against the original ni/2. So:
- The reconstruction inside each saved TIFF correctly uses
centeri = 1016
- The filename written for that TIFF still uses
(1024 − shift_array[i])
- The two disagree by exactly
nx/2 − vo_center pixels
The AI path (--rotation-axis-method ai) is not affected: _find_center() is skipped (use_ai == True), so params.centeri is never overwritten and save_centers remains correctly anchored at the value (nx/2) it was built against. The center_of_rotation_cache that's threaded into inference_pipeline thus contains true axis values, not mis-anchored labels.
Suggested fix
Minimal patch — re-anchor save_centers inside _find_center() once params.centeri is known. This is one block and is backwards-compatible:
# tomocupy/__main__.py
def _find_center(cl_reader):
clrotthandle = FindCenter(cl_reader)
args.rotation_axis = clrotthandle.find_center()
params.center = args.rotation_axis
params.centeri = args.rotation_axis
log.warning(f'set rotation axis {args.rotation_axis}')
# FIX: re-anchor try-mode save_centers labels now that centeri is known.
# Previously, save_centers was built in init_sizes_try() with the default
# centeri = ni/2, which left TIFF filenames mis-anchored at image centre
# while the image content was rendered at the auto-detected centeri.
if args.reconstruction_type[:3] == 'try' and hasattr(params, 'shift_array'):
params.save_centers = ((params.centeri - params.shift_array)
* 2**args.binning + params.st_n)
Equivalent fix in _find_center_ai() for the AI path.
Alternative fixes considered
- Reorder operations. Move
init_sizes_try() out of Reader.__init__ and into __main__.py explicitly after _find_center(). Cleaner conceptually but touches more of the public API.
- Warn the user. Add a
log.warning in init_sizes_try() when args.rotation_axis_auto == 'auto' so users are told that filenames will not reflect the detected centre. Doesn't fix anything but at least prevents silent failures.
I recommend the first patch (re-anchor in _find_center) because it eliminates the bug entirely with no behaviour change for users who don't use the auto-detect combo, and because it requires no changes to call sites or class hierarchies.
Workaround for users until a fix lands
Either:
- Pass
--rotation-axis X manually when running --reconstruction-type try. The filenames will then be anchored at X, matching the centre used in the reconstruction.
- Or don't use the saved filename label for the downstream full-recon command — instead, parse the
Rotation center search finished: <X> debug log line and pass that X directly to the full recon's --rotation-axis.
Environment
- tomocupy 1.1.0 (
process/tomocupy-1.1.0/ in output HDF5)
- cupy 12.3.0, torch 2.12.0+cu130
- NVIDIA Tesla on a beamline workstation (Rocky 9 / RHEL 9 family)
- Detector 2048 × 2048 and 2048 × 2448, 1501 projections, 180° sweep, paganin + fw + shepp pipeline
- Sample data: out-of-distribution in-situ CaCO3 (2026 campaign at APS 2-BM)
Summary
When
tomocupy recon_steps --reconstruction-type try(ortomocupy recon --reconstruction-type try) is invoked together with--rotation-axis-auto auto --rotation-axis-method vo(orsift), the per-TIFF filenames written to disk are silently mis-anchored at the image centrenx/2, while the image content inside each TIFF is reconstructed using the centre returned by the auto-detection method.The label printed in the filename therefore does not correspond to the rotation axis position that was actually used to render that slice. The offset is exactly
nx/2 − auto_detected_center.This makes the common workflow
silently incorrect by
nx/2 − auto_centerpixels for any dataset where the rotation axis is not at the geometric image centre. The full recon ends up at the wrong axis without any warning.Affected versions
Reproduced on
tomocupy-1.1.0(CUDA/cupy backend, defaultsave-format=h5nolinks). Reading thedataio/reader.pysource suggests the bug exists in currentmainas well.Affected flag combination
--reconstruction-type try(ortry_lamino)--rotation-axis-auto auto--rotation-axis-method {vo, sift}A try-recon run without
--rotation-axis-auto(i.e. with an explicit--rotation-axis X) is not affected — that is presumably why the bug has gone unnoticed: most users manually specify the sweep centre when running try.The
--rotation-axis-method aipath is not affected: whenmethod == 'ai',_check_use_ai()returnsTrueand theif args.rotation_axis_auto == 'auto' and not use_ai:gate inrun_recsteps/run_recskips_find_center()entirely.params.centeritherefore stays at the defaultnx/2,save_centersis built against that samenx/2, and the label-to-content relationshipsave_center == nx/2 - shift_array[i] == actual_axis_ usedholds correctly. The bug is specific to the two methods (vo,sift) that flow through_find_center()(and would affect any future auto-detect method added to that branch).Minimal reproduction
The same dataset, reconstructed two ways. Both runs are valid invocations of
tomocupy recon_steps. (CaCO3temp_049.h5is one of our in-situ scans; the only assumption used below is thatnx = 2048and vo's auto-detected centre for this dataset is1016.)Observed behaviour
Pixel-level proof that the labels are off by
nx/2 − vo_center = 8The TIFF labeled 1016 in the manual run and the TIFF labeled 1024 in the auto run contain the same reconstruction — same axis, same pixels, same numerics. The auto run just put the wrong number in the filename.
For symmetry, a separate full-mode test confirms the actual centre used by the auto run is 1016 (vo's value), not 1024:
So
nx/2 − auto_center(here,8) is exactly the discrepancy between the saved filename and the centre actually used inside the file.Real-world impact (how we discovered this)
We ran a 305-sample auto-vo try-recon and used the resulting TIFFs as input to an external AI center-finder. The AI correctly identified the visually-best TIFF in each case, returned that TIFF's filename label, and we then re-launched ~13 TB of full reconstructions passing those labels as
--rotation-axis. Every full reconstruction ended up at the wrong axis bynx/2 − vo_center. We caught it only when one of the team noticed that slice 1103 of an "AI-centered" recon was visibly worse than the equivalent slice of the original (vo-auto, full) recon. Several hours of diagnosis later, we traced it to the ordering bug below.
If a user runs an auto-vo try, eyeballs the TIFFs, picks the visually-best one, and re-runs
--reconstruction-type full --rotation-axis <that_filename_label>, they get a misaligned full recon. The error is silent unless the sample is large enough or the bias direction is unfortunate enough to produce visible artifacts at off-mid slices.Root cause
In
tomocupy/dataio/reader.py,init_sizes_try()is called from the reader's__init__(line 78):At this point
args.rotation_axis == -1(its default), soinit_sizes()resolvescenteri = ni/2, andinit_sizes_try()then computesThe reader is then passed to
_find_center()(intomocupy/__main__.py):params.centeriis now correct (1016) and is consequently used by the filter (backproj_functions.py:103) during reconstruction. Butparams.save_centersis never re-anchored — it is still the array computed against the originalni/2. So:centeri = 1016(1024 − shift_array[i])nx/2 − vo_centerpixelsThe AI path (
--rotation-axis-method ai) is not affected:_find_center()is skipped (use_ai == True), soparams.centeriis never overwritten andsave_centersremains correctly anchored at the value (nx/2) it was built against. Thecenter_of_rotation_cachethat's threaded intoinference_pipelinethus contains true axis values, not mis-anchored labels.Suggested fix
Minimal patch — re-anchor
save_centersinside_find_center()onceparams.centeriis known. This is one block and is backwards-compatible:Equivalent fix in
_find_center_ai()for the AI path.Alternative fixes considered
init_sizes_try()out ofReader.__init__and into__main__.pyexplicitly after_find_center(). Cleaner conceptually but touches more of the public API.log.warningininit_sizes_try()whenargs.rotation_axis_auto == 'auto'so users are told that filenames will not reflect the detected centre. Doesn't fix anything but at least prevents silent failures.I recommend the first patch (re-anchor in
_find_center) because it eliminates the bug entirely with no behaviour change for users who don't use the auto-detect combo, and because it requires no changes to call sites or class hierarchies.Workaround for users until a fix lands
Either:
--rotation-axis Xmanually when running--reconstruction-type try. The filenames will then be anchored atX, matching the centre used in the reconstruction.Rotation center search finished: <X>debug log line and pass thatXdirectly to the full recon's--rotation-axis.Environment
process/tomocupy-1.1.0/in output HDF5)