This note summarizes the coordinate conventions used when working with the current high-resolution (HR) TIFF slices and low-resolution (LR) MINC slices, together with the affine-based mapping rules between them.
The main goal is to avoid confusion between:
- image array coordinates
- full-image pixel coordinates
- world coordinates from the affines
There are three useful spaces.
These are the coordinates used to index a loaded 2D image array.
- Convention:
(z, x) - Meaning:
(row, col) - Indexing form:
img[z, x]
This is the natural convention when working directly with numpy arrays or image display code.
These are pixel coordinates in the full HR or LR image reference frame, not in a local crop.
- Convention:
(x, z) - Meaning: geometric pixel axes in the full slice
If a point is defined inside a crop, it must be converted to full-image pixel coordinates before performing HR/LR mapping.
These are physical coordinates obtained with the affine matrices.
- Convention:
(x, y, z) - Units: physical space defined by the input affines
World coordinates are useful mainly at the affine boundary. Most processing can stay in image or full-image pixel coordinates.
For the current HR data:
- the raw 2D image array is indexed as
(z, x) - the HR affine expects voxel coordinates in
(x, y, z)order
This means that a point read from the HR array must be reordered before being passed to the affine.
If a point is observed on the raw HR array as:
(z_hr, x_hr)then the corresponding HR voxel to feed into the affine is:
(x_hr, 0, z_hr)For the current LR data:
- the raw 2D slice view is indexed as
(z, x) - the LR affine uses voxel order
(z, y, x)
This means that after applying the inverse LR affine, the returned voxel already follows LR voxel order:
(z_lr, y_lr, x_lr)For 2D work on a single LR slice, the useful coordinates are therefore:
(z_lr, x_lr) = (lr_voxel[0], lr_voxel[2])This is not an extra swap applied after the inverse affine. It is simply the
correct way to read the LR voxel result, because LR voxel order is already
(z, y, x).
The preprocessing mask-generation helpers use a small internal convention chain that is worth writing down explicitly.
For both HR and LR mask generation:
- sliced surface contours in world space are represented as
(x, z) - after mapping through the inverse affine, image-space contour arrays are kept as
(z, x) - before exporting GeoJSON, those contour arrays are converted to polygon coordinates in
(x, z) - bbox JSON files are saved in full-image coordinates as
(x_min, x_max, z_min, z_max)
This matches the practical distinction between:
- image indexing work, which is easier in
(z, x) - geometric polygon export, which is easier in
(x, z)
The HR and LR raw rasters are vertically opposite in world z.
In practice:
- HR raw row
0corresponds to high worldz - HR raw row
H - 1corresponds to low worldz - LR raw row
0corresponds to low worldz - LR raw row
H - 1corresponds to high worldz
So the two raw images can appear vertically inverted with respect to one another even when both are geometrically correct.
For the current LR mask-generation workflow, there is an intentional difference between the saved bbox frame and the saved crop/GeoJSON frame.
The workflow is:
- compute the bbox in the original full-image LR coordinate system
- use that bbox directly to crop the raw LR image array
- keep the saved bbox JSON unchanged in that original LR full-image frame
- flip the cropped LR image vertically for export and viewing
- shift the contour coordinates into crop-local coordinates and flip them in the same way before GeoJSON export
So, for LR outputs:
- bbox JSON is unflipped and remains in full-image LR coordinates
- exported crop PNG is flipped vertically
- exported GeoJSON is crop-local and flipped to match the exported crop PNG
This difference is intentional. The bbox is meant for re-opening the raw LR image, while the exported crop and contours are meant for direct visual use.
matplotlib.pyplot.imshow() does not use the affine.
It only displays array rows and columns. Therefore, a plot may look correct or flipped simply because of raw raster orientation, not because the affine has been applied.
Any visual comparison between HR and LR should keep this in mind.
Assume the input point is expressed in full-image HR pixel coordinates as:
(x_hr, z_hr)Then the canonical mapping is:
hr_voxel = np.array([x_hr, 0, z_hr], dtype=float)
world = apply_affine(hr_affine, hr_voxel)
lr_voxel = apply_affine(np.linalg.inv(lr_affine), world)The LR 2D point in raw array coordinates is then:
z_lr = lr_voxel[0]
x_lr = lr_voxel[2]or equivalently:
lr_point_zx = np.array([lr_voxel[0], lr_voxel[2]])If the starting point comes from the raw HR image array, it is first known as:
(z_hr, x_hr)To map it correctly:
hr_voxel = np.array([x_hr, 0, z_hr], dtype=float)
world = apply_affine(hr_affine, hr_voxel)
lr_voxel = apply_affine(np.linalg.inv(lr_affine), world)
lr_point_zx = np.array([lr_voxel[0], lr_voxel[2]])This is the simplest correct rule when both source and destination are raw image arrays.
If the HR image was vertically flipped before selecting a point, only the z
coordinate must be undone before the affine step.
Let H_hr be the HR image height.
If a point is selected on the flipped display as:
(z_display, x_display)then the raw HR array coordinate is:
z_raw = H_hr - 1 - z_display
x_raw = x_displayAfter that, use the standard rule:
hr_voxel = np.array([x_raw, 0, z_raw], dtype=float)Only z is unflipped. x is unchanged.
The following patterns are not good general rules:
- flipping both HR axes before applying the affine
- feeding
(z, y, x)directly into the HR affine - assuming the LR inverse affine returns
(x, y, z) - mixing array coordinates
(z, x)with geometric coordinates(x, z)without an explicit reorder
If a complex transform seems necessary, it is usually a sign that array order and affine voxel order have been mixed together.
When mapping a point from HR world space into LR voxel space, the returned LR
voxel may contain a y value that does not look intuitive for single-slice work.
For matched 2D slice mapping, this is usually not the quantity of interest.
The useful outputs are the LR z and x indices on the known slice.
Therefore, for 2D HR/LR correspondence:
- use the affine to transform through world space
- keep the returned LR
zandx - do not over-interpret the LR
yvalue unless true 3D reasoning is needed
There is also a separate workflow-specific point for the current LR coronal
stack used by generate_masks_20um_from_volumes.py.
In that dataset, the LR affine does not carry the usable coronal slice position
in its translation term, so the slice y_world is reconstructed from the image
identifier instead:
y_world = -70.02 + int(image_id) * 0.02This is a dataset convention used for those LR mask-generation inputs, not a general property of LR affine usage.
Use this rule consistently:
- raw image arrays:
(z, x) - full-image geometric pixel coordinates:
(x, z) - affine/world interface: reorder explicitly when entering or leaving the affine
In practice:
- If a point comes from image indexing or clicking on a displayed image, think in
(z, x). - If a point is stored as a bbox corner, contour vertex, or full-image location, think in
(x, z). - Before applying the HR affine, always build
(x, y, z)explicitly. - After applying the inverse LR affine, read the 2D LR point as
(z, x). - If you are looking at exported LR crops, remember that the bbox stays in the unflipped full-image frame while the exported crop and GeoJSON are flipped.
A stable helper for 2D point mapping should declare its conventions explicitly.
Example contract:
Input: full-image HR pixel coordinates (x, z)
Output: raw LR array coordinates (z, x)or, for direct image-array use:
Input: raw HR array coordinates (z, x)
Output: raw LR array coordinates (z, x)The most important thing is not the exact contract, but that it is written down clearly and used consistently.
For point mapping, keep coordinates as floats as long as possible.
For bounding boxes:
- use
floorfor minima - use
ceilfor maxima
Avoid forcing integer conversion too early inside the core mapping step, since that can introduce off-by-one crop errors.
If a mapped point or crop looks wrong, check these questions in order:
- Is the source point in local crop coordinates or full-image coordinates?
- Is the source point currently
(z, x)or(x, z)? - Was the HR image flipped only for display?
- Was the HR affine fed with
(x, y, z)? - Was the LR result interpreted as
(z, y, x)before plotting or cropping? - Am I comparing an unflipped LR bbox with a flipped LR exported crop or GeoJSON?
- Is the apparent error a real mapping error, or only a vertical raster-orientation difference in display?
If the input comes from an HR image array, reorder once:
(z, x) -> (x, 0, z)Apply the HR affine, then the inverse LR affine, and read the LR result as:
(z, x) = (lr_voxel[0], lr_voxel[2])That is the simplest correct baseline for 2D HR/LR point mapping in the current data setup.