
For all my spatial functions, the input parameter list should starts with

def fun(adata, sample, spatial, ...)

the three parameters is fixed for every function, no matter it is used or not.

The adata structure contains the following key guaranteed:
- obs['x'], obs['y']: x and y coordinates in global coordinate system.
- user should specify a key for cell type categories if needed
- obsm['spatial']: contains the 2d spatial obsm, copied from obs['x'] obs['y']

kNN added:
- Adds transformed adjacency matrix to adata.obsp[f'connectivities.{key_added}']
- Adds kNN distances of selected distance metrics to adata.obsp[f'distances.{key_added}']
- Adds k-nearest neighbor indices to adata.obsm[f'knn.{key_added}']
- Adds k-nearest neighbor distances to adata.obsm[f'knn.d.{key_added}']
for a common neighborhood graph in replacement to scanpy.

- uns['spatial'] contains the spatial metadata dictionary (not image) in the following format

# structure of the metadata dict:
# {
#   'sample': {
#     'he': {
#       # transformation from the global to the image coordinate system
#       'transformation': np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]),
#       'shape': [2000, 4096, 3],
#       'channels': ['r', 'g', 'b']
#     },
#     'image2': {
#       'transformation': np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]]),
#       'shape': [1000, 2048, 3],
#       'channels': ['r', 'g', 'b']
#     },
#     'mask': {
#       'transformation': np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]]),
#       'shape': [1000, 2048],
#     }
#     ...
#   }
# }

spatial is a zarr store that lazily reads for real image data

# structure of the spatial zarr group:
#
#   spatial
#    |- sample
#        |- he    (976, 1024, 3)    # post mIF stains 
#        |- codex (976, 1024, 27)   # codex
#        |- tsa   (976, 1024, 6)    # tsa
#        |- mask  (976, 1024)       # segmentation mask
#
#   channels MUST be placed in the final dimension with order (y, x, c)

sample is a string indicating sample name. commonly of no use except plotting.