Touch targets in mobile apps are deceptively hard. Apple's HIG recommends 44×44pt, Material Design says 48dp, but real-world UIs demand irregular shapes: chat bubbles, freeform drawings, segmented image masks. When a user taps near—but not quite on—a small UI element, should the gesture register? The naive answer is "expand the bounding box," but that breaks down for non-rectangular shapes and overlapping elements. The robust answer comes from morphology: binary image operators borrowed from computer vision.

The Fat-Finger Problem in Non-Rectangular UIs

Consider a chat app where users tap to react to individual message bubbles. Bubbles have rounded corners, tail pointers, and variable widths. A tight bounding rectangle creates dead zones at corners; an expanded rectangle causes false positives when bubbles stack vertically. The correct hit area is the bubble's visual silhouette, dilated by a few pixels to forgive near-misses.

In production apps like SafeChat, where real-time WebRTC streams overlay interactive annotations, we maintain per-frame binary masks for each tappable region. A mask is a single-channel bitmap: 1 for pixels inside the shape, 0 outside. The challenge is expanding that mask uniformly in all directions—morphological dilation—without introducing jagged edges or per-pixel loops that kill frame rate.

Morphological Dilation: Definition and Naive Implementation

Dilation expands bright regions in a binary image by sliding a structuring element (SE)—typically a circle or square—across every pixel. For each position, if the SE overlaps any foreground pixel, the center pixel becomes foreground. Mathematically:

dst(x, y) = max { src(x + dx, y + dy) | (dx, dy) ∈ SE }

A 5×5 circular SE with radius 2px produces smooth, isotropic expansion. Naive code in Dart/Flutter:

Uint8List dilate(Uint8List src, int width, int height, int radius) {
  final dst = Uint8List(src.length);
  final r2 = radius * radius;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      bool hit = false;
      for (int dy = -radius; dy = 0 && nx < width && ny >= 0 && ny < height) {
            if (src[ny * width + nx] > 0) {
              hit = true;
              break;
            }
          }
        }
        if (hit) break;
      }
      dst[y * width + x] = hit ? 255 : 0;
    }
  }
  return dst;
}

For a 512×512 mask with radius=3, this loops 512² × (2×3+1)² ≈ 12.8M times. On a mid-range Android phone, that's 18ms—fine for one-time setup, catastrophic for per-frame gesture tracking at 120Hz (8.3ms budget).

Separable Approximation: Van Herk/Gil-Werman

Exact circular dilation is expensive because 2D convolution is O(N²M²) for N×N image and M×M SE. The trick: approximate with two 1D passes (horizontal then vertical) using a rectangular SE. This isn't a perfect circle, but for small radii (≤5px) the visual difference is negligible, and complexity drops to O(2NM).

Better still, the Van Herk/Gil-Werman algorithm computes 1D max-filter (dilation with a line SE) in O(N) per row, independent of radius. It precomputes forward and backward cumulative maxima in overlapping windows:

void dilateVHGW(Uint8List buf, int width, int height, int radius) {
  final temp = Uint8List(width * height);
  // Horizontal pass
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int maxVal = 0;
      for (int dx = -radius; dx