///|
/// Shared 3x3 morphology kernel: per-channel neighborhood minimum (erode) or
/// maximum (dilate) with clamp-to-edge sampling. Alpha is preserved.
fn Image::morph3(self : Image, take_max : Bool) -> Image {
  let out = Image::new(self.width, self.height)
  for y in 0.. best {
                best = v
              }
            } else if v < best {
              best = v
            }
          }
        }
        out.data[base + c] = clamp_byte(best)
      }
      out.data[base + 3] = self.data[base + 3]
    }
  }
  out
}

///|
/// Dilation: each channel takes the 3x3 neighborhood maximum, growing bright
/// regions by one pixel per application.
pub fn Image::dilate(self : Image) -> Image {
  self.morph3(true)
}

///|
/// Erosion: each channel takes the 3x3 neighborhood minimum, shrinking bright
/// regions by one pixel per application.
pub fn Image::erode(self : Image) -> Image {
  self.morph3(false)
}

///|
/// Morphological opening (erode then dilate): removes bright specks smaller
/// than the 3x3 element while roughly preserving larger shapes.
pub fn Image::morph_open(self : Image) -> Image {
  self.erode().dilate()
}

///|
/// Morphological closing (dilate then erode): fills dark pinholes smaller
/// than the 3x3 element while roughly preserving larger shapes.
pub fn Image::morph_close(self : Image) -> Image {
  self.dilate().erode()
}