Web Bluetooth 与点对点连接

news2024/11/23 14:53:15

前言

需求需要实现手持终端设备与 web 网页的点对点数据传输,不希望有服务器参与,想到了 web 的 USB 与 Bluetooth API,对 Web Bluetooth API 进行了研究。

蓝牙 GATT 基础知识

GATT(通用属性配置文件,蓝牙低功耗(BLE)中定义的一种规范)定义了如何在蓝牙低功耗设备之间进行数据的传输和交互。它规定了蓝牙设备之间的数据格式、通信协议以及数据的组织方式。通过 GATT,不同的蓝牙设备可以交换各种类型的数据,如传感器数据、设备状态信息等。

GATT 采用分层的组织结构,分为 Service(服务)、Characteristic(特性)、Property(属性)三层:

  • Service: 一个蓝牙设备可以有一个或多个服务,这些服务提供不同的功能,如 battery_service(电池服务)、heart_rate(心率服务)
  • Characteristic: 提供与服务相关的功能,比如 battery_service 服务的 battery_level 特征提供电池电量的数据
  • Property: 特性上的属性,用于操作特性值,比如 writeread 用于读写特性值

Service、Characteristic 都有一个 UUID 用于标识服务、特性,Service 的 UUID 格式固定为 0x0000[xxxx]-0000-1000-8000-00805F9B34FB,其中 [xxxx] 是可变部分,其余固定,比如电池服务的 UUID 为 0000180F-0000-1000-8000-00805f9b34fb,可简写为 0x180F,当自定义蓝牙 GATT 服务时定义的 UUID 需要采用相同的格式。

在使用 Web Bluetooth API 时,我们通过服务名称或服务 UUID 来找到我们需要的蓝牙服务。

Web Bluetooth API

Web Bluetooth 所有接口构建在 Promise 之上,只支持在可信来源中使用(localhost 或 https),主要有以下接口(完整接口查看 MDN 文档):

  • Bluetooth:提供查询蓝牙可用性和请求访问设备的方法
    • getAvailability:返回用户代理的蓝牙可用性
    • requestDevice:请求蓝牙设备,返回一个 BluetoothDevice 实例;必须由用户手势触发,一般会弹出蓝牙选择器,如果没有可用的蓝牙选择器则默认选择匹配的第一个
  • BluetoothDevice:蓝牙设备相关接口,具有一个 gatt 属性是对 BluetoothRemoteGATTServer 实例的引用
  • BluetoothRemoteGATTServer:可以理解为对 gatt 服务器的引用,通过 gatt 服务器可以获取到他所拥有的 Service
    • connected:脚本执行环境是否已与设备连接
    • connect:脚本执行环境连接到 BluetoothDevice
    • disconnect:脚本执行环境断开与 BluetoothDevice 的连接
    • getPrimaryService:通过服务别名或 UUID 获取到对应的 Service,是一个 BluetoothRemoteGATTService 实例
    • getPrimaryServices:获取多个 Service
  • BluetoothRemoteGATTService:对 Service 的引用
    • isPrimary:指示这是一个主要还是次要的服务
    • getCharacteristic:获取指定 UUID 的 Characteristic 特性,是一个 BluetoothRemoteGATTCharacteristic 实例
    • getCharacteristics:获取多个特性
  • BluetoothRemoteGATTCharacteristic:对特性的引用
    • readValue:读取特性值
    • writeValueWithResponse:写入特性值

看一个简单的示例:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button class="request">click me</button>

    <script>
      const request = window.document.querySelector('.request');

      request.addEventListener('click', async function () {
        const bluetooth = window.navigator.bluetooth;

        try {
          // 检查用户代理是否支持
          const isSupport = await bluetooth.getAvailability();

          if (!isSupport) {
            return window.alert('用户代理不支持蓝牙请求');
          }

          // 请求蓝牙设备
          const bluetoothDevice = await bluetooth.requestDevice({
            /** 过滤器选项 */
            filters: [
              {
                /** 过滤拥有 battery_service | 0x1101 | 0000180D-0000-1000-8000-00805f9b34fb 服务的设备 */
                services: ['battery_service', 0x1101, '0000180D-0000-1000-8000-00805f9b34fb'],
                /** 过滤名称为 RedMi Note13Pro 的设备 */
                name: 'RedMi Note13Pro',
                /** 过滤名称前缀为 RedMi 的设备 */
                namePrefix: 'RedMi'
              }
            ],
            /** 排除项,选项与 filters 相同 */
            exclusionFilters: [],
            /**
             * 服务选项,通常需要包含此项,告诉浏览器你随后想要访问的蓝牙服务;
             * 如果其他选项中没有指定服务,则必须在此处指定,否则随后访问服务时抛出异常
             * */
            optionalServices: ['battery_service'],
            /** 匹配所有设备,一般不建议使用 */
            acceptAllDevices: false
          });

          // gatt 服务器
          const server = bluetoothDevice.gatt;
          if (!server.connected) {
            // 创建连接
            await server.connect();
          }

          /** 获取电池 Service */
          const batteryService = await server.getPrimaryService('battery_service');

          /** 获取电池电量的 Characteristic */
          const batteryLevelCharacteristic =
            await batteryService.getCharacteristic('battery_level');

          /** 读取 Characteristic 值,这里是电池剩余电量 */
          const batteryLevelValue = await batteryLevelCharacteristic.readValue();

          /** 打印剩余电量百分比 */
          console.log(`Battery percentage is ${batteryLevelValue.getUint8(0)}`);
        } catch (err) {
          window.alert('Error: ' + err.message);
        }
      });
    </script>
  </body>
</html>

得到的效果为:

image

Android 自定义 GATT 服务

简单通过上述代码搜索设备并尝试建立连接进行通讯,会发现无法达到自己想要的效果;为了更好的理解 Web Bluetooth,自定义一个 GATT 服务实现通讯是很好的方法:

package com.example.myapplication

import android.Manifest
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattServer
import android.bluetooth.BluetoothGattServerCallback
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import java.util.UUID


class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        initBluetoothGATT()
    }

    // 初始化蓝牙 GATT 服务并开始广播
    @RequiresApi(Build.VERSION_CODES.S)
    private fun initBluetoothGATT() {
        // 获取蓝牙管理器
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager

        // 获取蓝牙适配器
        val bluetoothAdapter = bluetoothManager.adapter

        // 定义 gatt 服务器
        var bluetoothGattServer: BluetoothGattServer? = null

        // 检查权限
        if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
            // 定义 gatt 服务器的回调
            val bluetoothGattServerCallback = object : BluetoothGattServerCallback() {
                // 与蓝牙设备的连接状态变更
                override fun onConnectionStateChange(
                    device: BluetoothDevice,
                    status: Int,
                    newState: Int
                ) {
                    if (newState == BluetoothProfile.STATE_CONNECTED) {
                        Log.d(TAG, "Device connected")
                    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                        Log.d(TAG, "Device disconnected")
                    }
                }

                // Service 被添加
                override fun onServiceAdded(status: Int, service: BluetoothGattService) {
                    if (service.uuid == SERVICE_UUID) {
                        Log.d(TAG, "Service added successfully.")
                    }
                }

                // 特性读取请求
                override fun onCharacteristicReadRequest(
                    device: BluetoothDevice,
                    requestId: Int,
                    offset: Int,
                    characteristic: BluetoothGattCharacteristic
                ) {
                    // 判断是否指定特性请求
                    if (characteristic.uuid == CHARACTERISTIC_UUID) {

                        // 处理读取请求,这里可以设置返回的数据
                        val dataToSend = "Hello from custom characteristic!".toByteArray()

                        // 设置特性值
                        characteristic.setValue(dataToSend)

                        // 检查权限,这里是避免编辑器警告
                        if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
                            // 发送响应,必须调用此方法,web 端的 readValue 方法才能继续执行
                            bluetoothGattServer!!.sendResponse(
                                device,
                                requestId,
                                BluetoothGatt.GATT_SUCCESS,
                                offset,
                                dataToSend
                            )
                        }
                    }
                }

                // 特性写入请求
                override fun onCharacteristicWriteRequest(
                    device: BluetoothDevice,
                    requestId: Int,  // 请求的特性 id
                    characteristic: BluetoothGattCharacteristic,  // 特性
                    preparedWrite: Boolean,
                    responseNeeded: Boolean,  // 是否需要响应
                    offset: Int,  // 数据偏移,数据可能是分段发送的
                    value: ByteArray // 数据值
                ) {
                    // 判断是否指定特性
                    if (characteristic.uuid == CHARACTERISTIC_UUID) {
                        characteristic.setValue(value)
                    }
                }
            }

            // 打开一个 gatt 服务器
            bluetoothGattServer = bluetoothManager.openGattServer(this, bluetoothGattServerCallback)

            // 获取蓝牙广播器
            val bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser

            // 判断是否支持蓝牙
            if (bluetoothLeAdvertiser != null) {
                // 蓝牙广播设置
                val settings = AdvertiseSettings.Builder()
                    .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) // 设置低延迟模式
                    .setConnectable(true) // 设置可连接
                    .setTimeout(0) // 不超时关闭,除非手动关闭
                    .build()

                // 蓝牙广播数据
                val data = AdvertiseData.Builder()
                    .setIncludeDeviceName(true) // 设置广播时的数据包含设备名称
                    .addServiceUuid(ParcelUuid(SERVICE_UUID)) // 添加自定义服务的 UUID 到广播数据中
                    .build()

                // 开始广播
                bluetoothLeAdvertiser.startAdvertising(
                    settings,
                    data,
                    object : AdvertiseCallback() {
                        // 正常开始广播
                        override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
                            Log.d(TAG, "Advertising started successfully.")
                        }

                        // 无法开启广播
                        override fun onStartFailure(errorCode: Int) {
                            Log.e(TAG, "Advertising failed with error code: $errorCode")
                        }
                    })

                // 构造一个自定义 UUID 的 Service,指定为主要 Service
                val service =
                    BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)

                // 构造一个自定义 UUID 的 Characteristic
                val characteristic = BluetoothGattCharacteristic(
                    CHARACTERISTIC_UUID,
                    // 设置读写属性
                    BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE,
                    // 设置读写权限
                    BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE

                )

                // 将 Characteristic 添加至 Service
                service.addCharacteristic(characteristic)

                // 将 Service 添加至打开的 GATT 服务器
                bluetoothGattServer.addService(service)
            }
        } else {
            // 没有权限请求权限
            requestPermissions(
                arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
                REQUEST_BLUETOOTH_PERMISSION_CODE
            )
        }
    }

    @Deprecated("Deprecated in Java")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode == REQUEST_BLUETOOTH_PERMISSION_CODE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限被授予,可以进行蓝牙连接操作
                initBluetoothGATT()
            } else {
                // 权限被拒绝
                Toast.makeText(this, "蓝牙连接权限被拒绝", Toast.LENGTH_SHORT).show()
            }
        }
    }

    companion object {
        private const val TAG = "BluetoothService"

        // 请求权限 code
        private const val REQUEST_BLUETOOTH_PERMISSION_CODE = 1001

        // 定义服务 UUID
        private val SERVICE_UUID: UUID = UUID.fromString("00009527-0000-1000-8000-00805f9b34fb")

        // 定义特性 UUID
        private val CHARACTERISTIC_UUID: UUID =
            UUID.fromString("11009527-1100-1100-1100-110011001100")
    }
}

上述代码是 Android 应用主 Activity 的代码,配合上述代码需要在 Android 应用配置清单中添加对应的权限:

<!-- AndroidManifest.xml -->

<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- 蓝牙搜索配对 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 操纵蓝牙的开启-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 如果应用必须安装在支持蓝牙的设备上,可以将下面的required的值设置为true。-->
<uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="false" />

在编译打开这个 Android 应用后,会开启一个 UUID 为 0x9527 的蓝牙服务,包含一个 UUID 为 11009527-1100-1100-1100-110011001100 的特性,这个特性会在被读取时,将特性值设置为 “Hello from custom characteristic!”,并发送响应到调用方。我们用以下 web 端的代码来测试一下:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button class="request">click me</button>

    <script>
      const request = window.document.querySelector('.request');

      request.addEventListener('click', async function () {
        const bluetooth = window.navigator.bluetooth;

        try {
          const isSupport = await bluetooth.getAvailability();

          if (!isSupport) {
            return window.alert('用户代理不支持蓝牙请求');
          }

          const bluetoothDevice = await bluetooth.requestDevice({
            filters: [
              {
                services: [0x9527] // 过滤蓝牙服务 UUID 为 9527 的设备
              }
            ],
            optionalServices: [0x9527] // 稍后需要操作此服务
          });

          const server = bluetoothDevice.gatt; // 获取对蓝牙服务器的引用
          if (!server.connected) {
            await server.connect(); // 连接服务器
          }

          const service = await server.getPrimaryService(0x9527); // 获取 9527 的蓝牙服务

          const characteristic = await service.getCharacteristic(
            '11009527-1100-1100-1100-110011001100'
          ); // 获取 11009527-1100-1100-1100-110011001100 的特性
          
          // 侦听特性值变更
          characteristic.addEventListener('characteristicvaluechanged', (e) => {
            console.log('characteristicvaluechanged: ', e.target.value);
          });

          // 读取特性值
          const value = await characteristic.readValue();

          // 编码响应
          console.log(new TextDecoder().decode(value));
        } catch (err) {
          window.alert('Error: ' + err.message);
        }
      });
    </script>
  </body>
</html>

查看效果:

image

除了被动读写外,Android 端的 gatt 服务器还支持 notifyCharacteristicChanged 方法,此方法会触发 web 端 characteristic 实例的 characteristicvaluechanged 事件获取最新的特性值,通过这种方式可以做到主动通知 web 端的效果。

通过自定义终端应用实现自定义 GATT 服务器的方式可以完成与 web 端的点对点连接,但是 web bluetooth 的兼容性还不足以支持完成大型的项目,稳定性也无法考证。

原文地址:https://yuanyxh.com/articles/web_bluetooth_and_point_to_point_connection.html

参考资料

  • MDN: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API
  • 通过 JavaScript 与蓝牙设备通信: https://developer.chrome.com/docs/capabilities/bluetooth?hl=zh-cn
  • 一文带你认识蓝牙 GATT 协议: https://juejin.cn/post/7160308393503113247

– end

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

K8S 发布应用

前言 昨儿个用 unbuntu20.04 又装了一次K8S 用的 kubeadm containerd Cilium (CNI) 又重新撸了一遍 这里只记录 应用发布的笔记 正文 #创建deployment kubectl create deployment nginx --imagenginx #我这边大约30秒后显示为 ready kubectl get deployments kubectl desc…

4.7 Sensors -- useScroll

4.7 Sensors – useScroll https://vueuse.org/core/useScroll/ 作用 响应式的监听滚动位置和状态。 官方示例 <script setup lang"ts"> import { useScroll } from vueuse/coreconst el ref<HTMLElement | null>(null) const { x, y, isScrolling…

【Python系列】只更新非空的字段

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

数字电路专题:verilog 阻塞赋值和非阻塞赋值

verilog 阻塞赋值 和 非阻塞赋值 “”阻塞赋值&#xff0c; ”<”非阻塞赋值。阻塞赋值为执行完一条赋值语句&#xff0c;再执行下一条&#xff0c;可理解为顺序执行&#xff0c;而且赋值是立即执行&#xff1b; 非阻塞赋值可理解为并行执行&#xff0c;不考虑顺序&#x…

【python计算机视觉编程——7.图像搜索】

python计算机视觉编程——7.图像搜索 7.图像搜索7.1 基于内容的图像检索&#xff08;CBIR&#xff09;从文本挖掘中获取灵感——矢量空间模型&#xff08;BOW表示模型&#xff09;7.2 视觉单词**思想****特征提取**&#xff1a; 创建词汇7.3 图像索引7.3.1 建立数据库7.3.2 添加…

构建并训练卷积神经网络(CNN)对CIFAR-10数据集进行分类

深度学习实践&#xff1a;构建并训练卷积神经网络&#xff08;CNN&#xff09;对CIFAR-10数据集进行分类 引言 在计算机视觉领域中&#xff0c;CIFAR-10数据集是一个经典的基准数据集&#xff0c;广泛用于图像分类任务。本文将介绍如何使用PyTorch框架构建一个简单的卷积神经…

微信小程序uniappvue3版本-控制tabbar某一个的显示与隐藏

1. 首先在pages.json中配置tabbar信息 2. 在代码根目录下添加 tabBar 代码文件 直接把微信小程序文档里面的四个文件复制到自己项目中就可以了 3. 根据自己的需求更改index.js文件 首先我这里需要判断什么时候隐藏某一个元素&#xff0c;需要引入接口 然后在切换tabbar时&#…

git:认识git和基本操作(1)

目录 一、版本控制器 1.安装git 2.创建git本地仓库 3.配置git 二、git操作&#xff08;1&#xff09; 1.工作区、暂存区、版本库 2.添加文件 3.查看.git 4.修改文件 一、版本控制器 所谓的版本控制器&#xff0c;就是能让你了解到每一个文件的修改历史。相应的&#x…

Maven的安装

一、安装 压缩包解压完的目录如下所示&#xff08;此处为绿色免安装版&#xff09;&#xff1a; &#xff08;其余三个文件是针对Maven版本&#xff0c;第三方软件等简要介绍&#xff09; 二、环境变量 前提&#xff1a; jdk最低版本为JAVA7&#xff08;即jdk17&#xff09…

R语言统计分析——重复测量方差分析

参考资料&#xff1a;R语言实战【第2版】 所谓重复测量方差分析&#xff0c;即受试者被测量不止一次。本例使用数据集市co2数据集&#xff1a;因变量是二氧化碳吸收量&#xff08;uptake&#xff09;&#xff0c;自变量是植物类型&#xff08;Type&#xff09;和七种水平的二氧…

Gitness 基础安装

文章目录 Docker 安装注册账户创建项目导入已有仓库配置 Github Token同步源代码仓库 官方链接 Gitness was the next step in the evolution of Drone, from continuous integration to source code hosting, bringing code management and pipelines closer together. Gitnes…

自动化表格处理的革命:智能文档系统技术解析

在当今数据驱动的商业环境中&#xff0c;表格数据的自动化处理成为了企业提高效率、降低成本的关键。企业智能文档系统在智能表格识别方面展现出卓越的性能&#xff0c;通过精准识别和处理各种通用表格&#xff0c;显著提升了企业文档管理的智能化水平。本文将深入探讨该系统在…

STM32的使用方法一

注:我采用的是STM32F103RC芯片、相应的电路图和STM32CubeIDE软件这是在STM32CubeIDE软件定义芯片后&#xff0c;所给的必要的代码逻辑&#xff0c;加上了注释 #include "main.h"/* Private variables ---------------------------------------------------------*//…

Java数据结构(八)——插入排序、希尔排序

文章目录 插入排序算法介绍代码实现复杂度和稳定性 希尔排序算法介绍代码实现复杂度和稳定性 将这两种排序放在一起的原因是它们都属于 “插入”(式)排序。 还有很多排序思想&#xff0c;这里不放在一篇文章介绍是因为会导致篇幅过长&#xff0c;我们会按分类多次介绍不同的排序…

[数据集][目标检测]智慧农业草莓叶子病虫害检测数据集VOC+YOLO格式4040张9类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;4040 标注数量(xml文件个数)&#xff1a;4040 标注数量(txt文件个数)&#xff1a;4040 标注…

HTTP协议 HTTPS协议 MQTT协议介绍

目录 一&#xff0e;HTTP协议 1. HTTP 协议介绍 基本介绍&#xff1a; 协议&#xff1a; 注意&#xff1a; 2. HTTP 协议的工作过程 基础术语&#xff1a; 客户端&#xff1a; 主动发起网络请求的一端 服务器&#xff1a; 被动接收网络请求的一端 请求&#xff1a; …

基于MinerU的PDF解析API

基于MinerU的PDF解析API - MinerU的GPU镜像构建 - 基于FastAPI的PDF解析接口支持一键启动&#xff0c;已经打包到镜像中&#xff0c;自带模型权重&#xff0c;支持GPU推理加速&#xff0c;GPU速度相比CPU每页解析要快几十倍不等 主要功能 删除页眉、页脚、脚注、页码等元素&…

实验记录 | 点云处理 | K-NN算法3种实现的性能比较

引言 K近邻&#xff08;K-Nearest Neighbors, KNN&#xff09;算法作为一种经典的无监督学习算法&#xff0c;在点云处理中的应用尤为广泛。它通过计算点与点之间的距离来寻找数据点的邻居&#xff0c;从而有效进行点云分类、聚类和特征提取。本菜在复现点云文章过程&#xff…

【OpenCV2.2】图像的算术与位运算(图像的加法运算、图像的减法运算、图像的融合)、OpenCV的位运算(非操作、与运算、或和异或)

1 图像的算术运算 1.1 图像的加法运算 1.2 图像的减法运算 1.3 图像的融合 2 OpenCV的位运算 2.1 非操作 2.2 与运算 2.3 或和异或 1 图像的算术运算 1.1 图像的加法运算 add opencv使用add来执行图像的加法运算 图片就是矩阵, 图片的加法运算就是矩阵的加法运算, 这就要求加…

notepad下载安装教程

一、强大高效的代码编辑器 Notepad 是一款功能强大的代码编辑器&#xff0c;专为程序员和开发人员设计。无论是编写代码、处理文本文件&#xff0c;还是进行快速编辑&#xff0c;Notepad 都能提供卓越的性能和便利的功能&#xff0c;极大提升您的工作效率。 二、安装详细教程…