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

General beamcenter calibration #83

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
88 changes: 88 additions & 0 deletions src/PyHyperScattering/BeamCentering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import warnings
import numpy as np
import xarray as xr
from pyFAI import azimuthalIntegrator
from scipy.optimize import minimize
from tqdm.auto import tqdm


@xr.register_dataarray_accessor('beamcenter')
class CenteringAccessor:

def __init__(self, xr_obj):
self._obj = xr_obj
self._pyhyper_type = 'reduced'
try:
self._chi_min = np.min(xr_obj.chi)
self._chi_max = np.max(xr_obj.chi)
self._chi_range = [self._chi_min, self._chi_max]
except AttributeError:
self._pyhyper_type = 'raw'
self.integrator = None
self.centering_energy = None

def create_integrator(self, energy,poni1=None,poni2=None):
if poni1 == None:
poni1 = self._obj.poni1
if poni2 == None:
poni2 = self._obj.poni2
return azimuthalIntegrator.AzimuthalIntegrator(
self._obj.dist, self._obj.poni1, self._obj.poni2,
self._obj.rot1, self._obj.rot2, self._obj.rot3,
pixel1=self._obj.pixel1, pixel2=self._obj.pixel2,
wavelength=1.239842e-6/energy
)

def optimization_func(self, x, image, obj, energy,
q_min, q_max, pbar=None,
chi_min=-179, chi_max=179,
num_points=150):
#print(f'evaluating objective at {x}')
integrator = obj.create_integrator(energy,poni1=x[0],poni2=x[1])
_, image_int = integrator.integrate_radial(image, num_points,
radial_range=(q_min, q_max),
azimuth_range=(chi_min, chi_max),
radial_unit='q_A^-1')
if pbar is not None:
pbar.update(1)
return np.var(image_int[image_int != 0])

def refine_geometry(self, energy, q_min, q_max,
chi_min=-179, chi_max=179,
poni1_guess=None, poni2_guess=None,
bounds=None, method='Nelder-Mead',
num_points=150, max_iter = 150,
verbose=False, ):
if not poni1_guess:
poni1_guess = self._obj.poni1
poni2_guess = self._obj.poni2
if (not self.integrator) | (self.centering_energy != energy):
self.integrator = self.create_integrator(energy=energy)
self.centering_energy = energy
if not bounds:
bounds = [(poni1_guess*0.9, poni1_guess*1.1),
(poni2_guess*0.9, poni2_guess*1.1)]
options = {}
options['maxiter'] = max_iter
if verbose:
options['disp'] = True
image = self._obj.sel(energy=energy).values
with tqdm(total=1,desc='Optimizing Beamcenter') as pbar:
res = minimize(self.optimization_func, (poni1_guess, poni2_guess),
args=(image, self, energy,
q_min, q_max, pbar, chi_min, chi_max,
num_points),
bounds=bounds, method=method, options=options)

if res.success:
if (res.x == (self._obj.poni1,self._obj.poni2)).all():
print(f'Optimization successful, already at optimum values. Nothing changed.')

else:
print(f'Optimization successful. Updating beamcenter to ({res.x[0]},{res.x[1]}), old values were ({self._obj.poni1},{self._obj.poni2})')
self._obj.attrs['poni1'] = res.x[0]
self._obj.attrs['poni2'] = res.x[1]

return res
else:
warnings.warn('Optimization was unsuccessful. Try new guesses and start again')
141 changes: 141 additions & 0 deletions src/PyHyperScattering/DrawMask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import warnings
import xarray as xr
import numpy as np
import math

try:
import holoviews as hv
import hvplot.xarray

import skimage.draw
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm,Normalize
except (ModuleNotFoundError,ImportError):
warnings.warn('Could not import package for interactive integration utils. Install holoviews and scikit-image.',stacklevel=2)
import pandas as pd

import json

@xr.register_dataarray_accessor('drawmask')
class DrawMask:
'''
Utility class for interactively drawing a mask in a Jupyter notebook.


Usage:

Instantiate a DrawMask object using a PyHyper single image frame.

Call DrawMask.ui() to generate the user interface

Call DrawMask.mask to access the underlying mask, or save/load the raw mask data with .save or .load


'''

def __init__(self,xr_obj):
'''
Construct a DrawMask object

Args:
frame (xarray): a single data frame with pix_x and pix_y axes


'''
self._obj = xr_obj
self._pyhyper_type = 'reduced'
try:
self._chi_min = np.min(xr_obj.chi)
self._chi_max = np.max(xr_obj.chi)
self._chi_range = [self._chi_min, self._chi_max]
except AttributeError:
self._pyhyper_type = 'raw'

self.path_annotator = hv.annotate.instance()
self.poly = hv.Polygons([])

def ui(self,energy):
'''
Draw the DrawMask UI in a Jupyter notebook.


Returns: the holoviews object
'''
if energy == None:
if len(self._obj.shape) > 2:
try:
img = self._obj.isel(system=0)
except KeyError as e:
raise KeyError('This tool needs a single frame, not a stack! .sel down to a single frame before starting!') from e
else:
img = self._obj.sel(energy=energy)

if len(img.shape) > 2:
warnings.warn('This tool needs a single frame, not a stack! .sel down to a single frame before starting!',stacklevel=2)

self.frame = img

self.fig = self.frame.hvplot(cmap='terrain',clim=(5,5000),logz=True,data_aspect=1)


print('Usage: click the "PolyAnnotator" tool at top right. DOUBLE CLICK to start drawing a masked object, SINGLE CLICK to add a vertex, then DOUBLE CLICK to finish. Click/drag individual vertex to adjust.')
return self.path_annotator(
self.fig * self.poly.opts(
width=self.frame.shape[0],
height=self.frame.shape[1],
responsive=False),
annotations=['Label'],
vertex_annotations=['Value'])


def save(self,fname):
'''
Save a parametric mask description as a json dump file.

Args:
fname (str): name of the file to save

'''
dflist = []
for i in range(len(self.path_annotator.annotated)):
dflist.append(self.path_annotator.annotated.iloc[i].dframe(['x','y']).to_json())

with open(fname, 'w') as outfile:
json.dump(dflist, outfile)

def load(self,fname):
'''
Load a parametric mask description from a json dump file.

Args:
fname (str): name of the file to read from

'''
with open(fname,'r') as f:
strlist = json.load(f)
print(strlist)
dflist = []
for item in strlist:
dflist.append(pd.read_json(item))
print(dflist)
self.poly = hv.Polygons(dflist)

self.path_annotator(
self.fig * self.poly.opts(
width=self.frame.shape[1],
height=self.frame.shape[0],
responsive=False),
annotations=['Label'],
vertex_annotations=['Value'])


@property
def mask(self):
'''
Render the mask as a numpy boolean array.
'''
mask = np.zeros(self.frame.shape).astype(bool)
for i in range(len(self.path_annotator.annotated)):
mask |= skimage.draw.polygon2mask(self.frame.shape,self.path_annotator.annotated.iloc[i].dframe(['x','y']))

return mask
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@

import json

class Check:
@xr.register_dataarray_accessor('geocheck')
class GeoCheck:
'''
Quick Utility to display a mask next to an image, to sanity check the orientation of e.g. an imported mask

'''
def checkMask(integrator,img,img_min=1,img_max=10000,img_scaling='log',alpha=1):
def __init__(self, xr_obj):
self._obj = xr_obj
self._pyhyper_type = 'reduced'
try:
self._chi_min = np.min(xr_obj.chi)
self._chi_max = np.max(xr_obj.chi)
self._chi_range = [self._chi_min, self._chi_max]
except AttributeError:
self._pyhyper_type = 'raw'

def checkMask(self,integrator,energy=None,img_min=1,img_max=10000,img_scaling='log',alpha=1):
'''
draw an overlay of the mask and an image

Expand All @@ -32,6 +43,15 @@ def checkMask(integrator,img,img_min=1,img_max=10000,img_scaling='log',alpha=1):
img_max: max value to display
img_scaling: 'lin' or 'log'
'''
if energy == None:
if len(self._obj.shape) > 2:
try:
img = self._obj.isel(system=0)
except KeyError as e:
raise KeyError('This tool needs a single frame, not a stack! .sel down to a single frame before starting!') from e
else:
img = self._obj.sel(energy=energy)

if len(img.shape) > 2:
warnings.warn('This tool needs a single frame, not a stack! .sel down to a single frame before starting!',stacklevel=2)

Expand All @@ -43,7 +63,7 @@ def checkMask(integrator,img,img_min=1,img_max=10000,img_scaling='log',alpha=1):
img.plot(norm=norm,ax=ax)
ax.set_aspect(1)
ax.imshow(integrator.mask,origin='lower',alpha=alpha)
def checkCenter(integrator,img,img_min=1,img_max=10000,img_scaling='log'):
def checkCenter(self,integrator,energy=None,img_min=1,img_max=10000,img_scaling='log'):
'''
draw the beamcenter on an image

Expand All @@ -54,6 +74,15 @@ def checkCenter(integrator,img,img_min=1,img_max=10000,img_scaling='log'):
img_max: max value to display
img_scaling: 'lin' or 'log'
'''
if energy == None:
if len(self._obj.shape) > 2:
try:
img = self._obj.isel(system=0)
except KeyError as e:
raise KeyError('This tool needs a single frame, not a stack! .sel down to a single frame before starting!') from e
else:
img = self._obj.sel(energy=energy)

if len(img.shape) > 2:
warnings.warn('This tool needs a single frame, not a stack! .sel down to a single frame before starting!',stacklevel=2)

Expand All @@ -70,7 +99,7 @@ def checkCenter(integrator,img,img_min=1,img_max=10000,img_scaling='log'):
ax.add_patch(beamcenter)
ax.add_patch(guide1)
ax.add_patch(guide2)
def checkAll(integrator,img,img_min=1,img_max=10000,img_scaling='log',alpha=1,d_inner=50,d_outer=150):
def checkAll(self,integrator,energy=None,img_min=1,img_max=10000,img_scaling='log',alpha=1,d_inner=50,d_outer=150):
'''
draw the beamcenter and overlay mask on an image

Expand All @@ -81,6 +110,15 @@ def checkAll(integrator,img,img_min=1,img_max=10000,img_scaling='log',alpha=1,d_
img_max: max value to display
img_scaling: 'lin' or 'log'
'''
if energy == None:
if len(self._obj.shape) > 2:
try:
img = self._obj.isel(system=0)
except KeyError as e:
raise KeyError('This tool needs a single frame, not a stack! .sel down to a single frame before starting!') from e
else:
img = self._obj.sel(energy=energy)

if len(img.shape) > 2:
warnings.warn('This tool needs a single frame, not a stack! .sel down to a single frame before starting!',stacklevel=2)

Expand All @@ -98,6 +136,8 @@ def checkAll(integrator,img,img_min=1,img_max=10000,img_scaling='log',alpha=1,d_
ax.add_patch(guide1)
ax.add_patch(guide2)
ax.imshow(integrator.mask,origin='lower',alpha=alpha)

@xr.register_dataarray_accessor('drawmask')
class DrawMask:
'''
Utility class for interactively drawing a mask in a Jupyter notebook.
Expand All @@ -114,7 +154,7 @@ class DrawMask:

'''

def __init__(self,frame):
def __init__(self,xr_obj):
'''
Construct a DrawMask object

Expand All @@ -123,26 +163,42 @@ def __init__(self,frame):


'''
if len(frame.shape) > 2:
warnings.warn('This tool needs a single frame, not a stack! .sel down to a single frame before starting!',stacklevel=2)
self._obj = xr_obj
self._pyhyper_type = 'reduced'
try:
self._chi_min = np.min(xr_obj.chi)
self._chi_max = np.max(xr_obj.chi)
self._chi_range = [self._chi_min, self._chi_max]
except AttributeError:
self._pyhyper_type = 'raw'

self.frame=frame

self.fig = frame.hvplot(cmap='terrain',clim=(5,5000),logz=True,data_aspect=1)

self.poly = hv.Polygons([])
self.path_annotator = hv.annotate.instance()

def ui(self):
def ui(self,energy):
'''
Draw the DrawMask UI in a Jupyter notebook.


Returns: the holoviews object
'''
if energy == None:
if len(self._obj.shape) > 2:
try:
img = self._obj.isel(system=0)
except KeyError as e:
raise KeyError('This tool needs a single frame, not a stack! .sel down to a single frame before starting!') from e
else:
img = self._obj.sel(energy=energy)

if len(img.shape) > 2:
warnings.warn('This tool needs a single frame, not a stack! .sel down to a single frame before starting!',stacklevel=2)

self.frame = img

self.fig = self.frame.hvplot(cmap='terrain',clim=(5,5000),logz=True,data_aspect=1)


'''
self.poly = hv.Polygons([])
self.path_annotator = hv.annotate.instance()
print('Usage: click the "PolyAnnotator" tool at top right. DOUBLE CLICK to start drawing a masked object, SINGLE CLICK to add a vertex, then DOUBLE CLICK to finish. Click/drag individual vertex to adjust.')
return self.path_annotator(
self.fig * self.poly.opts(
Expand Down
Loading
Loading