///|
pub fn Roi::from_bounds(id : String, bounds : Bounds, image : GrayImage) -> Roi {
  let mask = BinaryMask(width=image.width, height=image.height)
  for y in bounds.min_y..<(bounds.max_y + 1) {
    for x in bounds.min_x..<(bounds.max_x + 1) {
      mask.set(x, y, true)
    }
  }
  { id, bounds, mask }
}

///|
pub fn Roi::from_mask(id : String, mask : BinaryMask) -> Roi {
  { id, bounds: mask.bounds(), mask }
}

///|
pub fn Roi::crop(roi : Roi, image : GrayImage) -> GrayImage {
  crop_image(image, roi.bounds)
}

///|
pub fn Roi::masked_image(
  roi : Roi,
  image : GrayImage,
  background : Double,
) -> GrayImage {
  let result = GrayImage(
    width=image.width,
    height=image.height,
    fill=background,
  )
  for y in 0.. Array[Double] {
  let result : Array[Double] = []
  for y in roi.bounds.min_y..<(roi.bounds.max_y + 1) {
    for x in roi.bounds.min_x..<(roi.bounds.max_x + 1) {
      if roi.mask.at(x, y) {
        result.push(image.at(x, y))
      }
    }
  }
  result
}

///|
pub fn Roi::statistics(roi : Roi, image : GrayImage) -> StatsSummary {
  describe_samples(roi.values(image))
}

///|
pub fn roi_grid(
  image : GrayImage,
  columns : Int,
  rows : Int,
  margin : Int,
) -> Array[Roi] {
  let result : Array[Roi] = []
  let safe_columns = columns.max(1)
  let safe_rows = rows.max(1)
  let cell_width = image.width / safe_columns
  let cell_height = image.height / safe_rows
  for row in 0.. Double {
  let values = roi.values(image)
  maximum_value(values) - minimum_value(values)
}

///|
pub fn roi_edge_density(
  roi : Roi,
  image : GrayImage,
  threshold : Double,
) -> Double {
  let edges = threshold_edges_in_roi(roi, image, threshold)
  let area = roi.mask.count()
  if area == 0 {
    0.0
  } else {
    Double::from_int(edges.count()) / Double::from_int(area)
  }
}

///|
pub fn threshold_edges_in_roi(
  roi : Roi,
  image : GrayImage,
  threshold : Double,
) -> BinaryMask {
  let edges = image_to_mask(gradient_magnitude(image), threshold)
  edges.intersect(roi.mask)
}

///|
pub fn roi_components(
  roi : Roi,
  image : GrayImage,
  threshold : Double,
) -> Array[Component] {
  label_components(
    threshold_image(roi.masked_image(image, 0.0), threshold).intersect(roi.mask),
  )
}

///|
pub fn roi_report(roi : Roi, image : GrayImage) -> String {
  let summary = roi.statistics(image)
  let out = StringBuilder()
  out.write_string(roi.id)
  out.write_string(" mean=")
  out.write_string(summary.mean.to_string())
  out.write_string(" stddev=")
  out.write_string(summary.standard_deviation.to_string())
  out.write_string(" count=")
  out.write_string(summary.count.to_string())
  out.to_string()
}

///|
pub fn combine_rois(rois : Array[Roi], width : Int, height : Int) -> BinaryMask {
  let result = BinaryMask(width~, height~)
  for roi in rois {
    for y in 0.. Bool {
  for i in 0.. Point {
  let component = largest_component(roi.mask)
  component.centroid
}

///|
pub fn expand_roi(roi : Roi, padding : Int, image : GrayImage) -> Roi {
  let margin = padding.max(0)
  Roi::from_bounds(
    roi.id,
    {
      min_x: roi.bounds.min_x - margin,
      min_y: roi.bounds.min_y - margin,
      max_x: roi.bounds.max_x + margin,
      max_y: roi.bounds.max_y + margin,
    },
    image,
  )
}

///|
pub fn shrink_roi(roi : Roi, padding : Int, image : GrayImage) -> Roi {
  let margin = padding.max(0)
  Roi::from_bounds(
    roi.id,
    {
      min_x: roi.bounds.min_x + margin,
      min_y: roi.bounds.min_y + margin,
      max_x: roi.bounds.max_x - margin,
      max_y: roi.bounds.max_y - margin,
    },
    image,
  )
}