CSS `Image Segmentation` 结果用于动态调整元素边界或内容排版

各位观众老爷,大家好!今天咱们来聊聊一个听起来很高大上,但其实挺接地气的玩意儿——CSS里的“Image Segmentation”结果,以及如何用它来让我们的网页元素跳起华尔兹,或者至少别再那么死板地站着。

一、啥是Image Segmentation?

Image Segmentation,图像分割,简单来说,就是把一张图片分成若干个有意义的区域,每个区域代表图片中的一个对象或者部分。这玩意儿在计算机视觉领域可是个老熟人了,比如自动驾驶要识别行人、车辆,医学影像要识别肿瘤啥的,都离不开它。

在Web开发里,我们用CSS直接做图像分割不太现实(毕竟CSS主要负责“长相”),但我们可以利用现有的图像分割模型(比如用JavaScript调用TensorFlow.js跑一个预训练模型,或者直接用后端API提供分割结果),拿到分割结果后,再用CSS来玩点花样。

二、拿到分割结果后,能干啥?

拿到分割结果后,我们手上就有了每个像素属于哪个区域的信息。有了这些信息,我们就可以:

  1. 动态调整元素边界: 让元素不再是规规矩矩的矩形,而是沿着分割出来的对象轮廓来显示,让页面更灵动。
  2. 内容排版: 将文本、图片等内容围绕分割出来的对象进行排版,避免遮挡重要区域,提高用户体验。
  3. 高级特效: 基于分割结果,实现一些高级的视觉特效,比如背景虚化、对象突出显示等。

三、实战演练:让元素边界跳舞

咱们先从最简单的“动态调整元素边界”开始。假设我们有一张图片,并且已经通过某种方式获得了分割结果,得到了一个JSON对象,里面包含了每个像素的类别信息。

// 假设的分割结果
{
  "width": 640,
  "height": 480,
  "segmentationMap": [
    // 这是一个二维数组,每个元素代表一个像素的类别
    // 比如 segmentationMap[0][0] = 1,表示 (0, 0) 这个像素属于类别 1
    // 这里的数组数据省略,真实数据会非常庞大
  ]
}

有了这个 segmentationMap,我们就可以用CSS的 clip-path 属性来定义元素的显示区域。clip-path 可以使用 polygon() 函数来定义多边形,我们需要根据分割结果,提取出对象轮廓的关键点,生成多边形坐标。

步骤1:提取轮廓

这是一个比较复杂的算法问题,简单来说,我们需要遍历 segmentationMap,找到属于目标对象的像素,然后沿着这些像素的边缘,提取出关键点。 这里简单展示一种思路,实际应用中需要更复杂的算法来平滑轮廓,减少锯齿。

function extractContour(segmentationMap, targetClass) {
  const width = segmentationMap[0].length;
  const height = segmentationMap.length;
  const contourPoints = [];

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      if (segmentationMap[y][x] === targetClass) {
        // 检查周围像素,判断是否是轮廓点
        let isContour = false;
        if (x === 0 || segmentationMap[y][x - 1] !== targetClass) isContour = true;
        if (x === width - 1 || segmentationMap[y][x + 1] !== targetClass) isContour = true;
        if (y === 0 || segmentationMap[y - 1][x] !== targetClass) isContour = true;
        if (y === height - 1 || segmentationMap[y + 1][x] !== targetClass) isContour = true;

        if (isContour) {
          contourPoints.push({ x, y });
        }
      }
    }
  }

  // 对轮廓点进行简化,减少顶点数量 (可选)
  const simplifiedContour = simplifyContour(contourPoints, 5); // 5 是简化容差值

  return simplifiedContour;
}

// 轮廓简化函数 (可以使用 Ramer-Douglas-Peucker 算法)
function simplifyContour(points, tolerance) {
  // 简化算法的实现...
  // 这里省略,因为算法比较复杂
  // 可以参考:https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
  // 简单实现:保留距离最远的点,然后递归处理
  if (points.length <= 2) return points; // 小于两个点,直接返回

  let maxDist = 0;
  let maxIndex = 0;

  for (let i = 1; i < points.length - 1; i++) {
    const dist = perpendicularDistance(points[i], points[0], points[points.length - 1]);
    if (dist > maxDist) {
      maxDist = dist;
      maxIndex = i;
    }
  }

  if (maxDist > tolerance) {
    const left = simplifyContour(points.slice(0, maxIndex + 1), tolerance);
    const right = simplifyContour(points.slice(maxIndex), tolerance);
    return left.slice(0, left.length - 1).concat(right);  // 去掉重复点
  } else {
    return [points[0], points[points.length - 1]];  // 简化为两个端点
  }

  function perpendicularDistance(point, lineStart, lineEnd) {
    const dx = lineEnd.x - lineStart.x;
    const dy = lineEnd.y - lineStart.y;

    if (dx === 0 && dy === 0) {
      // Line is a point
      return Math.sqrt(Math.pow(point.x - lineStart.x, 2) + Math.pow(point.y - lineStart.y, 2));
    }

    const t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy);

    let closestX = lineStart.x + t * dx;
    let closestY = lineStart.y + t * dy;

    if (t < 0) {
      closestX = lineStart.x;
      closestY = lineStart.y;
    } else if (t > 1) {
      closestX = lineEnd.x;
      closestY = lineEnd.y;
    }

    return Math.sqrt(Math.pow(point.x - closestX, 2) + Math.pow(point.y - closestY, 2));
  }
}

步骤2:生成 clip-path

有了轮廓点,我们就可以生成 clip-pathpolygon() 函数的参数了。

function generateClipPath(contourPoints) {
  const polygonPoints = contourPoints.map(point => `${point.x}px ${point.y}px`).join(', ');
  return `polygon(${polygonPoints})`;
}

步骤3:应用到元素

最后,把生成的 clip-path 值应用到我们的元素上。


<div class="segmented-image">
  <img src="your-image.jpg" alt="Segmented Image">
</div>

<style>
.segmented-image {
  width: 640px;
  height: 480px;
  position: relative;
}

.segmented-image img {
  width: 100%;
  height: 100%;
  object-fit: cover; /* 保证图片不失真 */
  clip-path: polygon(0px 0px, 640px 0px, 640px 480px, 0px 480px); /* 默认的矩形 */
}
</style>

<script>
// 假设 segmentationResult 是我们拿到的分割结果
const segmentationResult = {
  width: 640,
  height: 480,
  segmentationMap: [
    // 模拟数据
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(640).fill(0).map(() => Math.random() > 0.5 ? 1 : 0),
    Array(6

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注