一种快速填充轮廓的方法,能够导出为类似多边形的格式。

问题描述 投票:1回答:1

我有一些医学图片呈现在画布上,如下图为例。

knee

我想做一个工具,让你用工具以可展开的圆圈的形式选择图片的任何区域,只填充不超出原点击像素所在的轮廓的部分。填充后的轮廓是在单独的画布层上绘制的。

现在我使用最常用的 迭代堆栈实施 溢流填充的可变公差(比较函数)。你可以在这里熟悉它. 一切都不是很好,特别是在没有强烈对比差异的图片和高分辨率的图片中,其他一切都很慢。

flood fill result

我的想法是创建一个 国家容器 并寻找那里是否存在所需填充的轮廓,如果存在,则只需替换画布像素数组(不过,我又要求助于一些额外的处理,画布像素数组包含4个通道,而在算法的输出处只得到1个通道,只替换内容是不行的,你需要将每个像素替换成一个分成4个通道的像素),而不是每次都慢慢的泛滥填充。但是这种方法有一个很大的问题。占用内存. 正如你所猜测的那样,一个填充的轮廓,特别是一个体面的分辨率,可能会占用相当大的空间,它们的集合成为一个真正的内存消耗问题。

我们决定将完成的轮廓以多边形的形式存储,并从容器中提取它们,只需用更快速的 语境填充. 使用的算法可以让我输出一组边界,但是由于算法的特点,这个数组是无序的,按照这个顺序连接顶点,我们只能得到一个部分填充的轮廓(右图)。有没有一种方法可以将它们排序,让我只能将它们连接起来,得到一个封闭的路径(左图中填充轮廓中的孔洞应该不是先验的,所以我们不用担心)?

flood fillresulted path based on boundaries

综上所述,由于填充工作不是很好,我认为要使用不同的算法实现,但我不知道是哪一种。下面是我的一些想法。

  1. 使用不同的实现方法,例如,线扫描法。据我所知。这里是该算法在开源软件中最快最有效的实现之一。.优点:可能的效率和速度.缺点:我需要以某种方式将结果转换为多边形,重写算法为javascript(可能是)。稿件,可以做得很好,但无论如何,我将不得不重写相当一部分代码)。)

  2. 使用完全不同的方法。

    a)我不知道,但也许Canny检测器可以对提取多边形有用。但就程序在客户端的使用意义而言,提取所有的边界将是无利可图的,必须想办法只处理必要的部分,而不是整个图片。

    b)然后在知道边界的情况下,使用任何足够快的填充算法,根本不会超出找到的边界。

我很乐意知道一些其他的方法,如果能看到javascript中现成的实现就更好了。

UPD。

为了更好的理解,下面介绍工具光标和算法的预期结果。

tool cursorexpected result

javascript algorithm html5-canvas polygon flood-fill
1个回答
2
投票

下面是一个用opencv的例子

下面的工作应该或最终使用 琴键 提供的代码片段

感兴趣的是:approxPolyDP,它可能会满足您的需求(请检查 Ramer-Douglas-Peucker算法。)

// USE FIDDLE
// https://jsfiddle.net/c7xrq1uy/

async function loadSomeImage() {
  const ctx = document.querySelector('#imageSrc').getContext('2d')
  ctx.fillStyle = 'black'
  const img = new Image()
  img.crossOrigin = ''
  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'

  img.onload = () => {
    const imgwidth = img.offsetWidth
    const imgheight = img.offsetHeight
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
  }
}

function plotPoints(canvas, points, color = 'green', hold = false){
  const ctx = canvas.getContext('2d')
  !hold && ctx.clearRect(0, 0, 400, 400)
  ctx.strokeStyle = color

  Object.values(points).forEach(ps => {
    ctx.beginPath()
    ctx.moveTo(ps[0].x, ps[0].y)
    ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
    ctx.closePath()
    ctx.stroke()
  })
}
const binarize = (src, threshold) => {
  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
  const dst = new cv.Mat()
  src.convertTo(dst, cv.CV_8U)
  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
  cv.imshow('binary', dst)
  return dst
}
const flip = src => {
  const dst = new cv.Mat()
  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
  cv.imshow('flip', dst)
  return dst
}
const dilate = (src) => {
  const dst = new cv.Mat()
  let M = cv.Mat.ones(3, 3, cv.CV_8U)
  let anchor = new cv.Point(-1, -1)
  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
  M.delete()
  cv.imshow('dilate', dst)
  return dst
}
const PARAMS = {
  threshold: 102,
  anchor: { x: 180, y: 180 },
  eps: 1e-2
}
const dumpParams = ({ threshold, anchor, eps }) => {
  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
}
document.querySelector('input[type=range]').onmouseup = e => {
  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
  dumpParams(PARAMS)
  runCv(PARAMS)
}
document.querySelector('input[type=value]').onchange = e => {
  PARAMS.eps = parseFloat(e.target.value)
  dumpParams(PARAMS)
  runCv(PARAMS)
}
document.querySelector('#imageSrc').onclick = e => {
  const rect = e.target.getBoundingClientRect()
  PARAMS.anchor = {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top
  }
  dumpParams(PARAMS)
  runCv(PARAMS)
}
const contourToPoints = cnt => {
  const arr = []
  for (let j = 0; j < cnt.data32S.length; j += 2){
    let p = {}
    p.x = cnt.data32S[j]
    p.y = cnt.data32S[j+1]
    arr.push(p)
  }
  return arr
}
loadSomeImage()
dumpParams(PARAMS)
let CVREADY
const cvReady = new Promise((resolve, reject) => CVREADY = resolve)

const runCv = async ({ threshold, anchor, eps }) => {
  await cvReady
  const canvasFinal = document.querySelector('#final')
  const mat = cv.imread(document.querySelector('#imageSrc'))
  const binaryImg = binarize(mat, threshold, 'binary')
  const blurredImg = dilate(binaryImg)
  const flipImg = flip(blurredImg)
  var contours = new cv.MatVector()
  const hierarchy = new cv.Mat
  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

  const points = {}
  let matchingPoints = null
  let matchingContour = null
  for (let i = 0; i < contours.size(); ++i) {
  let minArea = 1e40
    const ci = contours.get(i)
    points[i] = contourToPoints(ci)
    if (anchor) {
      const point = new cv.Point(anchor.x, anchor.y)
      const inside = cv.pointPolygonTest(ci, point, false) >= 1
      const area = cv.contourArea(ci)
      if (inside && area < minArea) {
        matchingPoints = points[i]
        matchingContour = ci
        minArea = area
      }
    }
  }
  plotPoints(canvasFinal, points)

  if (anchor) {
    if (matchingPoints) {
      plotPoints(canvasFinal, [matchingPoints], 'red', true)
      if (eps) {
        const epsilon = eps * cv.arcLength(matchingContour, true)
        const approx = new cv.Mat()
        cv.approxPolyDP(matchingContour, approx, epsilon, true)
        const arr = contourToPoints(approx)
        console.log('polygon', arr)
        plotPoints(canvasFinal, [arr], 'blue', true)
      }
    }
  }
  mat.delete()
  contours.delete()
  hierarchy.delete()
  binaryImg.delete()
  blurredImg.delete()
  flipImg.delete()
}
function onOpenCvReady() {
  cv['onRuntimeInitialized'] = () => {console.log('cvready'); CVREADY(); runCv(PARAMS)}
}
// just so we can load async script
var script = document.createElement('script');
script.onload = onOpenCvReady
script.src = 'https://docs.opencv.org/master/opencv.js';
document.head.appendChild(script)
canvas{border: 1px solid black;}
  .debug{width: 200px; height: 200px;}
binarization threeshold<input type="range" min="0" max="100"/><br/>
eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
params: <span id="params"></span><br/>
<br/>
<canvas id="imageSrc" height="400" width="400"/></canvas>
<canvas id="final" height="400" width="400"></canvas>
<br/>
<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>

ps:多边形在控制台中输出


掩饰

编辑:在下面的片段中,我有更多的乐趣,并实现了面具。我们可能会使片段[完整的页面],然后悬停在第一个画布上。

// USE FIDDLE
// https://jsfiddle.net/c7xrq1uy/

async function loadSomeImage() {
  const ctx = document.querySelector('#imageSrc').getContext('2d')
  ctx.fillStyle = 'black'
  const img = new Image()
  img.crossOrigin = ''
  img.src = 'https://cors-anywhere.herokuapp.com/https://i.stack.imgur.com/aiZ7z.png'

  img.onload = () => {
    const imgwidth = img.offsetWidth
    const imgheight = img.offsetHeight
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 400) 
  }
}

function plotPoints(canvas, points, color = 'green', hold = false){
  const ctx = canvas.getContext('2d')
  !hold && ctx.clearRect(0, 0, 400, 400)
  ctx.strokeStyle = color

  Object.values(points).forEach(ps => {
    ctx.beginPath()
    ctx.moveTo(ps[0].x, ps[0].y)
    ps.slice(1).forEach(({ x, y }) => ctx.lineTo(x,y))
    ctx.closePath()
    ctx.stroke()
  })
}
const binarize = (src, threshold) => {
  cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0)
  const dst = new cv.Mat()
  src.convertTo(dst, cv.CV_8U)
  cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV)
  cv.imshow('binary', dst)
  return dst
}
const flip = src => {
  const dst = new cv.Mat()
  cv.threshold(src, dst, 128, 255, cv.THRESH_BINARY_INV)
  cv.imshow('flip', dst)
  return dst
}
const dilate = (src) => {
  const dst = new cv.Mat()
  let M = cv.Mat.ones(3, 3, cv.CV_8U)
  let anchor = new cv.Point(-1, -1)
  cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue())
  M.delete()
  cv.imshow('dilate', dst)
  return dst
}
const PARAMS = {
  threshold: 102,
  anchor: { x: 180, y: 180 },
  eps: 1e-2,
  radius: 50
}
const dumpParams = ({ threshold, anchor, eps }) => {
  document.querySelector('#params').innerHTML = `thres=${threshold} (x,y)=(${anchor.x}, ${anchor.y}) eps:${eps}`
}
document.querySelector('input[type=range]').onmouseup = e => {
  PARAMS.threshold = Math.round(parseInt(e.target.value, 10) / 100 * 255)
  dumpParams(PARAMS)
  runCv(PARAMS)
}
document.querySelector('input[type=value]').onchange = e => {
  PARAMS.eps = parseFloat(e.target.value)
  dumpParams(PARAMS)
  runCv(PARAMS)
}
document.querySelector('#imageSrc').onclick = e => {
  const rect = e.target.getBoundingClientRect()
  PARAMS.anchor = {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top
  }
  dumpParams(PARAMS)
  runCv(PARAMS)
}
// sorry for the globals, keep code simple
let DST = null
let MATCHING_CONTOUR = null
let DEBOUNCE = 0
document.querySelector('#imageSrc').onmousemove = e => {
  if (Date.now() - DEBOUNCE < 100) return
  if (!MATCHING_CONTOUR || !DST) { return }
  const rect = e.target.getBoundingClientRect()
  DEBOUNCE = Date.now()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  const dst = DST.clone()
  plotIntersectingMask(dst, MATCHING_CONTOUR, { anchor: { x, y }, radius: PARAMS.radius })
  dst.delete()
}
const contourToPoints = cnt => {
  const arr = []
  for (let j = 0; j < cnt.data32S.length; j += 2){
    let p = {}
    p.x = cnt.data32S[j]
    p.y = cnt.data32S[j+1]
    arr.push(p)
  }
  return arr
}
const plotIntersectingMask = (dst, cnt, { anchor, radius }) => {
  const { width, height } = dst.size()
  
  const contourMask = new cv.Mat.zeros(height, width, dst.type())
  const matVec = new cv.MatVector()
  matVec.push_back(cnt)
  cv.fillPoly(contourMask, matVec, [255, 255, 255, 255])

  const userCircle = new cv.Mat.zeros(height, width, dst.type())
  cv.circle(userCircle, new cv.Point(anchor.x, anchor.y), radius, [255, 128, 68, 255], -2)

  const commonMask = new cv.Mat.zeros(height, width, dst.type())
  cv.bitwise_and(contourMask, userCircle, commonMask)
  
  userCircle.copyTo(dst, commonMask)
  cv.imshow('final', dst)

  commonMask.delete()
  matVec.delete()
  contourMask.delete()
  userCircle.delete()
}
loadSomeImage()
dumpParams(PARAMS)
let CVREADY
const cvReady = new Promise((resolve, reject) => CVREADY = resolve)

const runCv = async ({ threshold, anchor, eps, radius }) => {
  await cvReady
  const canvasFinal = document.querySelector('#final')
  const mat = cv.imread(document.querySelector('#imageSrc'))
  const binaryImg = binarize(mat, threshold, 'binary')
  const blurredImg = dilate(binaryImg)
  const flipImg = flip(blurredImg)
  var contours = new cv.MatVector()
  const hierarchy = new cv.Mat
  cv.findContours(flipImg, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

  const points = {}
  let matchingPoints = null
  let matchingContour = null
  for (let i = 0; i < contours.size(); ++i) {
  let minArea = 1e40
    const ci = contours.get(i)
    points[i] = contourToPoints(ci)
    if (anchor) {
      const point = new cv.Point(anchor.x, anchor.y)
      const inside = cv.pointPolygonTest(ci, point, false) >= 1
      const area = cv.contourArea(ci)
      if (inside && area < minArea) {
        matchingPoints = points[i]
        matchingContour = ci
        minArea = area
      }
    }
  }
  plotPoints(canvasFinal, points)

  if (anchor) {
    if (matchingPoints) {
      MATCHING_CONTOUR = matchingContour
      plotPoints(canvasFinal, [matchingPoints], 'red', true)
      if (eps) {
        const epsilon = eps * cv.arcLength(matchingContour, true)
        const approx = new cv.Mat()
        cv.approxPolyDP(matchingContour, approx, epsilon, true)
        const arr = contourToPoints(approx)
        //console.log('polygon', arr)
        plotPoints(canvasFinal, [arr], 'blue', true)

        if (DST) DST.delete()
        DST = cv.imread(document.querySelector('#final'))
      }
    }
  }
  mat.delete()
  contours.delete()
  hierarchy.delete()
  binaryImg.delete()
  blurredImg.delete()
  flipImg.delete()
}
function onOpenCvReady() {
  cv['onRuntimeInitialized'] = () => {console.log('cvready'); CVREADY(); runCv(PARAMS)}
}
// just so we can load async script
var script = document.createElement('script');
script.onload = onOpenCvReady
script.src = 'https://docs.opencv.org/master/opencv.js';
document.head.appendChild(script)
  canvas{border: 1px solid black;}
  .debug{width: 200px; height: 200px;}
  #imageSrc{cursor: pointer;}
binarization threeshold<input type="range" min="0" max="100"/><br/>
eps(approxPolyDp) <input type="value" placeholder="0.01"/><br/>
params: <span id="params"></span><br/>
<br/>
<canvas id="imageSrc" height="400" width="400"/></canvas>
<canvas id="final" height="400" width="400"></canvas>
<br/>
<canvas class="debug" id="binary" height="400" width="400" title="binary"></canvas>
<canvas class="debug" id="dilate" height="400" width="400" title="dilate"></canvas>
<canvas class="debug" id="flip" height="400" width="400" title="flip"></canvas>
© www.soinside.com 2019 - 2024. All rights reserved.