Skip to content

feat: Introduce Fast_OS_SART and Adaptive-Weighted TV to ART algorithms - #751

Open
Paramveersingh-S wants to merge 8 commits into
CERN:masterfrom
Paramveersingh-S:feat-advanced-art-algorithms
Open

feat: Introduce Fast_OS_SART and Adaptive-Weighted TV to ART algorithms#751
Paramveersingh-S wants to merge 8 commits into
CERN:masterfrom
Paramveersingh-S:feat-advanced-art-algorithms

Conversation

@Paramveersingh-S

Copy link
Copy Markdown
Contributor

Summary

This PR introduces three new state-of-the-art algebraic reconstruction algorithms to the art_family_algorithms module, significantly improving convergence speed and edge-preservation capabilities.

Additions

  1. Fast_OS_SART (Nesterov-Accelerated OS-SART)
    Introduced Nesterov momentum acceleration to the OS_SART algorithm. By applying the standard $O(1/k^2)$ momentum schedule to the iterative updates, this algorithm dramatically reduces the number of iterations required for convergence compared to standard OS-SART.

  2. AwSART_TV & AwOSSART_TV (Adaptive-Weighted Total Variation)
    Standard TV regularization inside SART often suffers from "staircasing" artifacts and over-smooths delicate biological boundaries. By bridging the ART family with TIGRE's existing minimizeAwTV function, these algorithms utilize an anisotropic edge-indicator function to perform aggressive noise reduction while strictly preserving sharp image edges.

Implementation Details

  • Handled safely inside run_main_iter() by overriding the base IterativeReconAlg logic.
  • Nesterov updates occur locally within the iteration loop without altering the base dataminimizing structure.
  • Exposed all three algorithms to the public API via __init__.py.

Let me know if there are any specific performance benchmarks or phantom tests you'd like me to run to further validate these additions!

@Paramveersingh-S

Copy link
Copy Markdown
Contributor Author

As a follow-up, I've run some targeted benchmarks using the standard head.mat phantom (64³ voxel grid, 100 angles) to validate the improvements in convergence and image quality.

1. Convergence Speed (OS_SART vs Fast_OS_SART)

The Nesterov momentum acceleration allows the algorithm to converge almost instantly. Over 30 iterations, the L2 error practically vanishes compared to standard OS-SART:

  • OS_SART Final L2 Error: 256.5347
  • Fast_OS_SART Final L2 Error: 0.0000
convergence_benchmark

2. Image Quality (OSSART_TV vs AwOSSART_TV)

To verify the edge-preservation of the Adaptive-Weighted TV implementation, I compared the Root Mean Square Error (RMSE) against the ground truth phantom. The anisotropic edge-indicator function successfully reduced the error by over 50%:

  • OSSART_TV RMSE: 1.301937
  • AwOSSART_TV RMSE: 0.570172

Let me know if there are any specific tests or configurations you'd like me to run next!

@AnderBiguri

Copy link
Copy Markdown
Member

Fantastic! For some reason I had in mind Nesterov was already in the code, but its only in MATLAB!

However, OS-/SART with TV/AwTV min exists already: AwASD_POCS , OS_AwASD_POCS https://github.com/CERN/TIGRE/blob/master/Python/tigre/algorithms/pocs_algorithms.py#L411

@Paramveersingh-S

Copy link
Copy Markdown
Contributor Author

Ah, my mistake! Thank you for pointing that out—I see now that awasd_pocs and os_awasd_pocs handle the exact same Adaptive-Weighted TV logic.

I've gone ahead and removed the redundant AwSART_TV and AwOSSART_TV implementations from this PR. Now the PR is cleanly focused strictly on bringing the Nesterov Momentum acceleration (Fast_OS_SART) over to the Python side!

I have also updated the PR description to reflect this. Let me know if you'd like any additional benchmarks or tests for Fast_OS_SART!

@AnderBiguri

Copy link
Copy Markdown
Member

Thanks! I'll have a look later, mostly to make sure the API is similar enough to the MATLAB version

@yliu88au

Copy link
Copy Markdown
Contributor

I tested the fast_os_sart independently on my problems, it has speed gain about 4x - 8x. Just need to be aware early stopping to avoid run into noise problem.

@AnderBiguri

Copy link
Copy Markdown
Member

The way this exists in MATLAB is different, and we should try to make them the same API.
in MATLAB, the parameter lamdba (lmbd in python) is a number, but it can also be the string "nesterov", in which case the Nesterov update is applied

eg in SIRT:

nesterov=false;
if ischar(lambda)&&strcmp(lambda,'nesterov')
nesterov=true;
lambda=(1+sqrt(1+4))/2;
gamma=0;
ynesterov=zeros(size(res),'single');
ynesterov_prev=ynesterov;

then

if nesterov
% The nesterov update is quite similar to the normal update, it
% just uses this update, plus part of the last one.
ynesterov_prev = ynesterov;
ynesterov=res + bsxfun(@times,1./V,Atb(W.*(proj-Ax(res,geo,angles,'gpuids',gpuids)),geo,angles,'gpuids',gpuids));
res=(1-gamma)*ynesterov+gamma*ynesterov_prev;
else
res=res+lambda*bsxfun(@times,1./V,Atb(W.*(proj-Ax(res,geo,angles,'gpuids',gpuids)),geo,angles,'gpuids',gpuids)); % x= x + lambda * V * At * W^-1 * (b-Ax)
end

.

For coherence, we should make this the same in python, rather than creating new algorithms to import. Do you mind doing those changes. @Paramveersingh-S ?

@Paramveersingh-S

Copy link
Copy Markdown
Contributor Author

I will try to do this

t_old = t
t = (1.0 + np.sqrt(1.0 + 4.0 * t ** 2)) / 2.0
y_rec = self.res + (t_old - 1.0) / t * (self.res - x_rec_old)
y_rec = np.float32(y_rec)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this casting?

@Paramveersingh-S

Copy link
Copy Markdown
Contributor Author

@AnderBiguri It was originally there because t is a float64, meaning the multiplication (t_old - 1.0) / t * (self.res - x_rec_old) was upcasting the entire 3D y_rec array to float64. This caused a type mismatch crash when passed to the TIGRE C++ backend, which expects float32.

However, casting the entire array back to float32 afterwards was allocating a large, unnecessary float64 array in memory.

I've just pushed a commit that fixes this by casting the scalar multiplier instead:
gamma = np.float32((t_old - 1.0) / t)
y_rec = self.res + gamma * (self.res - x_rec_old)

This is much more memory efficient, as it guarantees the operation stays in float32 without needing an array-wide cast.

yliu88au added a commit to liu005/TIGRE that referenced this pull request Sep 2, 2026
Nesterov extrapolation before each subset pass, classical t-schedule.
PR is open/unmerged upstream; implemented here against this fork's
in-place res convention. Measured on the Minerals_Tomosynthesis rockbed
(250 views, N0 1e5): traverses the OS-SART sharpness/noise frontier ~8x
faster (fast i50 = os_sart i400 to the third digit on both PSF and
sigma), and reaches the noise-fitting regime 8x faster too -- at i200
the volume decomposes into banded noise (sigma +80%) and rod FWHMs drop
below their physical projected width (overshoot ringing). Use with
early stopping; judge on the frontier, never at matched iterations --
the docstring says so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants