四十七、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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/721111.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第十一章:接口

接口 文章目录 接口一、简介1.1 接口是什么1.2 接口的作用1.3 接口的开发与调用1.4 接口的组成 二、RESTful API三、json-server四、接口测试工具五、接口的创建 一、简介 1.1 接口是什么 接口是前后端通信的桥梁 简单理解&#xff1a;一个接口就是 服务中的一个路由规则&am…

十分钟学会微调大语言模型

有同学给我留言说想知道怎么训练自己的大语言模型&#xff0c;让它更贴合自己的业务场景。完整的大语言模型训练成本比较高昂&#xff0c;不是我们业余玩家能搞的&#xff0c;如果我们只是想在某个业务场景或者垂直的方面加强大模型的能力&#xff0c;可以进行微调训练。 本文…

ssl证书能认证多少个域名

SSL证书能认证的域名数量取决于SSL证书的类型。不同类型的SSL证书支持不同数量的域名&#xff1a; SSL证书&#xff0c;作为网络安全的基石之一&#xff0c;起着至关重要的作用。它通过为网站提供加密连接&#xff0c;确保数据传输的安全性和完整性&#xff0c;同时验证网站的真…

spark常见问题

写文章只是为了学习总结或者工作内容备忘&#xff0c;不保证及时性和准确性&#xff0c;看到的权当个参考哈&#xff01; 1. 执行Broadcast大表时&#xff0c;等待超时异常&#xff08;awaitResult&#xff09; 现象&#xff1a;org.apache.spark.SparkException: Exception…

答应我,完成单位投稿任务用对的方法别让自己受投稿之苦

在这个信息爆炸的时代,单位的形象塑造与品牌传播已成为不可忽视的关键环节。作为单位的信息宣传员,我深知每一次对外发声的重要性,它不仅是展示我们工作成果的窗口,更是连接公众、塑造品牌形象的桥梁。然而,在传统的投稿方式中,尤其是依赖于邮箱投稿,我经历了太多次的挫败与无奈…

LeetCode 算法:合并两个有序链表 c++

原题链接&#x1f517;&#xff1a;合并两个有序链表 难度&#xff1a;简单⭐️ 题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;…

PHP学习笔记--初学

笔录&#xff1a;我是从黑马里面找的资料学习的&#xff0c;如果有人看我的笔记话&#xff0c;建议去看黑马程序课程&#xff0c;更详细一些。 目录 php定义&#xff0c;平台支持&#xff1a; 静态网站&#xff1a; 动态网站&#xff1a; 服务器概念&#xff1a; IP的概念…

JavaFX 分隔符

Separator类表示水平或垂直分隔线。它分割元素&#xff0c;不产生任何动作。 我们可以设计风格&#xff0c;应用视觉效果&#xff0c;并为分隔符设置动画。 默认情况下&#xff0c;分隔符是水平的。我们可以使用setOrientation方法改变它的方向。 Separator类扩展了Node类。…

2024/6/18(RBAC,查询用户权限,细粒度授权,选课,支付宝生成二维码支付,支付结果查询需要内网穿透)

黑马程序员【学成在线项目】,P141 测试沙箱支付宝_黑马学成在线支付宝沙箱-CSDN博客 需要内网穿透

reverse-android-实战喜马拉雅-ollvm

资料 1. apk: com.ximalaya.ting.android.apk. 2020年8月 可以使用 2. 抓包分析 java层分析 so层分析 登录的算法so是在 liblogin_encrypt.so中。 32位的&#xff0c; 用 IDA打开&#xff0c;查看 静态的导出函数。 打开 一个 首先看到 IDA VIEW 是一个横向 比较多的分支&am…

【3D模型库】机械三维模型库整理

1 开拔网 简介&#xff1a;开拔网是中国较早的机械设计交流平台&#xff0c;广受行业内的各个大学&#xff0c;公司以及行业人士的欢迎。网站有非常丰富的3D模型&#xff0c;CAD图纸&#xff0c;以及各类热门软件的下载。同时我们也为行业搭建一个平台&#xff0c;提供各类设计…

AI智能盒子助力中钢天源设备工厂升级安全防护

中钢集团安徽天源科技股份有限公司成立于2002年3月27日,是中央企业中国中钢股份有限公司控股的上市公司&#xff0c;主导产品为永磁铁氧体器件、钕铁硼器件、四氧化三锰、锶铁氧体预烧料及各类磁选机等。 在中钢天源智能化升级过程中&#xff0c;采用并定制开发一系列厂区安全…

【C++】Template模板

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

【Spine学习12】之 事件帧

1、新建事件帧&#xff1a; 2、选择第8s的攻击帧&#xff0c;点击第一步新建的attack事件帧前面的钥匙 这样每次动作到8s的时候会自动跳出事件帧提示 这个文字实际动画不会显示 事件是动画过程中所发生情况的触发器。 给程序员识别的

C++使用教程

目录 一、软件使用 二、C基础规则补充 关键字 整型取值范围 浮点型取值范围 字符型使用规则 字符串型使用规则 布尔类型 常用的转义移字符 三、数组、函数、指针、结构体补充 1.数组 2.函数 声明&#xff1a; 分文件编写&#xff1a; 值传递&#xff1a; 3.指…

省市县选择三级联动(使用高德API实现)

省市县选择如果自己实现是比较麻烦的&#xff0c;最近发现可以使用高德实现省市县联动选择&#xff0c;实现后来记录一下供大家参考。 文章目录 最终效果&#xff1a;一、准备工作二、完整页面代码 最终效果&#xff1a; 实现单次点击获取省市县名称&#xff0c;选择完成后返回…

【串口通信-USART】

串口通信 前言一、串行通信和并行通信二、波特率三、USRAT如何实现USART 四、通信的时候共地五、奇偶校验位总结 前言 大三上时候的笔记⇨32入门-串口通信-发送和接收数据&#x1f31f;更加偏向32部分的吧。 大三上左右的时候写过串口通信的笔记&#xff0c;那时候虽然青涩啥也…

多路h265监控录放开发-(1)建立head窗口并实现鼠标拖动整个窗口

头文件&#xff1a; //鼠标事件 用于拖动窗口//一下三个函数都是QWidget的可重载成员函数void mouseMoveEvent(QMouseEvent* ev) override;void mousePressEvent(QMouseEvent* ev) override;void mouseReleaseEvent(QMouseEvent* ev) override; 源文件&#xff1a; / /// 鼠标…

运算放大器共模抑制比(CMRR)

目录 运算放大器共模抑制比(CMRR) 三运放共模抑制比电路 运算放大器共模抑制比(CMRR) 如果信号均等施加至运算放大器的两个输入端&#xff0c;使差分输入电压不受影响&#xff0c;则输出也不应受影响。实际上&#xff0c;共模电压的变化会引起输出变化。运算放大器共模抑制比…

Locust框架

Locust 简介&#xff1a;是一个Python的第三方库&#xff0c;专门用来进行性能并发测试 Locust特点&#xff1a; 基于Python的开源负载测试工具支持多种操作系统支持二次开发能够模拟更多用户基于协程&#xff08;微线程&#xff09;的并发 Jmeter与Locust对比 Locust安装 …