///|
pub fn edge_map(
  image : GrayImage,
  low : Double,
  high : Double,
) -> EdgeMapSummary {
  let magnitude = gradient_magnitude(image)
  let edges = hysteresis_edges(image, low~, high~)
  let strengths : Array[Double] = []
  for y in 0.. EdgeMapSummary {
  let magnitude = gradient_magnitude(image)
  let edges = image_to_mask(magnitude, threshold)
  let strengths : Array[Double] = []
  for y in 0.. Array[Component] {
  label_components(summary.edges)
}

///|
pub fn edge_map_largest(summary : EdgeMapSummary) -> Component {
  largest_component(summary.edges)
}

///|
pub fn edge_map_skeleton(summary : EdgeMapSummary) -> BinaryMask {
  let result = summary.edges.clone()
  let changed = true
  let mut iteration = 0
  while changed && iteration < 20 {
    iteration += 1
    let mut changed_now = false
    for y in 1..<(result.height - 1) {
      for x in 1..<(result.width - 1) {
        if result.at(x, y) {
          let neighbors = count_true_neighbors(result, x, y, 1)
          if neighbors >= 6 {
            result.set(x, y, false)
            changed_now = true
          }
        }
      }
    }
    if !changed_now {
      break
    }
  }
  result
}

///|
pub fn count_true_neighbors(
  mask : BinaryMask,
  x : Int,
  y : Int,
  radius : Int,
) -> Int {
  let mut result = 0
  let safe = radius.max(0)
  for dy in -safe..<=safe {
    for dx in -safe..<=safe {
      if (dx != 0 || dy != 0) && mask.at(x + dx, y + dy) {
        result += 1
      }
    }
  }
  result
}

///|
pub fn edge_map_thin(summary : EdgeMapSummary) -> EdgeMapSummary {
  let skeleton = edge_map_skeleton(summary)
  { ..summary, edges: skeleton, count: skeleton.count() }
}

///|
pub fn edge_map_dilate(
  summary : EdgeMapSummary,
  radius : Int,
) -> EdgeMapSummary {
  let edges = summary.edges.dilate(radius)
  { ..summary, edges, count: edges.count() }
}

///|
pub fn edge_map_close(summary : EdgeMapSummary, radius : Int) -> EdgeMapSummary {
  let edges = summary.edges.closing(radius)
  { ..summary, edges, count: edges.count() }
}

///|
pub fn edge_map_strengths(
  summary : EdgeMapSummary,
  magnitude : GrayImage,
) -> Array[Double] {
  let result : Array[Double] = []
  for y in 0.. Double {
  clamp_double(summary.density * summary.mean_strength, 0.0, 1.0)
}

///|
pub fn edge_map_report(summary : EdgeMapSummary) -> String {
  let out = StringBuilder()
  out.write_string("count=")
  out.write_string(summary.count.to_string())
  out.write_string("; density=")
  out.write_string(summary.density.to_string())
  out.write_string("; mean_strength=")
  out.write_string(summary.mean_strength.to_string())
  out.to_string()
}

///|
pub fn edge_points(summary : EdgeMapSummary) -> Array[EdgePoint] {
  let result : Array[EdgePoint] = []
  for y in 0.. Line {
  fit_line(edge_points(summary).map(fn(edge) { edge.point }))
}

///|
pub fn edge_map_circle_fit(summary : EdgeMapSummary) -> Circle {
  let points = edge_points(summary)
  if points.length() < 3 {
    { center: Point::Point(x=0.0, y=0.0), radius: 0.0, rms_error: 0.0 }
  } else {
    let selected : Array[Point] = [
      points[0].point,
      points[1].point,
      points[2].point,
    ]
    detect_circle_from_points(selected)
  }
}

///|
pub fn edge_map_overlay(
  image : GrayImage,
  summary : EdgeMapSummary,
  value : Double,
) -> GrayImage {
  let result = image.copy()
  for y in 0..