四十七、openlayers官网示例Image Filters——给地图添加锐化、浮雕、边缘等滤镜效果

 官网demo示例:

Image Filters

这篇讲的是如何给地图添加滤镜。

一看代码,,好家伙,信息量满满,全都看不懂。。。

咱只能一段一段扒。。。

首先添加一个底图到地图上,这个好理解。

    const imagery = new TileLayer({
      source: new OGCMapTile({
        url: "https://maps.gnosis.earth/ogcapi/collections/NaturalEarth:raster:HYP_HR_SR_OB_DR/map/tiles/WebMercatorQuad",
        crossOrigin: "",
      }),
    });
    const map = new Map({
      layers: [imagery],
      target: "map",
      view: new View({
        center: fromLonLat([-120, 50]),
        zoom: 6,
      }),
    });

 新建一个对象定义了一组卷积核。

    const kernels = {
      none: [0, 0, 0, 0, 1, 0, 0, 0, 0], //无
      sharpen: [0, -1, 0, -1, 5, -1, 0, -1, 0], //锐化滤波器
      sharpenless: [0, -1, 0, -1, 10, -1, 0, -1, 0], //增强图像的边缘和细节,但比 sharpen 更强烈。
      blur: [1, 1, 1, 1, 1, 1, 1, 1, 1], //平滑滤波器,通过对邻域像素值求平均来模糊图像
      shadow: [1, 2, 1, 0, 1, 0, -1, -2, -1], //阴影滤波器
      emboss: [-2, 1, 0, -1, 1, 1, 0, 1, 2], //浮雕滤波器
      edge: [0, 1, 0, 1, -4, 1, 0, 1, 0], //边缘检测滤波器
    };

 等等,啥叫卷积核?

卷积操作是一种通过一个卷积核矩阵(kernel)来滤波图像的技术,它可以实现各种图像效果,比如锐化、模糊、阴影、浮雕和边缘检测等。

 

 啥叫卷积核矩阵?

卷积核是一个 3x3 的矩阵,每个元素代表像素在滤波时的权重。卷积操作通过将这个卷积核在图像上滑动,将核矩阵与图像中的每个 3x3 区域逐个相乘,然后求和,生成新的像素值。

例如,对于 sharpen 卷积核:

0  -1  0
-1  5 -1
 0 -1  0

这个卷积核在图像上滑动时,会增强中心像素值(乘以 5)并减弱周围像素值(乘以 -1)。通过这种方式,不同的卷积核可以实现各种图像处理效果,如锐化、模糊、浮雕等。 

卷积核进行归一化处理函数:

function normalize(kernel) {
      // 获取卷积核的长度
      const len = kernel.length;
      // 创建一个与卷积核相同长度的新数组
      const normal = new Array(len);
      let i,
        sum = 0;
      // 计算卷积核中所有元素的总和
      for (i = 0; i < len; ++i) {
        sum += kernel[i];
      }
      // 如果总和小于等于0,设置sum为1并标记为未归一化
      if (sum <= 0) {
        normal.normalized = false;
        sum = 1;
      } else {
        // 如果总和大于0,标记为已归一化
        normal.normalized = true;
      }
      // 将卷积核中的每个元素除以总和,得到归一化后的值
      for (i = 0; i < len; ++i) {
        normal[i] = kernel[i] / sum;
      }
      // 返回归一化后的卷积核
      return normal;
    }

将卷积核中的每个元素除以总和 sum,以确保所有元素的总和为1。这样可以保证在卷积操作过程中,图像的整体亮度不会改变 。

看到这,你是不是也跟我一样还有点懵,没事,咱们记住这句话就行:

 卷积核是一个 3x3 的矩阵,每个元素代表像素在滤波时的权重。卷积操作通过将这个卷积核在图像上滑动,将核矩阵与图像中的每个 3x3 区域逐个相乘,然后求和,生成新的像素值。

也就是说,我们得获取图像中的像素数据,然后通过卷积核一类的计算操作,将图像的存储数据进行修改,生成一个新图像,最终实现滤镜效果。

那么问题来了,图像的数据是怎么存储的呢?

function convolve(context, kernel) {
      const canvas = context.canvas;
      const width = canvas.width;
      const height = canvas.height;

      const inputData = context.getImageData(0, 0, width, height).data;

      // 创建一个新的 ImageData 对象用于输出图像数据
      const output = context.createImageData(width, height);
      const outputData = output.data;
    }

使用 context.getImageData(0, 0, width, height).data来获取图像上的数据,打印一下,看到以下数组:

 对于一个图像来说,inputData 中的数据是按像素顺序存储的,每个像素占用 4 个连续的数组元素,分别表示该像素的红、绿、蓝和透明度(Alpha)值。具体结构如下:

[ R, G, B, A, R, G, B, A, R, G, B, A, ... ]

假设我们有一个 2x2 像素的图像,其像素颜色如下:

  • (0, 0): 红色 (255, 0, 0, 255)
  • (1, 0): 绿色 (0, 255, 0, 255)
  • (0, 1): 蓝色 (0, 0, 255, 255)
  • (1, 1): 白色 (255, 255, 255, 255)

inputData 将会是:

[ 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255 ]

每四个一组,刚好存储了四个像素点的值。 

这里有个小细节,我们在css中写颜色时透明度的取值是0-100,但实际上,计算机存储的时候范围是0-255,所以这里的透明度的取值是0-255。 

知道了图像结构,我们可以在数组中获取单个像素的值。

假设我们有一个宽度为 width 的图像,要访问坐标 (x, y) 处的像素,可以通过以下方式计算索引: 

const index = (y * width + x) * 4;
const red = inputData[index];
const green = inputData[index + 1];
const blue = inputData[index + 2];
const alpha = inputData[index + 3];

 假设图像宽度为 2,要访问坐标 (1, 0) 处的像素(绿色)的颜色值:

const width = 2;
const x = 1;
const y = 0;
const index = (y * width + x) * 4;
const red = inputData[index];         // 0
const green = inputData[index + 1];   // 255
const blue = inputData[index + 2];    // 0
const alpha = inputData[index + 3];   // 255

了解了图像基本的存储规律,接下来我们来看具体计算函数 convolve

function convolve(context, kernel) {
      const canvas = context.canvas;
      const width = canvas.width;
      const height = canvas.height;

      // 假设 kernel 是一个归一化的卷积核矩阵,其大小为 size x size
      const size = Math.sqrt(kernel.length);
      const half = Math.floor(size / 2);
      // 获取输入图像数据
      const inputData = context.getImageData(0, 0, width, height).data;
      // 创建一个新的 ImageData 对象用于输出图像数据
      const output = context.createImageData(width, height);
      const outputData = output.data;
      // 遍历每个像素
      for (let pixelY = 0; pixelY < height; ++pixelY) {
        const pixelsAbove = pixelY * width;
        for (let pixelX = 0; pixelX < width; ++pixelX) {
          let r = 0,
            g = 0,
            b = 0,
            a = 0;
          // 遍历卷积核
          for (let kernelY = 0; kernelY < size; ++kernelY) {
            for (let kernelX = 0; kernelX < size; ++kernelX) {
              let weight = kernel[kernelY * size + kernelX];
              const neighborY = Math.min(
                height - 1,
                Math.max(0, pixelY + kernelY - half)
              );
              const neighborX = Math.min(
                width - 1,
                Math.max(0, pixelX + kernelX - half)
              );
              const inputIndex = (neighborY * width + neighborX) * 4;

              // 累加加权后的像素值
              r += inputData[inputIndex] * weight;
              g += inputData[inputIndex + 1] * weight;
              b += inputData[inputIndex + 2] * weight;
              a += inputData[inputIndex + 3] * weight;
            }
          }
          const outputIndex = (pixelsAbove + pixelX) * 4;
          outputData[outputIndex] = r;
          outputData[outputIndex + 1] = g;
          outputData[outputIndex + 2] = b;
          outputData[outputIndex + 3] = kernel.normalized ? a : 255; // 如果卷积核是归一化的,则使用计算后的 alpha,否则设为 255
        }
      }
      // 将输出图像数据放回到画布上下文中
      context.putImageData(output, 0, 0);
    }

 代码很多,但主要是两个循环,一个是循环所有像素点,将每个像素点进行更改。一个是循环卷积核,拿到权重生成累加权重之后的rgb值

其中   let weight = kernel[kernelY * size + kernelX]; 就是获取卷积核的权重值

假设我们有一个 3x3 的卷积核,并希望获取其中元素的位置:

const kernel = [
  0, -1, 0,
  -1, 5, -1,
  0, -1, 0
];

const size = 3; // 卷积核的边长

// 假设我们要获取位置 (1, 1) 的权重值
const kernelX = 1;
const kernelY = 1;

const index = kernelY * size + kernelX; // 计算一维索引
const weight = kernel[index]; // 获取权重值

console.log(weight); // 输出:5

当进行卷积操作时,卷积核需要对每个像素及其周围的像素进行处理。在图像边缘处,卷积核会超出图像的边界,因此需要处理这些边界情况。neighborY 计算了在 pixelY 位置应用卷积核时相应的邻近像素的 y 坐标。 

const neighborY = Math.min(
      height - 1,
      Math.max(0, pixelY + kernelY - half)
);
const neighborX = Math.min(
      width - 1,
      Math.max(0, pixelX + kernelX - half)
);

 现实需求中,我们往往会碰到类似这种设计稿,地图上的地貌纹理相对突出,这时候,我们就可以加上滤镜效果来实现。

完整代码:

<template>
  <div class="box">
    <h1>Image Filters</h1>
    <div id="map" class="map"></div>
    <select id="kernel" name="kernel">
      <option>none</option>
      <option selected>sharpen</option>
      <option value="sharpenless">sharpen less</option>
      <option>blur</option>
      <option>shadow</option>
      <option>emboss</option>
      <option value="edge">edge detect</option>
    </select>
  </div>
</template>

<script>
import Map from "ol/Map.js";
import View from "ol/View.js";
import XYZ from "ol/source/XYZ.js";
import { fromLonLat } from "ol/proj.js";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer.js";
import { OGCMapTile, Vector as VectorSource } from "ol/source.js";
export default {
  name: "",
  components: {},
  data() {
    return {
      map: null,
      extentData: "",
      message: {
        name: "",
        color: "",
      },
    };
  },
  computed: {},
  created() {},
  mounted() {
    const imagery = new TileLayer({
      source: new OGCMapTile({
        url: "https://maps.gnosis.earth/ogcapi/collections/NaturalEarth:raster:HYP_HR_SR_OB_DR/map/tiles/WebMercatorQuad",
        crossOrigin: "",
      }),
    });
    const map = new Map({
      layers: [imagery],
      target: "map",
      view: new View({
        center: fromLonLat([-120, 50]),
        zoom: 6,
      }),
    });
    //卷积核
    const kernels = {
      none: [0, 0, 0, 0, 1, 0, 0, 0, 0], //无
      sharpen: [0, -1, 0, -1, 5, -1, 0, -1, 0], //锐化滤波器
      sharpenless: [0, -1, 0, -1, 10, -1, 0, -1, 0], //增强图像的边缘和细节,但比 sharpen 更强烈。
      blur: [1, 1, 1, 1, 1, 1, 1, 1, 1], //平滑滤波器,通过对邻域像素值求平均来模糊图像
      shadow: [1, 2, 1, 0, 1, 0, -1, -2, -1], //阴影滤波器
      emboss: [-2, 1, 0, -1, 1, 1, 0, 1, 2], //浮雕滤波器
      edge: [0, 1, 0, 1, -4, 1, 0, 1, 0], //边缘检测滤波器
    };

    function normalize(kernel) {
      // 获取卷积核的长度
      const len = kernel.length;
      // 创建一个与卷积核相同长度的新数组
      const normal = new Array(len);
      let i,
        sum = 0;
      // 计算卷积核中所有元素的总和
      for (i = 0; i < len; ++i) {
        sum += kernel[i];
      }
      // 如果总和小于等于0,设置sum为1并标记为未归一化
      if (sum <= 0) {
        normal.normalized = false;
        sum = 1;
      } else {
        // 如果总和大于0,标记为已归一化
        normal.normalized = true;
      }
      // 将卷积核中的每个元素除以总和,得到归一化后的值
      for (i = 0; i < len; ++i) {
        normal[i] = kernel[i] / sum;
      }
      // 返回归一化后的卷积核
      return normal;
    }

    const select = document.getElementById("kernel");
    let selectedKernel = normalize(kernels[select.value]);

    select.onchange = function () {
      selectedKernel = normalize(kernels[select.value]);
      console.log("selectedKernel", selectedKernel);
      map.render();
    };

    imagery.on("postrender", function (event) {
      convolve(event.context, selectedKernel);
    });

    function convolve(context, kernel) {
      const canvas = context.canvas;
      const width = canvas.width;
      const height = canvas.height;

      // 假设 kernel 是一个归一化的卷积核矩阵,其大小为 size x size
      const size = Math.sqrt(kernel.length);
      const half = Math.floor(size / 2);
      // 获取输入图像数据
      const inputData = context.getImageData(0, 0, width, height).data;
      // 创建一个新的 ImageData 对象用于输出图像数据
      const output = context.createImageData(width, height);
      const outputData = output.data;
      // 遍历每个像素
      for (let pixelY = 0; pixelY < height; ++pixelY) {
        const pixelsAbove = pixelY * width;
        for (let pixelX = 0; pixelX < width; ++pixelX) {
          let r = 0,
            g = 0,
            b = 0,
            a = 0;
          // 遍历卷积核
          for (let kernelY = 0; kernelY < size; ++kernelY) {
            for (let kernelX = 0; kernelX < size; ++kernelX) {
              let weight = kernel[kernelY * size + kernelX];
              const neighborY = Math.min(
                height - 1,
                Math.max(0, pixelY + kernelY - half)
              );
              const neighborX = Math.min(
                width - 1,
                Math.max(0, pixelX + kernelX - half)
              );
              const inputIndex = (neighborY * width + neighborX) * 4;

              // 累加加权后的像素值
              r += inputData[inputIndex] * weight;
              g += inputData[inputIndex + 1] * weight;
              b += inputData[inputIndex + 2] * weight;
              a += inputData[inputIndex + 3] * weight;
            }
          }
          const outputIndex = (pixelsAbove + pixelX) * 4;
          outputData[outputIndex] = r;
          outputData[outputIndex + 1] = g;
          outputData[outputIndex + 2] = b;
          outputData[outputIndex + 3] = kernel.normalized ? a : 255; // 如果卷积核是归一化的,则使用计算后的 alpha,否则设为 255
        }
      }
      // 将输出图像数据放回到画布上下文中
      context.putImageData(output, 0, 0);
    }
  },
  methods: {},
};
</script>

<style lang="scss" scoped>
#map {
  width: 100%;
  height: 500px;
}
.box {
  height: 100%;
}
</style>

最近更新

  1. TCP协议是安全的吗?

    2024-06-19 01:16:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-19 01:16:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-19 01:16:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-19 01:16:04       20 阅读

热门阅读

  1. 分数限制下,选好专业还是选好学校?

    2024-06-19 01:16:04       6 阅读
  2. linux git凭证管理

    2024-06-19 01:16:04       5 阅读
  3. 【MongoDB】如何在Debian 10 Linux上安装MongoDB

    2024-06-19 01:16:04       6 阅读
  4. 二分查找.

    2024-06-19 01:16:04       4 阅读
  5. oracle将字符串中的字符和数字拆分开等功能

    2024-06-19 01:16:04       5 阅读
  6. 华为OD机试 C++ - 评论转换输出

    2024-06-19 01:16:04       7 阅读
  7. 算法训练营day51

    2024-06-19 01:16:04       5 阅读
  8. 使用 Redis + Lua 实现分布式限流

    2024-06-19 01:16:04       9 阅读
  9. TTL 232难兄难弟对比

    2024-06-19 01:16:04       6 阅读
  10. Apache网页优化

    2024-06-19 01:16:04       6 阅读