mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

一种基于原始数据的图像显著性区域识别方法 #15

Open mengtuifrontend opened 5 years ago

mengtuifrontend commented 5 years ago

一种基于原始数据的图像显著性区域识别方法

最近在做一个热力图图像中显著性区域识别的需求,比如有如下一个图像:

原始图像

原始图像

需要识别出图像中高亮较集中的区域,即高亮集中的区域被认为是图像中的显著性区域,效果如下:

带范围选框的图像

带范围选框的图像

在没有原始数据情况下,需要对原始图像中的图像数据进行分析,然后可以通过射线法等方法得到显著性区域范围。

而在拥有原始数据的情况下,我们可以通过简单的操作原始数据获取图像显著性区域,十分方便。

这里通过一个例子,来介绍一下基于原始数据的图像显著性区域识别方法。

准备工作

首先准备了一个页面用于渲染图像,内容如下:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style>
    .box {width: 500px; height: 500px; border: 1px solid #bbb; position: relative;}
    .item {width: 10px; height: 10px; border-radius: 5px; position: absolute; background: rgba(0, 0, 0, 0.4); transform: translate(-50%, -50%)}
    .line {border: 1px solid red; position: absolute;}
  </style>
</head>
<body>
  <div class="box"></div>
  <script></script>
</body>
</html>

代码中预定义了样式,可以看出,box 元素将会是图像的"画布",item 元素是画布中的点,而 line 元素是包裹范围的选框。

原始数据

本文提到的原始数据是指含有 x, y 坐标信息的数据集合,并且该数据集合被用于绘制图像,如:

var data = [
  {x: 20, y: 31},
  {x: 104, y: 99},
  ...
]

这里通过随机数生成一个原始数据:

var items = [], max = 500, len = 1000, i = 0

// 生成 1000 个坐标点,范围 [0 - 500)
for (i; i < len; i++) {
  items.push([Math.floor(Math.random() * max), Math.floor(Math.random() * max)])
}

得到数据后,我们可以先在画布上绘制点。

var html = ''

items.map(function (item) {
  html += '<div class="item" style="left: ' + item[0] + 'px; top: ' + item[1] + 'px"></div>'
})
document.querySelector('.box').innerHTML = html

效果如图:

原始图像

识别算法分析

我们需要识别的范围,简单的说就是多个连续的数据点的集合所形成的范围。所以第一步,我们需要通过一个点来寻找它所在的连续数据点集合。

连续数据点集合的获取方法描述如下:

1 设定一个点作为起始点; 2 遍历数据寻找与起始点“连续”的点,即两点距离小于某一阈值; 3 对获取到的“连续”点集合进行遍历重复 1 - 3 步骤直到全部遍历完成。

完成一次连续数据点集合的获取即代表着获取到了一个数据范围。但是该数据范围是否“显著”,这个是需要判断的,例子中将仅以数据点的个数作为判断依据;实际应用中判断显著性应该还需要结合图像中的其他权重值,比如热力图中每个点的点击量值。

我们先定义一下获取两个点距离的方法:

// 十分熟悉的勾股定理
function getDistance (p1, p2) {
  return Math.sqrt(Math.pow(p2[1] - p1[1], 2) + Math.pow(p2[0] - p1[0], 2))
}

获取连续数据点的方法定义如下:

/**
 * infect 通过一个起始点获取一个连续数据点集合
 * @params {Array} startPoint 起始点
 * @params {Array} restPoints 剩余待计算点的集合
 * @params {Array} range 已计算所得的连续数据点集合
 * @return {Array} 连续数据点集合
 */
function infect (startPoint, restPoints, range) {
  var i = 0, temp = []

  //  还有剩余待计算点时,对下一个待计算点进行连续判断(这里仅以距离作为判断依据),如果是连续点,则将该点加入数据集合并从剩余待计算点集合中剔除。
  while (restPoints[i]) {
    if (getDistance(startPoint, restPoints[i]) <= 5 * 2) {// 距离小于两圆半径之和表示“连续”
      if (range.indexOf(restPoints[i]) === -1) {
        temp.push(restPoints[i])
        restPoints.splice(i, 1)
        i--
      }
    }
    i++
  }

  range = range.concat(temp)

  // 上面一次遍历仅获得了起始点的连续点,接下来计算起始点的连续点的连续点,依此类推,直到没有剩余待计算点为止
  i = 0
  while (temp[i] && restPoints.length > 0) {
    range = range.concat(infect(temp[i], restPoints, [temp[i]]))
    i++
  }

  return range
}

代码中通过对起始点的连续点中的每一个点进行连续点求值,获得最终一个范围内的所有数据点。

接下来,我们通过 infect 函数获得所有范围集合。

function findRangeAndDraw () {
  var startPoint = null, i = 0, ranges = [], rest = items.slice(0)

  // 获取到一个范围数据点集合后,如果还有剩余待计算点,继续进行范围获取。
  while (rest.length) {
    startPoint = rest.shift()
    ranges.push(infect(startPoint, rest, [startPoint]))
  }

  // 绘制所有范围
  draw(ranges)
}

在最后的绘制方法中,需要对范围内的所有数据点进行遍历以确定范围的左上角顶点和长宽,代码如下:

function draw (ranges) {
  var i = 0, points = ranges.shift(), maxX = 0, maxY = 0, minX = max, minY = max, width = 0, height = 0

  if (points.length === 1) {
    // 如果范围内仅一个点,继续绘制
    if (ranges.length) {
      draw(ranges)
      return
    }
  } else {
    // 获取左上角顶点和右下角顶点坐标
    points.map(function (point) {
      maxX = Math.max(point[0], maxX)
      maxY = Math.max(point[1], maxY)
      minX = Math.min(point[0], minX)
      minY = Math.min(point[1], minY)
    })

    // 计算获取高宽,这里的数值 5 每个点的半径,见 CSS 中的属性
    minX = minX - 5
    minY = minY - 5
    width = maxX - minX + 5
    height = maxY - minY + 5
  }

  document.querySelector('.box').insertAdjacentHTML('afterBegin', '<div class="line" style="left: ' + minX + 'px; top: ' + minY + 'px; width: ' + width + 'px; height: ' + height + 'px;"></div>')

  if (ranges.length) {
    draw(ranges)
  }
}

所有代码完成,最后执行 findRangeAndDraw 方法,运行后效果如下:

[!ranges.jpg]

范围图像

上述方法通过操作原始数据获取图像的显著性区域,还有很多方法可以不依靠原始数据获取图像显著性区域。

这里简单介绍两种方法。

1. 射线法

射线法是不依靠原始数据而能得到显著性区域范围的一种方法。

它的获取范围的步骤是:

1 设定显著性区域的阈值并通过图像获得图像的像素数据; 2 遍历图像像素数据,获得显著点的集合; 3 遍历显著点集合,对每一个显著点, 3.1 依序发射 4 条直线,每条直线成45度夹角,最终形成一个“米”字; 3.2 依序遍历这 4 条直线,判断距离显著点最近的该线上背景色点坐标; 3.3 遍历完成 4 条线,获得一个具有 8 个点的集合; 3.4 确定该显著点的范围,并在下次遍历中跳过该范围内的所有点的计算; 4 重复步骤 3 中的所有小步骤直到没有显著点可计算; 5 绘制所有范围。

2. 区块统计法

区块统计法是通过对图像进行 N 等分后获得 N 个区块,然后判断相邻区块是否含有显著性区块,最终获得显著性区域的一种方法。同样,该方法也不依靠原始数据。

它的步骤如下:

1 获得图像的像素数据; 2 将图像划分为 N 个区块,如以 5 * 5 的像素范围为一个区块; 3 遍历区块,判断在区块范围内的像素数据是否含有显著性像素,是则将区域标记为 1,否则为 0; 4 对标记为 1的区块进行遍历,并对相邻的标记为 1 的区块进行合并为一个范围; 5 绘制所有范围;

总结

通过操作图像的原始数据,可以获取到图像的显著性区域范围。而当没有原始数据时,我们也可以通过分析图像的像素数据获得范围。

目前,分析图像的显著性区域技术使用的非常广阔。比如常见的二维码识别、条形码识别、商品识别、色情图像识别等等,当然每一个具体的场景对应的算法都会有所不同,希望本文能够对大家能够深入其中带来一点帮助。