Skip to content

try reconstruction filenames are mislabeled when --rotation-axis-auto auto --rotation-axis-method {vo,sift} is used #76

Description

@decarlof

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:

  1. 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.
  2. 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions