Repository: napari/napari

Issue:
Transform interaction box is 0.5 px offset
### 🐛 Bug Report

The overlay transform interaction box is offset from the image layer data by 0.5 px.
Top left:
<img width="1198" alt="image" src="https://github.com/napari/napari/assets/76622105/2ca54458-f401-4cfb-b0ad-80320eded103">
bottom right:
<img width="1198" alt="image" src="https://github.com/napari/napari/assets/76622105/6772cda9-c876-42be-b651-beb99d04c3e7">


### 💡 Steps to Reproduce

1. Open any napari sample image in napari
2. Press `2` to activate Transform mode
3. Zoom in to a corner to see that the box doesn't match the edge of the data
4. `viewer.layers[0].bounding_box.visible = True` to see that the *bounding_box* is correct

### 💡 Expected Behavior

The transform interaction overlay should match the bounding box overlay.

### 🌎 Environment

napari: 0.5.0a2.dev543+gad78ee21a
Platform: macOS-13.3.1-arm64-arm-64bit
System: MacOS 13.3.1
Python: 3.11.7 | packaged by conda-forge | (main, Dec 23 2023, 14:38:07) [Clang 16.0.6 ]
Qt: 6.6.1
PyQt6: 
NumPy: 1.26.3
SciPy: 1.12.0
Dask: 2024.1.1
VisPy: 0.14.1
magicgui: 0.8.1
superqt: 0.6.1
in-n-out: 0.1.9
app-model: 0.2.4
npe2: 0.7.4

OpenGL:
- GL version: 2.1 Metal - 83.1
- MAX_TEXTURE_SIZE: 16384
- GL_MAX_3D_TEXTURE_SIZE: 2048

Screens:
- screen 1: resolution 1680x1050, scale 2.0

Settings path:
- /Users/piotrsobolewski/Library/Application Support/napari/napari-dev_a1eb8b76ba95fa16ad06e26097b46b8455dfbf0b/settings.yaml

### 💡 Additional Context

_No response_


Discussion:
So looking at the bounding box overlay, which is correct, I find:
https://github.com/napari/napari/blob/1ee84b3677f8bb6b6cdd4a97754022003043c180/napari/_vispy/overlays/bounding_box.py#L32-L36

I'm not sure where that 0.5 exactly comes from, but it's not present for the interaction box:
https://github.com/napari/napari/blob/1ee84b3677f8bb6b6cdd4a97754022003043c180/napari/layers/utils/interaction_box.py#L15-L60

Indeed adding 0.5 to the vertices does fix the issue (but breaks tests).

@brisvag any thoughts here?
Is this the center vs corner of a pixel issue again?
>  Is this the center vs corner of a pixel issue again?

it would appear to be the case, *but* I find it a bit weird — as I recall, we achieved that by offsetting the *pixels* by *-0.5*, so that their centers rather than corners would be at (0, 0). So this looks to me like the interaction box is offset by an extra -0.5? (Rather than not be offset.) That is, the pixel corner *should* be at (-0.5, -0.5), so the interaction box is actually at (-1, -1).

What do the pixel coordinates say on the status bar as you hover on that region @psobolewskiPhD? Does this layer have any scale or translate set on it?

(aka please provide a script to reproduce this 😂)
Open any napari sample image using File menu, e.g. astronaut
No scale or transforms needed.
The top-left handle of the interaction box shows [-1, -1] in the status bar.
For astronaut, the bottom right shows [511, 511], same as the bottom-right pixel, but the handle is clearly inside the pixel and not at the edge.

Resolving patch:
diff --git a/napari/_vispy/layers/base.py b/napari/_vispy/layers/base.py
--- a/napari/_vispy/layers/base.py
+++ b/napari/_vispy/layers/base.py
@@ -214,6 +214,8 @@ def _on_matrix_change(self):
         affine_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix
         affine_matrix[-1, : len(translate)] = translate
 
+        offset = np.zeros(len(self.layer._slice_input.displayed))
+
         if self._array_like and self.layer._slice_input.ndisplay == 2:
             # Perform pixel offset to shift origin from top left corner
             # of pixel to center of pixel.
@@ -244,7 +246,7 @@ def _on_matrix_change(self):
         translate_child = (
             self.layer.translate[dims_displayed]
             + self.layer.affine.translate[dims_displayed]
-        )[::-1]
+        )[::-1] - offset[::-1]
         trans_rotate = simplified_transform.rotate[
             np.ix_(dims_displayed, dims_displayed)
         ]
diff --git a/napari/_vispy/overlays/bounding_box.py b/napari/_vispy/overlays/bounding_box.py
--- a/napari/_vispy/overlays/bounding_box.py
+++ b/napari/_vispy/overlays/bounding_box.py
@@ -29,11 +29,6 @@ def _on_bounds_change(self):
         if len(bounds) == 2:
             # 2d layers are assumed to be at 0 in the 3rd dimension
             bounds = np.pad(bounds, ((1, 0), (0, 0)))
-        if self.layer._array_like and self.layer._slice_input.ndisplay == 2:
-            # array-like layers (images) are offset by 0.5 in 2d.
-            # This is not needed in 3D because vispy's VolumeVisual
-            # is already centered on voxels
-            bounds += 0.5
 
         self.node.set_bounds(bounds[::-1])  # invert for vispy
         self._on_lines_change()


Test patch: