Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible issue with entropy based centering #479

Open
MarkRivers opened this issue May 12, 2020 · 38 comments
Open

Possible issue with entropy based centering #479

MarkRivers opened this issue May 12, 2020 · 38 comments
Labels
bug A confirmed issue that needs to be fixed
Projects

Comments

@MarkRivers
Copy link

MarkRivers commented May 12, 2020

I have been using reconstruction software written in IDL at APS beamline 13-BM-D. It uses gridrec (tomoRecon) written in C++ at its core, but uses IDL as the user interface and programming language.

I want to switch to using tomopy. I am comparing results and performance between IDL and tomopy, and I am having problems with entropy based centering. The problem may just be that I don't understand how to use it in tomopy.

For IDL I first did the centering with the following configuration:
• Sinogram padding to 2048 (X size of projections is 1920)
• 10 pixel average of air pixels
• No ring artifact removal

This is a screen shot of the configuration screen. Note that Air pixels=10, Ring smoothing width=0, Padded Sinogram Width=2048.

image

This is a screen shot of the main IDL screen. In the Reconstruct section I have set Optimize method=Entropy, the initial guess of the rotation center=960, Optimize range=30 (+- 15 pixels), and the Optimize step=0.25 pixels. This will reconstruct the upper and lower slices at 120 different centers (30 pixels at 0.25 pixel step). I have selected an upper slice=120 and a lower slice=1080 to do the centering.

image

When I press optimize center I get this plot of the entropy as a function of rotation center. One plot is for the upper slice, the other for the lower slice. These curves show that the entropy is an extremely smooth function of rotation center, with single very well-defined minimum.

image

This is the main screen after optimization. Notice that the rotation center for both the upper and lower slices is 962.50. Thus, within the step size of 0.25 pixels the rotation center is the same for slices 120 and 1080. This means that the rotation axis is very well aligned with the columns of the camera.

image

I changed my initial guess of the center from 960 to 970. That resulted in this plot.

image

The minima were found in the identical location, 962.5.

I repeated the optimization again, but starting with an initial guess of 950. That also resulted in exactly the same center for both slices, 962.5

I optimized over a range of 30 pixels for this example. That required 26.7 seconds total to optimize both slices. In practice it is usually sufficient to optimize over a range of 10 pixels, since the rotation axis typically does not change during an experiment by more than +-5 pixels. Optimizing over a 10 pixel range takes 8.9 seconds.

In IDL I can also find the center using the 0 and 180 views and subtracting the images.
This is the setup for that. Note that Optimize center method=0-180 and the Optimize step is 0.5, which is the maximum resolution for this method.

image

This is the resulting plot. Note that the Y axis label is incorrect, it is not entropy it is the difference between the 0 and flipped 180 image as a function of the shift in one image.

This technique found the center at 963.0, within the 0.5 pixel step size of the value found by the entropy method.

image

Optimizing the center using tomopy

I used this Python program to test finding the centers with tomopy. It is intended to do the same steps as IDL above.

  • Normalize to dark and flat fields
  • Normalize to 10 pixels of air
  • Pad to 2048 pixel width
  • Find the center using the 0-180 method
  • Find the center using the entropy method with initial guesses of 960, 950, and 970.
import tomopy
import dxchange
import logging

top_slice=120
bottom_slice = 1080

logging.basicConfig(
    format='%(asctime)s %(levelname)-8s %(message)s',
    level=logging.INFO,
    datefmt='%Y-%m-%d %H:%M:%S')

logging.info('Reading data ...')
proj, flat, dark, theta = dxchange.read_aps_13bm('NaCl_A1.nc', file_format='netcdf4')

logging.info('Normalizing ...')
norm = tomopy.normalize(proj, flat, dark)

logging.info('Secondary normalize to air ...')
norm = tomopy.normalize_bg(norm, air=10)

npad = int((2048 - norm.shape[2]) / 2)
logging.info('Padding to 2048, npad=%d', npad)
norm = tomopy.misc.morph.pad(norm, axis=2, npad=npad, mode='edge')
logging.info('Shape after padding %s', norm.shape)

logging.info('Taking log ...')
norm = tomopy.minus_log(norm)

center_guess = 960 + npad
logging.info("")
logging.info('Finding centers with 0-180')
top_center = tomopy.find_center_pc(norm[0, top_slice:top_slice+10, :], norm[-1, top_slice:top_slice+10, :], rotc_guess=center_guess, tol=0.5)
bottom_center = tomopy.find_center_pc(norm[0, bottom_slice:bottom_slice+10, :], norm[-1, bottom_slice:bottom_slice+10 :], rotc_guess=center_guess, tol=0.5)
logging.info('top_center: %f, bottom_center:%f', top_center-npad, bottom_center-npad)

center_guess = 960 + npad
logging.info("")
logging.info('Finding centers with entropy, initial guess=%f', center_guess)
top_center = tomopy.find_center(norm, theta, init=center_guess, ind=top_slice, tol=0.5)
bottom_center = tomopy.find_center(norm, theta, init=center_guess, ind=bottom_slice, tol=0.5)
logging.info('top_center: %f, bottom_center:%f', top_center, bottom_center)

center_guess = 950 + npad
logging.info("")
logging.info('Finding centers with entropy, initial guess=%f', center_guess)
top_center = tomopy.find_center(norm, theta, init=center_guess, ind=top_slice, tol=0.5)
bottom_center = tomopy.find_center(norm, theta, init=center_guess, ind=bottom_slice, tol=0.5)
logging.info('top_center: %f, bottom_center:%f', top_center, bottom_center)

center_guess = 970 + npad
logging.info("")
logging.info('Finding centers with entropy, initial guess=%f', center_guess)
top_center = tomopy.find_center(norm, theta, init=center_guess, ind=top_slice, tol=0.5)
bottom_center = tomopy.find_center(norm, theta, init=center_guess, ind=bottom_slice, tol=0.5)
logging.info('top_center: %f, bottom_center:%f', top_center, bottom_center)

#logging.info('Reconstructing')
#recon = tomopy.recon(norm, theta, center=top_center, algorithm='gridrec', sinogram_order=False)

#logging.info('Reconstruction complete')

I have attached the program here as a .txt file.
test_center.py.txt

This is the output when running that program on the same dataset processed with IDL above. It's dimensions are NX=1920, NY=1200, NProjections=900. I have deleted some of the logging.info output from tomopy.find_center() to save space.

corvette:~/scratch/tomo_data/NaCl>python test_center.py
2020-05-12 15:18:23 INFO     Reading data ...
2020-05-12 15:18:30 INFO     Data successfully imported: /home/epics/scratch/tomo_data/NaCl/NaCl_A2.nc
2020-05-12 15:18:30 INFO     Data successfully imported: /home/epics/scratch/tomo_data/NaCl/NaCl_A1.nc
2020-05-12 15:18:30 INFO     Data successfully imported: /home/epics/scratch/tomo_data/NaCl/NaCl_A3.nc
2020-05-12 15:18:31 INFO     Normalizing ...
2020-05-12 15:18:36 INFO     Secondary normalize to air ...
2020-05-12 15:18:46 INFO     Padding to 2048, npad=64
/home/epics/anaconda3/lib/python3.7/site-packages/tomopy/misc/morph.py:141: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  ne.evaluate("arr", out=out[slc_in])
/home/epics/anaconda3/lib/python3.7/site-packages/tomopy/misc/morph.py:148: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  ne.evaluate("vec", local_dict={'vec': arr[slc_l_v]},
/home/epics/anaconda3/lib/python3.7/site-packages/tomopy/misc/morph.py:149: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  out=out[slc_l])
/home/epics/anaconda3/lib/python3.7/site-packages/tomopy/misc/morph.py:150: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  ne.evaluate("vec", local_dict={'vec': arr[slc_r_v]},
/home/epics/anaconda3/lib/python3.7/site-packages/tomopy/misc/morph.py:151: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  out=out[slc_r])
2020-05-12 15:18:48 INFO     Shape after padding (900, 1200, 2048)
2020-05-12 15:18:48 INFO     Taking log ...
2020-05-12 15:18:49 INFO
2020-05-12 15:18:49 INFO     Finding centers with 0-180
2020-05-12 15:18:49 INFO     top_center: 962.250000, bottom_center:962.250000
2020-05-12 15:18:49 INFO
2020-05-12 15:18:49 INFO     Finding centers with entropy, initial guess=1024.000000
2020-05-12 15:18:49 INFO     Trying rotation center: [1024.]
2020-05-12 15:18:50 INFO     Function value = 2.493676
2020-05-12 15:18:50 INFO     Trying rotation center: [1075.2]
…
2020-05-12 15:19:07 INFO     Trying rotation center: [941.2]
2020-05-12 15:19:08 INFO     Function value = 2.444963
2020-05-12 15:19:08 INFO     top_center: 1076.800000, bottom_center:941.600000
2020-05-12 15:19:08 INFO
2020-05-12 15:19:08 INFO     Finding centers with entropy, initial guess=1014.000000
2020-05-12 15:19:08 INFO     Trying rotation center: [1014.]
2020-05-12 15:19:08 INFO     Function value = 2.494112
2020-05-12 15:19:08 INFO     Trying rotation center: [1064.7]
…
2020-05-12 15:19:24 INFO     Trying rotation center: [958.94296875]
2020-05-12 15:19:24 INFO     Function value = 2.444797
2020-05-12 15:19:24 INFO     top_center: 1082.128125, bottom_center:959.339063
2020-05-12 15:19:24 INFO
2020-05-12 15:19:24 INFO     Finding centers with entropy, initial guess=1034.000000
2020-05-12 15:19:24 INFO     Trying rotation center: [1034.]
2020-05-12 15:19:25 INFO     Function value = 2.493973
…
2020-05-12 15:19:39 INFO     Trying rotation center: [956.04609375]
2020-05-12 15:19:40 INFO     Function value = 2.444716
2020-05-12 15:19:40 INFO     top_center: 1072.775000, bottom_center:956.450000m.

Conclusions

Using the 0-180 algorithm in tomopy gives the top and bottom centers as 962.25, which is within 0.25 pixels of the values obtained with the entropy method in IDL (962.5).

However, the following table shows that the centers determined with the entropy method in tomopy are highly scattered, and depend greatly on the initial guess.

Program Initial guess Top slice center Bottom slice center
IDL 960 962.5 962.5
IDL 950 962.5 962.5
IDL 970 962.5 962.5
Tomopy 960 1076.8 941.6
Tomopy 950 1082.1 959.3
Tomopy 970 1072.7 956.4

Am I doing something wrong? Why are the tomopy centers using entropy so wrong, and highly dependent on the initial guess?

@carterbox
Copy link
Member

@dgursoy, implemented the find_center method, but he's on parental leave right now, so thanks for your patience while I get up to speed.

From my understanding, you compared your IDL code with two of TomoPy's three automated find_center algorithms, and you are trying to determine why they don't all match the output of your IDL code. It looks like your IDL code does an exhaustive search for the center given a range and step size. This is pretty much guaranteed to find the center if it is in the requested search range.

As stated in the documentation, find_center is uses Nelder-Mead from scipy.optimize; not an exhaustive search, so we wouldn't expect the answer to be the same. However, I'm shocked that the default parameters did not get closer to the answer.

Since it does seem from your log output that the entropy value is decreasing, I suggest first trying a smaller tol value. This should make SciPy search longer. If that doesn't work, we'll have to check that the entropy is being calculated correctly.

@carterbox
Copy link
Member

@decarlof, do you have any tips for the robustness of find_center?

@MarkRivers
Copy link
Author

Since it does seem from your log output that the entropy value is decreasing, I suggest first trying a smaller tol value. This should make SciPy search longer. If that doesn't work, we'll have to check that the entropy is being calculated correctly.

I decreased tol from 0.5 to 0.1. That did not help. The new results are within 0.2 pixels of the previous results.

Program Initial guess Top slice center Bottom slice center
Tomopy 960 1077.0 941.6
Tomopy 950 1082.1 959.1
Tomopy 970 1073.1 956.6

If you look at my plots of entropy in IDL you will see that they vary by about 0.02 units out of 7.75 when center is off by 10 pixels. This is about 0.26% for a 10 pixel error.

With tomopy we see the following. This is the optimal value it found:

2020-05-12 18:48:33 INFO     Trying rotation center: [956.55097656]
2020-05-12 18:48:33 INFO     Function value = 2.444744

Here is an evaluation at +13 pixels.

2020-05-12 18:48:27 INFO     Trying rotation center: [969.375]
2020-05-12 18:48:28 INFO     Function value = 2.445428

Note that the function value differs from the optimal by 0.00068400, or 0.028%. This is 10x less than I am seeing in IDL for a similar pixel error.

Here is an evaluation at -7 pixels.

2020-05-12 18:48:28 INFO     Trying rotation center: [949.9875]
2020-05-12 18:48:28 INFO     Function value = 2.444744

Note that the function value is exactly the same as at the optimum, within the digits that Python is printing! Something is wrong!

I am beginning to suspect there is something wrong with the entropy calculation in tomopy.

As stated in the documentation, find_center is uses Nelder-Mead from scipy.optimize; not an exhaustive search, so we wouldn't expect the answer to be the same.

The IDL exhaustive search over a range of 10 pixels takes 8.9 seconds. tomopy is taking 16 to 19 seconds with an optimized search, and is coming up with the wrong answer. 10 pixel range is generally all that is required for a particular set of experiments. One does a large range search for one dataset, and then a 10 pixel range should be OK for other datasets until the setup is changed.

But if we can fix the tomopy algorithm then the optimized search should work without needing to know the approximate center.

@carterbox carterbox added the bug A confirmed issue that needs to be fixed label May 13, 2020
@carterbox
Copy link
Member

It seems there is something that needs to be fixed.

The citation for find_center is https://doi.org/10.1364/JOSAA.23.001048; I will read it tomorrow. Do you have a reference that describes how to compute the entropy? I'm not familiar with this metric.

@MarkRivers
Copy link
Author

The paper you cite is what I used in my IDL code. The paper explains how to compute the entropy. It is the sum of H*log(H) where H is the histogram of the image. The entropy is basically a measure of the sharpness of the histogram.

My IDL code that does the entropy minimization is here:
https://github.com/CARS-UChicago/IDL_Tomography/blob/master/optimize_center.pro

At first glance I see a major difference between my implementation and the one in tomopy.

My code has these 2 lines:

 binsize=(histMax-histMin)/1.e4
 h = histogram(r, min=histMin, max=histMax, bin=binsize) > 1

Since my binsize is 1e-4 of the difference between the min and max histogram values that means my histogram has 10,000 bins.

tomopy rotation.py has this line:

   hist, e = np.histogram(rec, bins=64, range=[hmin, hmax])

So tomopy is only using 64 bins in the histogram, rather than 10,000. I'm pretty sure that is at least part of the problem. When the reconstructions differ only slightly the histograms with only 64 bins will be very similar, if not identical. That is exactly what we seem to be seeing.

I wrote the first version of the entropy minimization algorithm in that IDL file in 2007, which was the year after Donath's paper was written. My code has always been publicly available.

@MarkRivers
Copy link
Author

MarkRivers commented May 13, 2020

I tested my idea that increasing the number of histogram bins from 64 to 10000 would fix the problem. I was wrong, it did not help.

I then changed my test program to remove the air normalization and padding steps. I tested using the current version of tomopy/recon/rotation.py, not the version I modified to increase the histogram bins.

This is the new test program. It uses both the entropy and 0-180 centering methods. In both cases tol=0.1. I changed the value of center_guess each time I ran the program.

import tomopy
import dxchange
import logging

top_slice=120
bottom_slice = 1080
tol = 0.1
center_guess = 970

logging.basicConfig(
    format='%(asctime)s %(levelname)-8s %(message)s',
    level=logging.INFO,
    datefmt='%Y-%m-%d %H:%M:%S')

logging.info('Reading data ...')
proj, flat, dark, theta = dxchange.read_aps_13bm('NaCl_A1.nc', file_format='netcdf4')

logging.info('Normalizing ...')
norm = tomopy.normalize(proj, flat, dark)

npad = 0

logging.info('Taking log ...')
norm = tomopy.minus_log(norm)


logging.info("")
logging.info('Finding centers with 0-180')
top_center = tomopy.find_center_pc(norm[0, top_slice:top_slice+10, :], norm[-1, top_slice:top_slice+10, :], rotc_guess=center_guess+npad, tol=tol)
bottom_center = tomopy.find_center_pc(norm[0, bottom_slice:bottom_slice+10, :], norm[-1, bottom_slice:bottom_slice+10 :], rotc_guess=center_guess+npad, tol=tol)
logging.info('top_center: %f, bottom_center:%f', top_center-npad, bottom_center-npad)

logging.info("")
logging.info('Finding centers with entropy, initial guess=%f', center_guess)
top_center = tomopy.find_center(norm, theta, init=center_guess+npad, ind=top_slice, tol=tol, mask=True)
bottom_center = tomopy.find_center(norm, theta, init=center_guess+npad, ind=bottom_slice, tol=tol, mask=True)
logging.info('top_center: %f, bottom_center:%f', top_center-npad, bottom_center-npad)

These are the results.

Method Initial guess Top slice center Bottom slice center
0-180 960 962.20 962.30
0-180 950 962.15 962.30
0-180 970 962.30 962.35
Entropy 960 961.50 962.44
Entropy 950 962.43 962.43
Entropy 970 962.42 962.04

Both methods are working quite well. The entropy method is giving differences of up to 0.9 pixels depending on the initial guess. That seems incorrect when tol=0.1, since one would expect the optimized value to be within tol=0.1.

I now need to figure out if it is the air normalization or the padding that is causing the entropy method to fail.

@newville
Copy link
Contributor

@MarkRivers @carterbox I think there was some recent discussion here about how much FFT padding was happening with TomoPy's version of gridrec, with the conclusion that it should probably be increased (maybe by 2x). I don't know when that changed in the master branch, but you might check that as well - it might have a similar effect of changing the amount of air. There might also be a difference in whether the padding is truly adding zero or a value that is very small but has a valid logarithm.

I might also wonder if it would be faster -- and possibly more stable -- to use scipy.optimize.leastsq (MINPACK) on the array of H*log(H) rather than Nelder-Mead on (H*log(H)).sum().

@MarkRivers
Copy link
Author

MarkRivers commented May 13, 2020

I modified the program in the previous comment as follows, adding the call to tomopy.normalize_bg.

logging.info('Normalizing ...')
norm = tomopy.normalize(proj, flat, dark)

logging.info('Secondary normalize to air ...')
norm = tomopy.normalize_bg(norm, air=10)

These are the results:

Method Initial guess Top slice center Bottom slice center
0-180 960 962.25 962.30
Entropy 960 1020.00 1068.56

So with air normalization the 0-180 method still works fine, but the entropy method fails completely.

I then commented out the air normalization and added in padding to 2048 with mode='edge' as follows:

logging.info('Normalizing ...')
norm = tomopy.normalize(proj, flat, dark)

#logging.info('Secondary normalize to air ...')
#norm = tomopy.normalize_bg(norm, air=10)

npad = int((2048 - norm.shape[2]) / 2)
logging.info('Padding to 2048, npad=%d', npad)
norm = tomopy.misc.morph.pad(norm, axis=2, npad=npad, mode='edge')
logging.info('Shape after padding %s', norm.shape)

Here are the results:

Method Initial guess Top slice center Bottom slice center
0-180 960 962.20 962.25
Entropy 960 1018.30 892.70

So with padding the 0-180 method still works fine, but the entropy method fails completely.

Conclusion
There appears to be a serious problem with the entropy centering method in tomopy when either air normalization or edge mode padding is used. The IDL entropy method works fine with air normalization and/or edge padding.

@MarkRivers
Copy link
Author

MarkRivers commented May 13, 2020

I tried changing the padding to constant mode with a value of 1 as follows:

norm = tomopy.misc.morph.pad(norm, axis=2, npad=npad, mode='constant', constant_values=1)

These are the results:

Method Initial guess Top slice center Bottom slice center
0-180 960 962.15 962.20
Entropy 960 909.20 914.30

Again the 0-180 method works fine but the entropy method fails completely.

@carterbox
Copy link
Member

There are multiple issues with TomoPy's gridrec that need to be addressed. In fact, 5 of the 7 bugs on our bug board are gridrec related.

@MarkRivers, did you check whether TomoPy's _find_center_cost() produces a convex shape for your dataset?

@carterbox carterbox added this to Needs triage in Bug Board via automation May 13, 2020
@decarlof
Copy link
Contributor

@decarlof, do you have any tips for the robustness of find_center?

I am looking into it. I never use the entropy method but the find_center_vo which works well.

@dgursoy
Copy link
Collaborator

dgursoy commented May 13, 2020

A quick feedback after I read some of the comments. There are few differences from the IDL entropy based centering code that I remember. First, the ring correction is not implemented in ours and I remember it was ensuring some robustness. Second, NM optimizer can be replaced by a hierarchical brute search or by another optimizer as Matt suggested. I also remember that selection of histogram bins and some custom data treatments on the range is also important.

@MarkRivers
Copy link
Author

Here is a comparison of 0-180, entropy, and Vo both with and without padding. Here I also list the execution time. The times listed are the times to center both the top slice and the bottom slice.

No padding:

Method Initial guess Top slice center Bottom slice center Execution time (s)
0-180 960 962.20 962.30 <1
Entropy 960 961.50 962.44 16
Vo 960 962.75 963.00 121

So Vo works, but it takes over 2 minutes using the default parameters. That is about 4 times longer than it takes to reconstruct the entire dataset! I have not tried a smaller coarse or fine search radius. @decarlof do you normally use a smaller search radius?

2048 padding:

Method Initial guess Top slice center Bottom slice center Execution time (s)
0-180 960 962.15 962.20 <1
Entropy 960 909.20 914.30 22
Vo 960 962.50 962.75 136

@MarkRivers
Copy link
Author

A quick feedback after I read some of the comments. There are few differences from the IDL entropy based centering code that I remember. First, the ring correction is not implemented in ours and I remember it was ensuring some robustness.

The example I show at the beginning of this issue in IDL has ring correction disabled, so that cannot be the difference.

Second, NM optimizer can be replaced by a hierarchical brute search or by another optimizer as Matt suggested.

I don't believe the problem is the optimizer. The problem is, for example, that the entropy values for centers that are 7 pixels apart are identical (see my second comment from top of issue). That means the problem is either the entropy calculation, or the reconstruction itself. In IDL the entropy is a monotonically decreasing function over at least 30 pixels, and it should never have the same value 7 pixels apart (except for symmetry about the minimum).

I also remember that selection of histogram bins and some custom data treatments on the range is also important.

I tried changing tomopy rotation.py to use the same logic as IDL (10000 bins, clipping histogram to 1 rather than adding 1e-12, etc.). But that did not help. I think the difference is probably in the reconstructions that gridrec is producing on the two systems.

@MarkRivers
Copy link
Author

MarkRivers commented May 14, 2020

I am trying to track down why tomopy and IDL produce such different results for centering based on entropy.

I first wrote a little program that reads in the same dataset as above, does the normalization in tomopy, and writes out slices 600 and 601 as TIFF files. That lets me start with the same input slice for both tomopy and IDL reconstructions and entropy minimization.

I then wrote the following program in tomopy. It does the following:

  • Constructs an array of 320 rotation centers, 960 +- 40 in 0.25 pixel steps.
  • Reads in the TIFF file containing the normalized data for slice 600.
  • Takes the minus log
  • Reconstructs the slice with the default rotation center to compute the min and max for the histogram. This is exactly what tomopy.find_center() does.
  • Repeats the slice 320 times into a 3-D array so that it can be reconstructed with those 320 different centers in a single call to tomopy.recon().
  • Computes the entropy in each of the 320 reconstructed slices using the identical code from tomopy.find_center().
  • Calls tomopy.find_center on the original slice to see how that compares to the brute-force search.

This is the program:

import tomopy
import numpy as np
import dxchange
import logging
import tifffile
import matplotlib.pyplot as plt

center_range = 40.
center_step = 0.25
center_guess = 960.

logging.basicConfig(
    format='%(asctime)s %(levelname)-8s %(message)s',
    level=logging.INFO,
    datefmt='%Y-%m-%d %H:%M:%S')

centers = np.arange(center_guess-center_range, center_guess+center_range, center_step)
num_centers = centers.shape[0]

logging.info('Reading data ...')
slice = tifffile.imread('NaCl_norm_slice600.tiff')
num_projections = slice.shape[0]
nx = slice.shape[1]
slice = slice.reshape((num_projections, 1, nx))
slice = tomopy.minus_log(slice)
theta = np.arange(num_projections) * (np.pi / num_projections)
recon = tomopy.recon(slice, theta, algorithm='gridrec', sinogram_order=False)
hmin = recon.min()
if (hmin < 0):
    hmin *= 2.0
else:
    hmin *= 0.5
hmax = recon.max()
if (hmax < 0):
    hmax *= 0.5
else:
    hmax *= 2.0
data = np.repeat(slice, num_centers, 1)
entropy = np.empty(num_centers)
logging.info('Reconstructing single slice with %d different centers ...', num_centers)
recon = tomopy.recon(data, theta, centers, algorithm='gridrec', sinogram_order=False)
logging.info('Computing entropy ...')
for i in range(num_centers):
    rec = recon[i,:,:]
    hist, e = np.histogram(rec, bins=64, range=[hmin, hmax])
    hist = hist.astype('float32') / rec.size + 1e-12
    entropy[i] = -np.dot(hist, np.log2(hist))
    logging.info('center=%f, entropy=%f', centers[i], entropy[i])

#p = plt.plot(centers, entropy)
#plt.show()

logging.info('Calling tomopy.find_center() ...')
tomopy_center = tomopy.find_center(slice, theta, init=center_guess, mask=False, ind=0, tol=0.1)
logging.info('tomopy_center=%f', tomopy_center)

This is the output of that program, where I have deleted most of the printing of the entropy for each slice.

corvette:~/scratch/tomo_data/NaCl>python -i compute_center_entropy.py
2020-05-14 16:58:41 INFO     Reading data ...
2020-05-14 16:58:43 INFO     Reconstructing single slice with 320 different centers ...
2020-05-14 16:58:50 INFO     Computing entropy ...
2020-05-14 16:58:50 INFO     center=920.000000, entropy=2.615403
2020-05-14 16:58:50 INFO     center=920.250000, entropy=2.615403
2020-05-14 16:58:50 INFO     center=920.500000, entropy=2.614860
2020-05-14 16:58:50 INFO     center=920.750000, entropy=2.614859
2020-05-14 16:58:50 INFO     center=921.000000, entropy=2.615174
2020-05-14 16:58:50 INFO     center=921.250000, entropy=2.615171
2020-05-14 16:58:50 INFO     center=921.500000, entropy=2.614942
2020-05-14 16:58:50 INFO     center=921.750000, entropy=2.614942
2020-05-14 16:58:50 INFO     center=922.000000, entropy=2.614942
2020-05-14 16:58:50 INFO     center=922.250000, entropy=2.614942
...
2020-05-14 16:59:02 INFO     center=998.250000, entropy=2.615497
2020-05-14 16:59:02 INFO     center=998.500000, entropy=2.615661
2020-05-14 16:59:02 INFO     center=998.750000, entropy=2.615660
2020-05-14 16:59:02 INFO     center=999.000000, entropy=2.615546
2020-05-14 16:59:02 INFO     center=999.250000, entropy=2.615547
2020-05-14 16:59:02 INFO     center=999.500000, entropy=2.615802
2020-05-14 16:59:02 INFO     center=999.750000, entropy=2.615802
2020-05-14 16:59:02 INFO     Calling tomopy.find_center() ...
2020-05-14 16:59:03 INFO     Trying rotation center: [960.]
2020-05-14 16:59:03 INFO     Function value = 2.617255
2020-05-14 16:59:03 INFO     Trying rotation center: [1008.]
2020-05-14 16:59:03 INFO     Function value = 2.615720
2020-05-14 16:59:03 INFO     Trying rotation center: [1056.]
2020-05-14 16:59:04 INFO     Function value = 2.620893
2020-05-14 16:59:04 INFO     Trying rotation center: [984.]
2020-05-14 16:59:04 INFO     Function value = 2.615454
2020-05-14 16:59:04 INFO     Trying rotation center: [960.]
2020-05-14 16:59:04 INFO     Function value = 2.617255
2020-05-14 16:59:04 INFO     Trying rotation center: [996.]
2020-05-14 16:59:05 INFO     Function value = 2.615741
2020-05-14 16:59:05 INFO     Trying rotation center: [996.]
2020-05-14 16:59:05 INFO     Function value = 2.615741
2020-05-14 16:59:05 INFO     Trying rotation center: [972.]
2020-05-14 16:59:05 INFO     Function value = 2.615822
2020-05-14 16:59:05 INFO     Trying rotation center: [990.]
2020-05-14 16:59:06 INFO     Function value = 2.615509
2020-05-14 16:59:06 INFO     Trying rotation center: [978.]
2020-05-14 16:59:06 INFO     Function value = 2.615741
2020-05-14 16:59:06 INFO     Trying rotation center: [987.]
2020-05-14 16:59:06 INFO     Function value = 2.615637
2020-05-14 16:59:06 INFO     Trying rotation center: [987.]
2020-05-14 16:59:07 INFO     Function value = 2.615637
2020-05-14 16:59:07 INFO     Trying rotation center: [981.]
2020-05-14 16:59:07 INFO     Function value = 2.615513
2020-05-14 16:59:07 INFO     Trying rotation center: [982.5]
2020-05-14 16:59:07 INFO     Function value = 2.615218
2020-05-14 16:59:07 INFO     Trying rotation center: [981.]
2020-05-14 16:59:08 INFO     Function value = 2.615513
2020-05-14 16:59:08 INFO     Trying rotation center: [983.25]
2020-05-14 16:59:08 INFO     Function value = 2.615220
2020-05-14 16:59:08 INFO     Trying rotation center: [981.75]
2020-05-14 16:59:08 INFO     Function value = 2.615528
2020-05-14 16:59:08 INFO     Trying rotation center: [982.875]
2020-05-14 16:59:09 INFO     Function value = 2.615385
2020-05-14 16:59:09 INFO     Trying rotation center: [982.875]
2020-05-14 16:59:09 INFO     Function value = 2.615385
2020-05-14 16:59:09 INFO     Trying rotation center: [982.125]
2020-05-14 16:59:09 INFO     Function value = 2.615574
2020-05-14 16:59:09 INFO     Trying rotation center: [982.6875]
2020-05-14 16:59:10 INFO     Function value = 2.615526
2020-05-14 16:59:10 INFO     Trying rotation center: [982.6875]
2020-05-14 16:59:10 INFO     Function value = 2.615526
2020-05-14 16:59:10 INFO     Trying rotation center: [982.3125]
2020-05-14 16:59:10 INFO     Function value = 2.615666
2020-05-14 16:59:10 INFO     Trying rotation center: [982.59375]
2020-05-14 16:59:11 INFO     Function value = 2.615508
2020-05-14 16:59:11 INFO     tomopy_center=982.500000

This is a plot of the entropy computed for each of the 320 slices as a function of the rotation center.

image

The problem is clear! The entropy is not at a minimum near the rotation center of ~962.5 it is at a maximum! The entropy is very noisy. tomopy's find_center() happened to find the local minimum at 982.5. That is because it did not search down by 930, or up near 993, but the whole thing is clearly not working. The entropy should be a smooth function with a minimum near 962, not a noisy function with a maximum near 962.

The next job is to duplicate the above program in IDL and compare the results.

@MarkRivers
Copy link
Author

MarkRivers commented May 14, 2020

I realized that in the above test I was setting mask=False in tomopy.find_center(). That is not the default, and not what I was using in the tests above yesterday.

I have modified the program to allow setting mask=True or False, and applying the mask in the program as well as in the call to find_center(). I also changed from testing with slice 600 to testing with slice 120, which is the top slice from the tests yesterday. It is also one of the slices tested with IDL in the first message in this issue.

This is the new version of the program.

import tomopy
import numpy as np
import dxchange
import logging
import tifffile
import matplotlib.pyplot as plt

center_range = 40.
center_step = 0.25
center_guess = 960.
mask = True

logging.basicConfig(
    format='%(asctime)s %(levelname)-8s %(message)s',
    level=logging.INFO,
    datefmt='%Y-%m-%d %H:%M:%S')

centers = np.arange(center_guess-center_range, center_guess+center_range, center_step)
num_centers = centers.shape[0]

logging.info('Reading data ...')
slice = tifffile.imread('NaCl_norm_slice120.tiff')
num_projections = slice.shape[0]
nx = slice.shape[1]
slice = slice.reshape((num_projections, 1, nx))
slice = tomopy.minus_log(slice)
theta = np.arange(num_projections) * (np.pi / num_projections)
recon = tomopy.recon(slice, theta, algorithm='gridrec', sinogram_order=False)
hmin = recon.min()
if (hmin < 0):
    hmin *= 2.0
else:
    hmin *= 0.5
hmax = recon.max()
if (hmax < 0):
    hmax *= 0.5
else:
    hmax *= 2.0
data = np.repeat(slice, num_centers, 1)
entropy = np.empty(num_centers)
logging.info('Reconstructing single slice with %d different centers ...', num_centers)
recon = tomopy.recon(data, theta, centers, algorithm='gridrec', sinogram_order=False)
logging.info('Computing entropy ...')
if mask is True:
    recon = tomopy.circ_mask(recon, axis=0)
for i in range(num_centers):
    rec = recon[i,:,:]
    hist, e = np.histogram(rec, bins=64, range=[hmin, hmax])
    hist = hist.astype('float32') / rec.size + 1e-12
    entropy[i] = -np.dot(hist, np.log2(hist))
    logging.info('center=%f, entropy=%f', centers[i], entropy[i])

#p = plt.plot(centers, entropy)
#plt.show()

logging.info('Calling tomopy.find_center() ...')
tomopy_center = tomopy.find_center(slice, theta, init=center_guess, mask=mask, ind=0, tol=0.1)
logging.info('tomopy_center=%f', tomopy_center)

This is the output

corvette:~/scratch/tomo_data/NaCl>python -i compute_center_entropy.py
2020-05-14 17:24:54 INFO     Reading data ...
2020-05-14 17:24:55 INFO     Reconstructing single slice with 320 different centers ...
2020-05-14 17:25:02 INFO     Computing entropy ...
2020-05-14 17:25:03 INFO     center=920.000000, entropy=2.092763
2020-05-14 17:25:03 INFO     center=920.250000, entropy=2.092763
2020-05-14 17:25:03 INFO     center=920.500000, entropy=2.093292
2020-05-14 17:25:03 INFO     center=920.750000, entropy=2.093291
2020-05-14 17:25:03 INFO     center=921.000000, entropy=2.092808
2020-05-14 17:25:03 INFO     center=921.250000, entropy=2.092808
...
2020-05-14 17:25:16 INFO     center=998.250000, entropy=2.095188
2020-05-14 17:25:16 INFO     center=998.500000, entropy=2.095408
2020-05-14 17:25:16 INFO     center=998.750000, entropy=2.095410
2020-05-14 17:25:16 INFO     center=999.000000, entropy=2.095229
2020-05-14 17:25:16 INFO     center=999.250000, entropy=2.095231
2020-05-14 17:25:16 INFO     center=999.500000, entropy=2.095941
2020-05-14 17:25:16 INFO     center=999.750000, entropy=2.095941
2020-05-14 17:25:16 INFO     Calling tomopy.find_center() ...
2020-05-14 17:25:16 INFO     Trying rotation center: [960.]
2020-05-14 17:25:17 INFO     Function value = 2.809573
2020-05-14 17:25:17 INFO     Trying rotation center: [1008.]
2020-05-14 17:25:17 INFO     Function value = 2.822270
2020-05-14 17:25:17 INFO     Trying rotation center: [912.]
2020-05-14 17:25:17 INFO     Function value = 2.818215
2020-05-14 17:25:17 INFO     Trying rotation center: [936.]
2020-05-14 17:25:18 INFO     Function value = 2.814688
2020-05-14 17:25:18 INFO     Trying rotation center: [984.]
2020-05-14 17:25:18 INFO     Function value = 2.815835
2020-05-14 17:25:18 INFO     Trying rotation center: [948.]
2020-05-14 17:25:18 INFO     Function value = 2.811972
2020-05-14 17:25:18 INFO     Trying rotation center: [972.]
2020-05-14 17:25:19 INFO     Function value = 2.811514
2020-05-14 17:25:19 INFO     Trying rotation center: [966.]
2020-05-14 17:25:19 INFO     Function value = 2.808802
2020-05-14 17:25:19 INFO     Trying rotation center: [972.]
2020-05-14 17:25:19 INFO     Function value = 2.811514
2020-05-14 17:25:19 INFO     Trying rotation center: [963.]
2020-05-14 17:25:20 INFO     Function value = 2.807622
2020-05-14 17:25:20 INFO     Trying rotation center: [960.]
2020-05-14 17:25:20 INFO     Function value = 2.809573
2020-05-14 17:25:20 INFO     Trying rotation center: [964.5]
2020-05-14 17:25:20 INFO     Function value = 2.808437
2020-05-14 17:25:20 INFO     Trying rotation center: [961.5]
2020-05-14 17:25:21 INFO     Function value = 2.807442
2020-05-14 17:25:21 INFO     Trying rotation center: [960.]
2020-05-14 17:25:21 INFO     Function value = 2.809573
2020-05-14 17:25:21 INFO     Trying rotation center: [960.]
2020-05-14 17:25:21 INFO     Function value = 2.809573
2020-05-14 17:25:21 INFO     Trying rotation center: [962.25]
2020-05-14 17:25:22 INFO     Function value = 2.807084
2020-05-14 17:25:22 INFO     Trying rotation center: [963.]
2020-05-14 17:25:22 INFO     Function value = 2.807622
2020-05-14 17:25:22 INFO     Trying rotation center: [961.875]
2020-05-14 17:25:22 INFO     Function value = 2.807075
2020-05-14 17:25:22 INFO     Trying rotation center: [961.5]
2020-05-14 17:25:23 INFO     Function value = 2.807442
2020-05-14 17:25:23 INFO     Trying rotation center: [962.0625]
2020-05-14 17:25:23 INFO     Function value = 2.807530
2020-05-14 17:25:23 INFO     Trying rotation center: [962.0625]
2020-05-14 17:25:23 INFO     Function value = 2.807530
2020-05-14 17:25:23 INFO     Trying rotation center: [961.6875]
2020-05-14 17:25:24 INFO     Function value = 2.806985
2020-05-14 17:25:24 INFO     Trying rotation center: [961.5]
2020-05-14 17:25:24 INFO     Function value = 2.807442
2020-05-14 17:25:24 INFO     Trying rotation center: [961.5]
2020-05-14 17:25:24 INFO     Function value = 2.807442
2020-05-14 17:25:24 INFO     Trying rotation center: [961.78125]
2020-05-14 17:25:25 INFO     Function value = 2.807410
2020-05-14 17:25:25 INFO     Trying rotation center: [961.78125]
2020-05-14 17:25:25 INFO     Function value = 2.807410
2020-05-14 17:25:25 INFO     tomopy_center=961.687500

This is the plot of entropy vs rotation center:
image

Setting mask=True improves the behavior considerably.

Now the entropy does have a minimum near the rotation axis. It is still rather noisy, and one can see how it can find a different local minimum near the true minimum because of the noise.

I am quite sure the IDL code is not applying a mask. Perhaps tomopys implementation of gridrec is producing more noise outside the reconstructed circle than the IDL version?

@MarkRivers
Copy link
Author

Yesterday I showed that tomopy.find_center() failed badly if air normalization was applied.

I just changed the test program above to add the air normalization as follows:

logging.info('Reading data ...')
slice = tifffile.imread('NaCl_norm_slice120.tiff')
num_projections = slice.shape[0]
nx = slice.shape[1]
slice = slice.reshape((num_projections, 1, nx))

logging.info('Secondary normalize to air ...')
slice = tomopy.normalize_bg(slice, air=10)

slice = tomopy.minus_log(slice)

The program still has mask=True.

This is the end of the output:

2020-05-14 17:41:00 INFO     center=997.250000, entropy=2.107031
2020-05-14 17:41:00 INFO     center=997.500000, entropy=2.107091
2020-05-14 17:41:00 INFO     center=997.750000, entropy=2.107092
2020-05-14 17:41:00 INFO     center=998.000000, entropy=2.107103
2020-05-14 17:41:00 INFO     center=998.250000, entropy=2.107102
2020-05-14 17:41:00 INFO     center=998.500000, entropy=2.107107
2020-05-14 17:41:00 INFO     center=998.750000, entropy=2.107106
2020-05-14 17:41:00 INFO     center=999.000000, entropy=2.107028
2020-05-14 17:41:00 INFO     center=999.250000, entropy=2.107028
2020-05-14 17:41:01 INFO     center=999.500000, entropy=2.106822
2020-05-14 17:41:01 INFO     center=999.750000, entropy=2.106822
2020-05-14 17:41:01 INFO     Calling tomopy.find_center() ...
2020-05-14 17:41:01 INFO     Trying rotation center: [960.]
2020-05-14 17:41:01 INFO     Function value = 2.992904
2020-05-14 17:41:01 INFO     Trying rotation center: [1008.]
2020-05-14 17:41:02 INFO     Function value = 2.991738
2020-05-14 17:41:02 INFO     Trying rotation center: [1056.]
2020-05-14 17:41:02 INFO     Function value = 2.992801
2020-05-14 17:41:02 INFO     Trying rotation center: [1032.]
2020-05-14 17:41:02 INFO     Function value = 2.992146
2020-05-14 17:41:02 INFO     Trying rotation center: [984.]
2020-05-14 17:41:03 INFO     Function value = 2.992244
2020-05-14 17:41:03 INFO     Trying rotation center: [1020.]
2020-05-14 17:41:03 INFO     Function value = 2.991049
2020-05-14 17:41:03 INFO     Trying rotation center: [1032.]
2020-05-14 17:41:03 INFO     Function value = 2.992146
2020-05-14 17:41:03 INFO     Trying rotation center: [1014.]
2020-05-14 17:41:04 INFO     Function value = 2.991329
2020-05-14 17:41:04 INFO     Trying rotation center: [1026.]
2020-05-14 17:41:04 INFO     Function value = 2.991480
2020-05-14 17:41:04 INFO     Trying rotation center: [1017.]
2020-05-14 17:41:04 INFO     Function value = 2.991026
2020-05-14 17:41:04 INFO     Trying rotation center: [1014.]
2020-05-14 17:41:05 INFO     Function value = 2.991329
2020-05-14 17:41:05 INFO     Trying rotation center: [1018.5]
2020-05-14 17:41:05 INFO     Function value = 2.991618
2020-05-14 17:41:05 INFO     Trying rotation center: [1018.5]
2020-05-14 17:41:05 INFO     Function value = 2.991618
2020-05-14 17:41:05 INFO     Trying rotation center: [1015.5]
2020-05-14 17:41:06 INFO     Function value = 2.991374
2020-05-14 17:41:06 INFO     Trying rotation center: [1016.25]
2020-05-14 17:41:06 INFO     Function value = 2.990811
2020-05-14 17:41:06 INFO     Trying rotation center: [1015.5]
2020-05-14 17:41:06 INFO     Function value = 2.991374
2020-05-14 17:41:06 INFO     Trying rotation center: [1016.625]
2020-05-14 17:41:07 INFO     Function value = 2.991079
2020-05-14 17:41:07 INFO     Trying rotation center: [1016.625]
2020-05-14 17:41:07 INFO     Function value = 2.991079
2020-05-14 17:41:07 INFO     Trying rotation center: [1015.875]
2020-05-14 17:41:07 INFO     Function value = 2.991561
2020-05-14 17:41:07 INFO     Trying rotation center: [1016.4375]
2020-05-14 17:41:08 INFO     Function value = 2.991157
2020-05-14 17:41:08 INFO     Trying rotation center: [1016.4375]
2020-05-14 17:41:08 INFO     Function value = 2.991157
2020-05-14 17:41:08 INFO     Trying rotation center: [1016.0625]
2020-05-14 17:41:08 INFO     Function value = 2.990784
2020-05-14 17:41:08 INFO     Trying rotation center: [1015.875]
2020-05-14 17:41:09 INFO     Function value = 2.991561
2020-05-14 17:41:09 INFO     Trying rotation center: [1015.875]
2020-05-14 17:41:09 INFO     Function value = 2.991561
2020-05-14 17:41:09 INFO     Trying rotation center: [1016.15625]
2020-05-14 17:41:09 INFO     Function value = 2.991092
2020-05-14 17:41:09 INFO     Trying rotation center: [1016.15625]
2020-05-14 17:41:10 INFO     Function value = 2.991092
2020-05-14 17:41:10 INFO     tomopy_center=1016.062500

This is the plot:
image

So applying air normalization makes the entropy no longer have a minimum at the rotation center. tomopy.find_center is getting 1016.1, rather than 962.

However, IDL seems to work fine with air normalization.

@MarkRivers
Copy link
Author

I have now written an IDL program to perform similar functions to the Python program above.
This is the program:

; This program computes the entropy as a function of center position using
; Gridrec (tomoRecon version) in IDL

center_range = 40.
center_step = 0.25
center_guess = 960.

num_center = (center_range * 2)/center_step
center = (center_guess - center_range) + findgen(num_center) * center_step

print, 'Reading TIFF slices ...'
slice120 = read_tiff('NaCl_norm_slice120.tiff')
slice1080 = read_tiff('NaCl_norm_slice1080.tiff')

;slice120 = smooth(slice120,2)
;slice1080 = smooth(slice1080,2)

s = size(slice120, /dimensions)
num_pixels = s[0]
num_projections = s[1]
vol = fltarr(num_pixels, 2, num_projections)
vol[*,0,*] = slice120
vol[*,1,*] = slice1080

print, 'Initializing tomo_params ...'
tp = tomo_params_init(vol, airPixels=0, ringWidth=0, gr_filtername='shepp', paddedSinogramWidth=num_pixels, paddingaverage=0, sinoScale=1., reconScale=1.)

;help, tp, output=tp_text
;print, tp_text

print, 'Optimizing rotation center ...'
optimize_rotation_center, tp, [0,1], vol, center, entropy

print, 'Determining entropy minima ...'
e1 = entropy[*,0]
e2 = entropy[*,1]
mindiff = min(e1, minpos1) - min(e2, minpos2)
center1 = center[minpos1]
center2 = center[minpos2]
print, 'Center for slice 120:', center1
print, 'Center for slice 1080:', center2
print, 'Plotting results ...'
p1 = plot(center, e1, color='red', symbol='+', name='Slice 120')
p2 = plot(overplot=p1, center, e2+mindiff, color='blue', symbol='x', name='Slice 1080')
leg = legend(target=[p1,p2])

r = reconstruct_slice(tp, 0, vol, center=center1)
write_png, 'recon.png', bytscl(r)

end

This program does the following:

  • Creates an array of rotation centers to test, 960 +-40 in 0.25 pixel steps
  • Reads in the slice120 and slice1080 TIFF files that were created with tomopy. These are normalized to dark and flat, but have no other processing (log, etc.).
  • Puts those 2 slices into a 3-D array (2 slices).
  • Initializes the tomo_params structure that controls the Gridrec/tomoRecon reconstruction. The structure is initialized with:
    • airPixels=0. This disables secondary normalization to air on both sides of the image.
    • ringWidth=0. This disables ring artifact reduction.
    • gr_filtername='shepp' selects the Shepp-Logan filter for GridRec.
    • paddedSinogramWidth=num_pixels. num_pixels is the actual width of the projections, so this disables padding the sinogram.
    • paddingAverage=0. This disables any averaging of the border pixels if sinogram padding is enabled.
    • sinoScale=1. Disables any scaling of the input sinogram
    • reconScale=1. Disables any scaling of the output reconstruction.
  • Calls optimize_rotation_center. This does the following:
    • Calls tomoRecon to reconstruct the two slices at each rotation center. Gridrec reconstructs 2 sinograms at once, one in the real part and one in the imaginary part of a complex number, using the same center. So it is just as fast to reconstruct the top and bottom slices at the same time.
    • Computes the entropy of each reconstructed slice
    • Returns an 2-D array of entropy containing the entropy for each center for each of the 2 slices.
  • Finds the minimum entropy position (best center) for each slice
  • Plots entropy of each slice as a function of center position. Shifts the entropy for the second slice so the minimum is at the same entropy as the first slice to make the plot easier to visualize.
  • Reconstructs the top slice using the optimal center position found.
  • Writes an 8-bit scaled version of the reconstructed slice to a png file so it can be viewed here.

This is the output of the program, the plot of the entropy, and the reconstructed image when the above program is run:

IDL> .run compute_center_entropy
Reading TIFF slices ...
Initializing tomo_params ...
Optimizing rotation center ...
tomo_recon: time to convert to float:   7.0095062e-05
                 time to reconstruct:       13.976023
                          total time:       13.976093
optimize_rotation_center, time=       27.187029
Determining entropy minima ...
Center for slice 120:      961.500
Center for slice 1080:      925.500
Plotting results ...
tomo_recon: time to convert to float:   1.4066696e-05
                 time to reconstruct:      0.42197394
                          total time:      0.42198801

image

recon

To my surprise this plot shows a noisy entropy and no clear minima, which is very different from the plot in the first post in this issue. The top center is 961.5 which is not too far from the correct value of 962.5, but the bottom center is far off. I figured out that the difference was my original post was using the "hann" filter in Gridrec, rather than the "shepp" filter. I thus changed the above program to use 'hann':

tp = tomo_params_init(vol, airPixels=0, ringWidth=0, gr_filtername='hann', paddedSinogramWidth=num_pixels, paddingaverage=0, sinoScale=1., reconScale=1.)

and ran the program again. This was the result:

IDL> .run compute_center_entropy
Reading TIFF slices ...
Initializing tomo_params ...
Optimizing rotation center ...
tomo_recon: time to convert to float:   4.3869019e-05
                 time to reconstruct:       14.081919
                          total time:       14.081963
optimize_rotation_center, time=       27.825037
Determining entropy minima ...
Center for slice 120:      962.500
Center for slice 1080:      962.500
Plotting results ...
tomo_recon: time to convert to float:   1.5020370e-05
                 time to reconstruct:      0.42539501
                          total time:      0.42541003

image

recon

Now the entropy has a beautiful smooth shape with a clear minimum. The centers of both slices are at 962.5

The "hann" filter does some smoothing/noise reduction compared to the "shepp" filter. I then went back to the 'shepp' filter, but before reconstruction I smoothed the sinogram with a 2x2 boxcar filter by uncommenting these 2 lines in the program above:

slice120 = smooth(slice120,2)
slice1080 = smooth(slice1080,2)

This is the result:

IDL> .run compute_center_entropy
Reading TIFF slices ...
Initializing tomo_params ...
Optimizing rotation center ...
tomo_recon: time to convert to float:   4.4107437e-05
                 time to reconstruct:       13.967693
                          total time:       13.967737
optimize_rotation_center, time=       27.139068
Determining entropy minima ...
Center for slice 120:      962.500
Center for slice 1080:      962.500
Plotting results ...
tomo_recon: time to convert to float:   1.8119812e-05
                 time to reconstruct:      0.43086505
                          total time:      0.43088317

image

recon

The result is nearly identical to using the 'hann' filter. So in order to get a good entropy minimization for centering it is necessary, at least with this rather noisy dataset, to either use the 'hann' filter or to do some smoothing on the slices before reconstruction.

I then disabled the smoothing and changed the tomo_params to use the values we typically use when reconstructing:

  • airPixels=10 to do secondary normalization to air on both sides of the sinogram
  • gr_filtername='hann'
  • paddedSinogramWidth=2048 to pad the sinogram to next biggest power of 2.
  • paddingaverage=10 to average the last 10 pixels on each side when padding
tp = tomo_params_init(vol, airPixels=10, ringWidth=0, gr_filtername='hann', paddedSinogramWidth=2048, paddingaverage=10, sinoScale=1., reconScale=1.)

This is the result:

IDL> .run compute_center_entropy
Reading TIFF slices ...
Initializing tomo_params ...
Optimizing rotation center ...
tomo_recon: time to convert to float:   6.1035156e-05
                 time to reconstruct:       14.188906
                          total time:       14.188967
optimize_rotation_center, time=       27.390647
Determining entropy minima ...
Center for slice 120:      962.500
Center for slice 1080:      962.500
Plotting results ...
tomo_recon: time to convert to float:   2.5033951e-05
                 time to reconstruct:      0.42528915
                          total time:      0.42531419

image

recon

The result of the entropy minimization is very similar. However, now the reconstructed image is improved. In all of the previous reconstructions the edge of the aluminum cylinder containing the sample has a bright edge. This is is a reconstruction artifact caused by the fact that the cylinder was slightly larger than the field of view. By doing the secondary normalization to air this artifact is removed. Note however, that this artifact did not appear to interfere with the entropy minimization.

@decarlof
Copy link
Contributor

decarlof commented May 17, 2020 via email

@newville
Copy link
Contributor

@MarkRivers @decarlof Sorry this will be so long:

I have often had trouble finding centers for XRF sinograms. I figured this was just sort of marginal data with XRF-specific self-absorption, but the discussion here prompted me to look at the automated centering too. What I find (for a 660 x values 720 angles over 360 degree XRF dataset at https://millenia.cars.aps.anl.gov/gsecars/data/Newville/tomo_slice.npz is consistent with what we normally see: the centering is often OK but needs tweaking by as much as 10 pixels. By eye for this data, the center is around 330 pixels, which gives a reconstruction

image

Following but trying to simplify the tomopy code, I have this:

import numpy as np
import matplotlib.pyplot as plt
from tomopy import recon, circ_mask

zfile = np.load('tomo_slice.npz')
sino  = zfile['arr_0']
omega = zfile['arr_1']
omega = np.radians(omega)

centers = np.linspace(-25, 25, 201) + 325

rec = recon(sino, omega, center=325, sinogram_order=True,
            algorithm='gridrec', filter_name='shepp')
rec = circ_mask(rec, axis=0)

# tomopy score, simplified rescaling of histogram bounds 
rmin, rmax = rec.min(), rec.max()
rmin  -= 0.5*(rmax-rmin)
rmax  += 0.5*(rmax-rmin)

tpscore = []
for cen in centers:
    rec = recon(sino, omega, center=cen, sinogram_order=True,
                algorithm='gridrec', filter_name='shepp')
    rec = circ_mask(rec, axis=0)
    hist, e = np.histogram(rec, bins=64, range=[rmin, rmax])
    hist    = 1.e-15 + hist/rec.size
    tpscore.append(-np.dot(hist, np.log2(hist)))

plt.plot(centers, tpscore, label='tomopy score')
plt.legend()
plt.title('centering scores')
plt.xlabel('center')
plt.show()

this gives

Figure_1

which doesn't seem too noisy, but is also several pixels away from center=330.

Then I remembered that a while back I was playing with auto-focusing of microscope images, and found that using neg-entropy as the score to optimize was often problematic. I played with other methods then broke down and looked up some papers(!). It turns out that some nice studies had also shown neg-entropy to be only OK. The recommended score, especially when close to in-focus, was just "RMS image":

 ((image-image.mean())**2).sum()

("make the bright spots bright and the dark spots dark"). Trying this for centering (and now rescaling the scores to go from 0 to 1) is just:

blur, tpscore = [], []

for cen in centers:
    rec = recon(sino, omega, center=cen, sinogram_order=True,
                algorithm='gridrec', filter_name='shepp')
    rec = circ_mask(rec, axis=0)
    hist, e = np.histogram(rec, bins=64, range=[rmin, rmax])
    hist    = 1.e-15 + hist/rec.size
    tpscore.append(-np.dot(hist, np.log2(hist)))

    # simple blurriness score
    blur.append(-((rec - rec.mean())**2).sum())    
    
def rescale(s):
    s = np.array(s)
    return (s-s.min()) / (s.max() - s.min())

plt.plot(centers, rescale(tpscore), label='tomopy score')
plt.plot(centers, rescale(blur), label='blurriness')
plt.legend()
plt.title('centering scores')
plt.xlabel('center')
plt.show()

which gives this:

Figure_1

which is pretty consistent with what I see for microscope images too: this blurriness score is very good when you are close to focus, and pretty useless when far away from it.

Would it be worth adding to tomopy a centering method that combined the current neg-entropy score and this blurriness? I would suggest doing that as a two-element array to be minimized in the least-squares sense so that you didn't have to fuss with trying to automatically scale the two scores.

If this is potentially useful, I'd be willing to make a PR for it. Is there a sufficient suite of test data to be able to run some benchmarks to decide which centering method should be the default?

@decarlof
Copy link
Contributor

@newville I get mixed results:
In case of weakly absorbing samples with large sections moving in and out I get:

Figure_1

in case of regular (good absorption and signal) well centered samples, I get:
Figure_2

I have a large collection of both types with a sort of ground truth for the center (after vo auto-centering the user confirmed by inspection and tweaked when necessary, which is for vo less than 10% of the time on the first class of samples and almost never in the second class) so I will run the centering on all to see how much each method depends from the sample.

For the code, I am using a slightly modified version of yours (here) where I basically included the exact code that is in tomopy + added per @MarkRivers suggestion filter_name='shepp'.

@newville
Copy link
Contributor

@decarlof Interesting -- thanks. I found for our limited XRF tomo data that finding the center first with neg-entropy then with blurriness worked pretty well. I wasn't sure whether this had been tried with other CMT datasets.

@MarkRivers
Copy link
Author

I have done a comparison of entopy calculations with tomopy and IDL with the dataset from @newville.

This is the Python code, very similar to Matt's but writing out a TIFF file with the reconstruction at center=330.

import numpy as np
import matplotlib.pyplot as plt
from tomopy import recon, circ_mask
import tifffile

zfile = np.load('tomo_slice.npz')
sino  = zfile['arr_0']
omega = zfile['arr_1']
omega = np.radians(omega)

centers = np.linspace(-25, 25, 201) + 325

rec = recon(sino, omega, center=325, sinogram_order=True,
            algorithm='gridrec', filter_name='shepp')
rec = circ_mask(rec, axis=0)

# tomopy score, simplified rescaling of histogram bounds 
rmin, rmax = rec.min(), rec.max()
rmin  -= 0.5*(rmax-rmin)
rmax  += 0.5*(rmax-rmin)

tpscore = []
for cen in centers:
    rec = recon(sino, omega, center=cen, sinogram_order=True,
                algorithm='gridrec', filter_name='shepp')
    rec = circ_mask(rec, axis=0)
    hist, e = np.histogram(rec, bins=64, range=[rmin, rmax])
    hist    = 1.e-15 + hist/rec.size
    tpscore.append(-np.dot(hist, np.log2(hist)))

opt_center = np.argmin(tpscore)
print('Best center=%f', centers[opt_center])
plt.plot(centers, tpscore, label='tomopy score')
plt.legend()
plt.title('centering scores')
plt.xlabel('center')
plt.show(block=False)

rec = recon(sino, omega, center=330, sinogram_order=True,
            algorithm='gridrec', filter_name='shepp')
rec = rec.reshape((rec.shape[1], rec.shape[2]))
rec = np.rot90(rec, 3)
tifffile.imwrite('newville_tomopy.tiff', 1e6*rec)

For some reason the tomopy reconstructions are rotated by 270 degrees compared to the IDL reconstructions, so I use rot90 so the TIFF files read into ImageJ are the same orientation. I also scaled both the IDL and tomopy images by 1e6 before writing to TIFF, because ImageJ GUIs don't handle very small numbers so well.

This is the IDL code.

; This program determines the rotation center of Matt Newville's slice using IDL

center_range = 50.
center_step = 0.25
center_guess = 325.

num_center = (center_range * 2)/center_step
center = (center_guess - center_range) + findgen(num_center) * center_step
omega = findgen(721)/2

print, 'Reading TIFF slice ...'
slice = read_tiff('tomo_slice.tiff')
; This slice already has negative log taken, revert
slice = exp(-slice)

s = size(slice, /dimensions)
num_pixels = s[0]
num_projections = s[1]
; tomoRecon wants at least 2 slices to reconstruct, just duplicate
vol = fltarr(num_pixels, 2, num_projections)
vol[*,0,*] = slice
vol[*,1,*] = slice

print, 'Initializing tomo_params ...'
tp = tomo_params_init(vol, airPixels=0, ringWidth=0, gr_filtername='shepp', paddedSinogramWidth=1024, paddingaverage=0, sinoScale=1., reconScale=1.)

print, 'Optimizing rotation center ...'
optimize_rotation_center, tp, [0,1], vol, center, angles=omega, entropy

print, 'Determining entropy minima ...'
e = entropy[*,0]
mine = min(e, minpos)
maxe = max(e)
center_pos = center[minpos]
norm = (e-mine)/(maxe-mine)
print, 'Center for slice:', center_pos
print, 'Plotting results ...'
p1 = plot(center, norm, color='red', symbol='+', name='Slice')

rec = reconstruct_slice(tp, 0, vol, angles=omega, center=330)
write_tiff, 'newville_idl.tiff', xresol=1, yresol=1, units=1, 1e6*rec, /float

end

It does the same thing as the Python code using the Gridrec/tomoRecon library called from IDL.

This is the entropy plot with tomopy, which is the same as Matt's first plot:

image

This is the entropy plot with IDL:

image

They are somewhat similar, but IDL has a pronounced double minimum, with a local maximum at the correct rotation center of 330. Note that this dataset suffers from artifacts because the sample was actually too absorbing so there are pronounced streaks in the image in the directions where the beam traversed more than one highly absorbing particle.

This is the tomopy reconstruction from the TIFF file, displayed in ImageJ over the gray scale range -500 to +1000.

image

This is the IDL reconstruction from the TIFF file, displayed in ImageJ over the gray scale range -500 to +1000.

image

The two images look very similar, it is very difficult to see any differences by eye.

This is the difference of the two images, tomopy minus IDL.

image

So there are actually significant differences between the reconstructions, both inside the sample area, and outside of it.

This is a measurement of the statistics in a 50x50 region located at 0,0, i.e. in the upper left corner of each image.

image

The first line is the tomopy reconstruction, the second line is the IDL reconstruction. Note that the tomopy reconstruction has significantly more noise: the standard deviation, min, and max are all nearly twice as large as the IDL reconstruction.

The differences in the entropy calculation are due to these differences in the reconstructions.

Why do we have these differences when we are nominally using the same Gridrec code?

@MarkRivers
Copy link
Author

@decarlof wrote:

I have a large collection of both types with a sort of ground truth for the center (after vo auto-centering the user confirmed by inspection and tweaked when necessary, which is for vo less than 10% of the time on the first class of samples and almost never in the second class) so I will run the centering on all to see how much each method depends from the sample.

@decarlof could you make available 32-bit float TIFF files for normalized data for a couple of slices from each of those datasets? I would be interested to compare IDL entropy with tomopy entropy on those.

@carterbox
Copy link
Member

Why do we have these differences when we are nominally using the same Gridrec code?

@MarkRivers, TomoPy gridrec has been adjusted for performance reasons (to use faster libraries and compiler features) a few times over the years, so IMO it's very likely that the behavior was unintentionally changed. If you have a thorough understanding of the gridrec algorithm, I would be very grateful if you could check the source code for deviations from the original.

@MarkRivers
Copy link
Author

If you have a thorough understanding of the gridrec algorithm, I would be very grateful if you could check the source code for deviations from the original.

@carterbox I will be happy to do that. I want to switch to using tomopy, but need to figure out why the reconstructions are almost 2X noisier than we have been getting with IDL. Hopefully it is just some setting that can be changed.

In order to track it down I need to be able to build the development version of tomopy, rather than using the conda pre-built distribution. When I try to do that on my Centos 7 system I get the following errors:

corvette:~/tomography/tomopy>python setup.py develop


--------------------------------------------------------------------------------
-- Trying "Ninja" generator
--------------------------------
---------------------------
----------------------
-----------------
------------
-------
--
Not searching for unused variables given on the command line.
-- The C compiler identification is GNU 4.8.5
-- Check for working C compiler using: Ninja
-- Check for working C compiler using: Ninja -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/epics/tomography/tomopy/_cmake_test_compile/build
--
-------
------------
-----------------
----------------------
---------------------------
--------------------------------
-- Trying "Ninja" generator - success
--------------------------------------------------------------------------------

Configuring Project
  Working directory:
    /home/epics/tomography/tomopy/_skbuild/linux-x86_64-3.7/cmake-build
  Command:
    cmake /home/epics/tomography/tomopy -G Ninja -DCMAKE_INSTALL_PREFIX:PATH=/home/epics/tomography/tomopy/_skbuild/linux-x86_64-3.7/cmake-install -DPYTHON_EXECUTABLE:FILEPATH=/home/epics/anaconda3/bin/python -DPYTHON_VERSION_STRING:STRING=3.7.3 -DPYTHON_INCLUDE_DIR:PATH=/home/epics/anaconda3/include/python3.7m -DPYTHON_LIBRARY:FILEPATH=/home/epics/anaconda3/lib/libpython3.7m.so -DSKBUILD:BOOL=TRUE -DCMAKE_MODULE_PATH:PATH=/home/epics/anaconda3/lib/python3.7/site-packages/skbuild/resources/cmake -DCMAKE_BUILD_TYPE:STRING=Release

CMake Error at CMakeLists.txt:2 (cmake_minimum_required):
  CMake 3.9 or higher is required.  You are running version 2.8.12.2


-- Configuring incomplete, errors occurred!
Traceback (most recent call last):
  File "/home/epics/anaconda3/lib/python3.7/site-packages/skbuild/setuptools_wrap.py", line 586, in setup
    languages=cmake_languages
  File "/home/epics/anaconda3/lib/python3.7/site-packages/skbuild/cmaker.py", line 240, in configure
    os.path.abspath(CMAKE_BUILD_DIR())))

An error occurred while configuring with CMake.
  Command:
    cmake /home/epics/tomography/tomopy -G Ninja -DCMAKE_INSTALL_PREFIX:PATH=/home/epics/tomography/tomopy/_skbuild/linux-x86_64-3.7/cmake-install -DPYTHON_EXECUTABLE:FILEPATH=/home/epics/anaconda3/bin/python -DPYTHON_VERSION_STRING:STRING=3.7.3 -DPYTHON_INCLUDE_DIR:PATH=/home/epics/anaconda3/include/python3.7m -DPYTHON_LIBRARY:FILEPATH=/home/epics/anaconda3/lib/libpython3.7m.so -DSKBUILD:BOOL=TRUE -DCMAKE_MODULE_PATH:PATH=/home/epics/anaconda3/lib/python3.7/site-packages/skbuild/resources/cmake -DCMAKE_BUILD_TYPE:STRING=Release
  Source directory:
    /home/epics/tomography/tomopy
  Working directory:
    /home/epics/tomography/tomopy/_skbuild/linux-x86_64-3.7/cmake-build
Please see CMake's output for more information.
corvette:~/tomography/tomopy>

The error appears to be that I need a newer version of cmake. Are there instructions somewhere on how to set up a system to build tomopy?

@carterbox
Copy link
Member

We have a development guide that is helpful (hopefully). Everything should "just work" if you set up a conda environment using one of our provided environment yamls.

@MarkRivers
Copy link
Author

My apologies, I just discovered I had missed the step of setting up the tomopy environment and activating it. Now the build seems to have worked fine!

@MarkRivers
Copy link
Author

I am going to open a new issue on the tomopy implementation of gridrec. That is independent of the centering issue.

@decarlof
Copy link
Contributor

@decarlof wrote:

I have a large collection of both types with a sort of ground truth for the center (after vo auto-centering the user confirmed by inspection and tweaked when necessary, which is for vo less than 10% of the time on the first class of samples and almost never in the second class) so I will run the centering on all to see how much each method depends from the sample.

@decarlof could you make available 32-bit float TIFF files for normalized data for a couple of slices from each of those datasets? I would be interested to compare IDL entropy with tomopy entropy on those.

@MarkRivers normalized sino are here, these are 10 per datasets around the center. The readme file has the list of vo and manual centers.

@MarkRivers
Copy link
Author

I am trying to copy those files with Globus. It fails every time with this error in the Globus event log:
Error (session setup)
Endpoint: My desktop computer camaro (6e7bb600-390a-11e8-b985-0ac6873fc732)
Server: Globus Connect
Command: SITE UPRT 5fGO BXPHWVts/PVWWTZIB+llpF 1,2013266431,164.54.102.61,44566,host
Message: Fatal FTP response

Details: 500 globus_xio: ICE negotiation failed.\r\n

What could be wrong?

@decarlof
Copy link
Contributor

@MarkRivers I shared this folder with Globus user: Mark Rivers (rivers@anl.gov). Did you perhaps log in into Globus with different credentials?

@decarlof
Copy link
Contributor

@MarkRivers I found another set of credentials (rivers@cars.uchicago.edu) I just shared folder to these one. Please try again

@nikitinvv
Copy link
Collaborator

@MarkRivers, just a small observation. Maybe I am wrong. I see in both your provided tests in IDL and tomopy you use the Shep logan filter. It is defined differently in these two packages:

Tomopy:

// Shepp-Logan filter
float
filter_shepp(float x, int i, int j, int fwidth, const float* pars)
{
    if(i == 0)
        return 0.0;
    return fabsf(2 * x) * (sinf(M_PI * x) / (M_PI * x));
}

IDL:

float shlo(float x){	/* Shepp-Logan filter */
	return abs(sin(pi*x)/pi);
}
struct {char* name; float (*fp)(float);} fltbl[]= {
	{"shlo",shlo},		/* The default choice */
	{"shepp",shlo},
	{"hann",hann},
	{"hamm",hamm},
	{"hamming",hamm},
	{"ramp",ramp},
	{"ramlak",ramp}
};	

The Shepp-Logan as it is defined in IDL kills more high frequencies than the one in tomopy, thus the results are less noisy. To my opinion, the Shepp-Logan filter in IDL is defined incorrectly (x is missing in the denominator?). Could you please verify that? Also for the center searching I would use other filters that really kill high frequencies: hann, hamm, parzen..

@MarkRivers
Copy link
Author

I'm not sure I agree. This is the tomopy definition:

return fabsf(2 * x) * (sinf(M_PI * x) / (M_PI * x));

If you look at this expression carefully you will see that the first "x" and the last "x" cancel, because one is in the numerator and the other is in the denominator, assuming X is always non-negative. This then becomes:

return fabsf(2) * (sinf(M_PI * x) / (M_PI));

This now differs from the IDL definition only by a factor of +-2, not by "x". So I don't think it is changing high frequency content.

With IDL I do normally use the "hann" filter when centering. However, I tried that with tomopy and it also gave very different results from IDL.

@newville
Copy link
Contributor

Is x strictly on [0, 1]?  If so, why are either version use abs? Both are.
If x can go outside [0, 1], then they are not the same.

@nikitinvv
Copy link
Collaborator

nikitinvv commented May 26, 2020

@MarkRivers, @newville if you look at all filter functions in IDL and in tomopy:

float shlo(float x){	/* Shepp-Logan filter */
	return abs(sin(pi*x)/pi);
}

float hann(float x){	/* Hann filter */
	return abs(x)*0.5*(1.+cos(2*pi*x));
}

float hamm(float x){	/* Hamming filter */
	return abs(x)*(0.54+0.46*cos(2*pi*x));
}

float ramp(float x){	/* Ramp filter */
	return abs(x);
}

Tomopy:

// Shepp-Logan filter
float
filter_shepp(float x, int i, int j, int fwidth, const float* pars)
{
    if(i == 0)
        return 0.0;
    return fabsf(2 * x) * (sinf(M_PI * x) / (M_PI * x));
}

// Cosine filter
float
filter_cosine(float x, int i, int j, int fwidth, const float* pars)
{
    return fabsf(2 * x) * (cosf(M_PI * x));
}

// Hann filter
float
filter_hann(float x, int i, int j, int fwidth, const float* pars)
{
    return fabsf(2 * x) * 0.5 * (1. + cosf(2 * M_PI * x / pars[0]));
}

// Hamming filter
float
filter_hamming(float x, int i, int j, int fwidth, const float* pars)
{
    return fabsf(2 * x) * (0.54 + 0.46 * cosf(2 * M_PI * x / pars[0]));
}

// Ramlak filter
float
filter_ramlak(float x, int i, int j, int fwidth, const float* pars)
{
    return fabsf(2 * x);
}

In tomopy pars[0]=0.5 by default, and range for x is [0,0.5]. What is the range for x in IDL? For consistency with tomopy hann, hamming.. filters it should be in [0,1]. But then the shepp-logan is not consistent for both versions.

@MarkRivers
Copy link
Author

@MarkRivers I found another set of credentials (rivers@cars.uchicago.edu) I just shared folder to these one. Please try again

Just for the record, the problem was not the credentials. The problem was that @decarlof computer was on an APS beamline network, and my computer was on an APS CAT network. APS networking had an ACL in place between those networks that blocked Globus. They modified the ACL and now it works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A confirmed issue that needs to be fixed
Projects
Bug Board
  
Needs triage
Development

No branches or pull requests

6 participants