OpenCV快速入门:目标检测——轮廓检测、轮廓的距、点集拟合和二维码检测

文章目录

  • 前言
  • 一、轮廓检测
    • 1.1 图像轮廓的概念
    • 1.2 轮廓检测算法简介
    • 1.3 轮廓检测基本步骤
    • 1.4 轮廓检测函数说明
      • 1.4.1 轮廓发现
      • 1.4.2 轮廓面积
      • 1.4.3 轮廓周长
      • 1.4.4 轮廓外接多边形
      • 1.4.5 点到轮廓距离
      • 1.4.6 凸包检测
    • 1.5 轮廓检测代码实现
  • 二、轮廓的距
    • 2.1 几何距
    • 2.2 中心距
    • 2.3 Hu距
    • 2.4 代码实现
  • 三、点集拟合
      • 最小包围三角形
      • 最小包围圆形
  • 四、二维码检测
  • 总结


前言

在当今数字化时代,计算机视觉的崛起使得目标检测成为科技领域中的一项关键技术。本文将带您快速入门OpenCV中的目标检测,深入探讨轮廓检测、轮廓的距、点集拟合以及二维码检测等核心概念。

OpenCV,作为一种强大的开源计算机视觉库,为开发者提供了丰富的工具和算法,使得目标检测不再是高门槛的技术难题。在本文中,我们将逐步了解目标检测中的关键步骤,从轮廓检测到轮廓的距,再到点集拟合和二维码检测。
OpenCV Logo

一、轮廓检测

1.1 图像轮廓的概念

图像轮廓是由一系列连续的边界点组成的曲线,表示了图像中目标的形状和结构。这些边界点连接在一起,形成了目标的外部轮廓。在计算机视觉中,理解和提取图像轮廓是进行目标检测和形状分析的基础。

1.2 轮廓检测算法简介

轮廓检测的算法旨在识别图像中的显著变化,即目标与背景之间的边界。常用的算法包括Sobel、Canny等边缘检测算法,它们通过检测图像中的梯度变化来确定轮廓位置。

1.3 轮廓检测基本步骤

在OpenCV中,轮廓检测主要使用findContours函数。该函数接受输入图像,并返回轮廓的列表。通过设定合适的阈值,可以在图像中找到目标的轮廓。接着,可以使用drawContours函数将轮廓绘制在原始图像上,使得我们能够直观地观察到目标的形状。

以下是轮廓检测的基本步骤:

  1. 读取图像并将其转换为灰度图像。
  2. 使用合适的边缘检测算法(如Canny)找到图像的边缘。
  3. 应用阈值,将边缘图像转换为二值图像。
  4. 使用findContours函数找到图像中的轮廓。
  5. 绘制轮廓,以便可视化或进一步的分析。

通过深入学习轮廓检测,我们为后续的目标检测过程奠定了坚实的基础。这一章节将帮助读者理解轮廓检测的核心原理以及在OpenCV中的具体实现方法。

1.4 轮廓检测函数说明

在进行轮廓检测时,我们不仅仅关注轮廓的发现,还要深入了解轮廓的一些重要属性。下面我们将通过Python和OpenCV代码演示如何实现轮廓检测及其相关操作。

1.4.1 轮廓发现

在计算机视觉和图像处理中,轮廓是表示图像中对象边界的一种重要方式。OpenCV库提供了 findContours 函数,用于在灰度图像中查找对象的轮廓。

下面是一个简单的代码示例,演示如何使用OpenCV发现轮廓:

# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用Canny边缘检测
edges = cv2.Canny(gray, 50, 150)
# 查找轮廓
_, contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 在原图上绘制轮廓
cv2.drawContours(image, contours, -1, (0, 255, 0), 2)

findContours 函数基本信息

def findContours(image, mode, method, contours=None, hierarchy=None, offset=None)

参数解释

  1. image: 输入的二值图像,通常为一个8位单通道图像(灰度图像),非零像素被视为1,零像素保持为0。可以使用其他OpenCV函数(例如
    compareinRangethresholdadaptiveThresholdCanny
    等)将灰度图像或彩色图像转换为二值图像。如果mode参数等于RETR_CCOMPRETR_FLOODFILL,则输入也可以是一个32位整数标签图像(CV_32SC1)。

  2. mode: 轮廓检索模式,控制轮廓的层次关系。有几种模式可选,常见的有 RETR_EXTERNAL(只检测最外面的轮廓)、RETR_LIST(检测所有的轮廓,不建立层次关系)、RETR_CCOMP(检测所有轮廓,但只保留两个层次的轮廓信息)和 RETR_TREE(检测所有轮廓,保留完整的层次信息)。

  3. method: 轮廓逼近方法,控制轮廓的表示精度。有几种方法可选,常见的有 CHAIN_APPROX_SIMPLE(压缩水平、垂直和对角方向的轮廓,只保留其端点)、CHAIN_APPROX_TC89_L1CHAIN_APPROX_TC89_KCOS

  4. contours: 输出参数,用于存储检测到的轮廓。每个轮廓以一组点的形式存储,例如 std::vector<std::vector<cv::Point>>

  5. hierarchy: 输出参数,可选,用于存储图像拓扑结构的信息。对于每个轮廓,hierarchy 中的一个元素是一个包含四个整数的数组,分别表示在同一层次上的下一个轮廓、上一个轮廓、第一个子轮廓和父轮廓的索引。如果某个轮廓在相应的方向上没有下一个、上一个、子轮廓或父轮廓,则对应的索引将为负数。

  6. offset: 可选参数,是一个偏移量,用于将每个轮廓点进行偏移。这在从图像ROI提取轮廓后,需要在整个图像上进行分析时很有用。

findContours 函数的主要作用是在给定的二值图像中查找对象的轮廓。它使用Suzuki算法进行轮廓检测,并返回检测到的轮廓,以及可选的图像拓扑结构信息。轮廓是用一组点表示的,这些点描述了对象的边界。这个函数在形状分析、对象检测和识别等领域中非常有用。在检测到轮廓后,你可以进一步进行轮廓的绘制、分析、过滤或者在原图像上进行标记等操作。

1.4.2 轮廓面积

轮廓面积在图像处理中具有广泛的应用。通过计算对象的轮廓面积,我们可以进行目标识别、大小过滤和形状分析。例如,在目标检测中,我们可以通过设定一定的面积阈值来排除过小或过大的轮廓,从而过滤掉不感兴趣的区域。
在OpenCV中,我们可以使用cv2.contourArea(contour)函数来计算轮廓的面积,其中contour是轮廓的点集。

下面是一个简单的代码示例,演示如何使用OpenCV计算轮廓的面积:

# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 进行阈值处理
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 寻找轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 计算每个轮廓的面积并输出
for contour in contours:area = cv2.contourArea(contour)print(f"Contour Area: {area} pixels")

contourArea函数是OpenCV中用于计算轮廓面积的函数。以下是该函数的参数和功能的简要说明:

def contourArea(contour, oriented=None)

参数:

  • contour: 输入参数,表示轮廓的点集,通常是一个包含2D点(轮廓顶点)的向量。
  • oriented: 可选参数,表示是否计算有向面积。如果设置为True,函数将返回一个带有方向的面积值,具体取决于轮廓的方向(顺时针或逆时针)。默认值为False,即返回面积的绝对值。

功能:

  • 该函数计算轮廓的面积,使用的是Green公式。
  • 返回的面积值可能与使用drawContoursfillPoly绘制轮廓时得到的非零像素数不同。这是因为Green公式计算的是理论上的面积,而绘制轮廓时计算的是像素的数量。
  • 注意,对于具有自交点的轮廓,该函数可能给出错误的结果。

1.4.3 轮廓周长

轮廓周长是形状描述的一个重要特征,特别在目标检测和边缘检测中经常被用到。通过计算周长,我们可以获取有关轮廓的详细信息,例如对象的形状复杂程度。这对于区分不同形状的目标或者进行形状分析非常有帮助。

轮廓周长是指轮廓的闭合曲线的长度。在OpenCV中,我们可以使用cv2.arcLength(curve, closed)函数来计算轮廓的周长,其中curve是轮廓的点集,而closed是一个标志,指示轮廓是否闭合。

下面是一个简单的代码示例,演示如何使用OpenCV计算轮廓的周长:

# 计算每个轮廓的周长并输出
for contour in contours:perimeter = cv2.arcLength(contour, True)print(f"Contour Perimeter: {perimeter}")

arcLength函数是OpenCV中用于计算轮廓周长的函数。以下是该函数的参数和功能的简要说明:

def arcLength(curve, closed)

参数:

  • curve: 输入参数,表示轮廓的点集,通常是一个包含2D点(轮廓顶点)的向量。
  • closed: 标志参数,指示轮廓是否闭合。如果轮廓是封闭的,则为True;否则为False。

功能:

  • 该函数计算轮廓的周长或者曲线的长度。
  • 如果closed参数为True,函数将计算封闭轮廓的周长;如果为False,则计算曲线的长度。

1.4.4 轮廓外接多边形

轮廓外接多边形提供了一种简单但有效的方式来描述和表示目标的形状。这种方法对于快速计算目标的边界框以及后续的目标跟踪和分析非常有用。通过绘制外接矩形,我们可以更直观地了解目标的位置和大小。

轮廓外接多边形是指能够完全包围轮廓的最小矩形,通常是一个矩形框。在OpenCV中,我们可以使用cv2.boundingRect(points)函数来计算轮廓的外接矩形,其中points是轮廓的点集。

下面是一个简单的代码示例,演示如何使用OpenCV计算轮廓的外接矩形并在图像上绘制矩形:

# 外接矩形
for contour in contours:x, y, w, h = cv2.boundingRect(contour)cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)

boundingRect函数是OpenCV中用于计算轮廓外接多边形的函数。以下是该函数的参数和功能的简要说明:

def boundingRect(points)

参数:

  • points: 输入参数,表示轮廓的点集,通常是一个包含2D点(轮廓顶点)的向量。

功能:

  • 该函数计算并返回指定点集的最小外接矩形,即能够完全包围点集的矩形框。

1.4.5 点到轮廓距离

点到轮廓的距离是进行形状分析和目标识别的关键步骤之一。通过计算点到轮廓的距离,我们可以判断一个点是否属于某个目标,以及该点相对于目标的具体位置。这对于许多应用场景,如手势识别、物体定位等都是至关重要的。

点到轮廓的距离是指一个给定点到轮廓的最短距离,这可以帮助我们确定点相对于轮廓的位置关系,是在轮廓内部、外部还是在轮廓上。

下面是一个简单的代码示例,演示如何使用OpenCV的pointPolygonTest函数计算点到轮廓的最短距离:

# 计算点到轮廓的最短距离
point = (100, 100)
for contour in contours:distance = cv2.pointPolygonTest(contour, point, True)print(f'Distance from point to contour: {distance}')

pointPolygonTest函数是OpenCV中用于计算点到轮廓距离的函数。以下是该函数的参数和功能的简要说明:

def pointPolygonTest(contour, pt, measureDist)

参数:

  • contour: 输入参数,表示轮廓的点集,通常是一个包含2D点(轮廓顶点)的向量,可以存储在std::vectorMat中。
  • pt: 输入参数,表示测试点的坐标。
  • measureDist: 输入参数,表示是否测量点到轮廓的距离。如果为True,函数返回点到轮廓的有向距离;如果为False,函数仅检查点相对于轮廓的位置关系,返回+1、-1或0。

功能:

  • 该函数用于执行点在轮廓内的测试。
  • 返回正值表示点在轮廓内部,负值表示点在轮廓外部,零值表示点在轮廓上或与轮廓上的顶点重合。
  • measureDist为True时,返回值为点到最近轮廓边缘的有向距离。

1.4.6 凸包检测

凸包检测在图像处理和计算机视觉中广泛应用,特别是在目标检测和形状分析中。通过计算凸包,我们可以更好地理解目标的整体形状,从而帮助进行目标识别和分析。

凸包检测是寻找一个点集的最小凸多边形的过程。在OpenCV中,我们可以使用cv2.convexHull(points)函数来计算给定点集的凸包。

下面是一个简单的代码示例,演示如何使用OpenCV的convexHull函数进行凸包检测并在图像上绘制凸包:

# 凸包检测
for contour in contours:hull = cv2.convexHull(contour)cv2.drawContours(image, [hull], 0, (0, 0, 255), 2)

convexHull函数是OpenCV中用于凸包检测的函数。以下是该函数的参数和功能的简要说明:

def convexHull(points, hull=None, clockwise=None, returnPoints=None)

参数:

  • points: 输入参数,表示点集的2D坐标,通常是一个包含2D点的向量,可以存储在std::vectorMat中。
  • hull: 输出参数,表示凸包的点集或索引。可以是一个整数向量,表示凸包点在原始点集中的索引;也可以是一个包含凸包点的向量,表示凸包的实际坐标点。
  • clockwise: 可选参数,表示凸包的方向。如果为True,表示凸包的方向是顺时针的;如果为False,表示凸包的方向是逆时针的。
  • returnPoints: 可选参数,操作标志。如果为True,函数返回凸包的实际坐标点;如果为False,函数返回凸包点在原始点集中的索引。

功能:

  • 该函数用于找到给定点集的凸包。
  • 函数返回凸包的点集或索引,取决于returnPoints参数的设置。
  • 可以选择指定凸包的方向是顺时针还是逆时针。

1.5 轮廓检测代码实现

首先,我们创建一个空白的图像,然后定义了两个函数,一个用于绘制随机椭圆,另一个用于检查新椭圆是否与已存在的椭圆重叠。

# 创建一个空白的图像
width, height = 800, 800
image = np.zeros((height, width, 3), dtype=np.uint8)# 定义绘制椭圆的函数
def draw_random_ellipse(img):# 生成随机椭圆的参数# ...# 定义检查椭圆是否重叠的函数
def check_overlap(new_ellipse, existing_ellipses):# ...

接下来,通过调用draw_random_ellipse函数生成一组不重叠的椭圆,并将它们绘制在图像上。

# 生成不重叠的椭圆
num_ellipses = 15
ellipses = []
for _ in range(num_ellipses):while True:new_ellipse = ((random.randint(0, width - 1), random.randint(0, height - 1)),(random.randint(10, 100), random.randint(10, 100)),random.randint(0, 360),(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))if not check_overlap(new_ellipse, ellipses):ellipses.append(new_ellipse)break# 绘制不重叠的椭圆
for ellipse in ellipses:cv2.ellipse(image, ellipse[0], ellipse[1], ellipse[2], 0, 360, ellipse[3], -1)

然后,我们定义了两个参数字典,用于设置绘制文本的共享参数。

# 共享的参数
shared_params = {"fontFace": cv2.FONT_HERSHEY_SIMPLEX,"fontScale": 0.5,"thickness": 2,"color": (0, 0, 0),"lineType": cv2.LINE_AA,
}
shared_params2 = {"fontFace": cv2.FONT_HERSHEY_SIMPLEX,"fontScale": 0.5,"thickness": 1,"color": (0, 255, 0),"lineType": cv2.LINE_AA,
}

接下来,将图像转换为灰度图,并使用Canny边缘检测。

# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 使用Canny边缘检测
edges = cv2.Canny(gray, 10, 150)

通过调用cv2.findContours函数,找到图像中的轮廓。

# 查找轮廓
_, contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

然后,对每个轮廓进行一系列操作,包括计算轮廓面积、周长、中心位置,绘制外接矩形和最短距离的线段。

# 在原图上绘制轮廓并输出面积和周长信息
for contour in contours:# 计算轮廓面积和周长area = int(cv2.contourArea(contour))perimeter = int(cv2.arcLength(contour, True))moments = cv2.moments(contour)# ...

最后,对每个轮廓进行凸包检测,并在图像上绘制凸包。

    # 凸包检测hull = cv2.convexHull(contour)cv2.drawContours(image, [hull], 0, (0, 0, 255), 1)

最终,显示包含轮廓信息的图像。

下面是完整的代码内容:

import cv2
import numpy as np
import random# 创建一个空白的图像
width, height = 800, 800
image = np.zeros((height, width, 3), dtype=np.uint8)# 定义绘制椭圆的函数
def draw_random_ellipse(img):# 生成随机椭圆的参数center = (random.randint(0, width - 1), random.randint(0, height - 1))axes = (random.randint(10, 100), random.randint(10, 100))angle = random.randint(0, 360)color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))# 绘制椭圆cv2.ellipse(img, center, axes, angle, 0, 360, color, -1)# 定义检查椭圆是否重叠的函数
def check_overlap(new_ellipse, existing_ellipses):for existing_ellipse in existing_ellipses:# 获取椭圆的掩码new_mask = np.zeros_like(image, dtype=np.uint8)existing_mask = np.zeros_like(image, dtype=np.uint8)cv2.ellipse(new_mask, new_ellipse[0], new_ellipse[1], new_ellipse[2], 0, 360, (255, 255, 255), -1)cv2.ellipse(existing_mask, existing_ellipse[0], existing_ellipse[1], existing_ellipse[2], 0, 360,(255, 255, 255), -1)# 检查是否有重叠的部分overlap = cv2.bitwise_and(new_mask, existing_mask)if np.sum(overlap) > 0:return Truereturn False# 生成不重叠的椭圆
num_ellipses = 15
ellipses = []
for _ in range(num_ellipses):while True:new_ellipse = ((random.randint(0, width - 1), random.randint(0, height - 1)),(random.randint(10, 100), random.randint(10, 100)),random.randint(0, 360),(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))if not check_overlap(new_ellipse, ellipses):ellipses.append(new_ellipse)break# 绘制不重叠的椭圆
for ellipse in ellipses:cv2.ellipse(image, ellipse[0], ellipse[1], ellipse[2], 0, 360, ellipse[3], -1)# 共享的参数
shared_params = {"fontFace": cv2.FONT_HERSHEY_SIMPLEX,"fontScale": 0.5,"thickness": 2,"color": (0, 0, 0),"lineType": cv2.LINE_AA,
}
shared_params2 = {"fontFace": cv2.FONT_HERSHEY_SIMPLEX,"fontScale": 0.5,"thickness": 1,"color": (0, 255, 0),"lineType": cv2.LINE_AA,
}# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 使用Canny边缘检测
edges = cv2.Canny(gray, 10, 150)# 查找轮廓
_, contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 在原图上绘制轮廓
cv2.drawContours(image, contours, -1, (0, 255, 0), 2)# 在原图上绘制轮廓并输出面积和周长信息
for contour in contours:# 计算轮廓面积area = int(cv2.contourArea(contour))# 计算轮廓周长perimeter = int(cv2.arcLength(contour, True))moments = cv2.moments(contour)# 检查 moments['m00'](轮廓的面积) 是否为零if moments['m00'] != 0:# 计算轮廓的中心位置cx = int(moments['m10'] / moments['m00'])cy = int(moments['m01'] / moments['m00'])# 在中心位置绘制一个点cv2.circle(image, (cx, cy), 5, (255, 255, 255), -1)cv2.circle(image, (cx, cy), 3, (0, 0, 255), -1)# 外接矩形x, y, w, h = cv2.boundingRect(contour)cv2.rectangle(image, (x, y), (x + w, y + h), (255, 255, 0), 1)# 提取每个点的坐标,将contour转换为二维数组contour_2d = contour[:, 0, :]# 计算每个点到目标点的距离的平方和distances = np.sum((contour_2d - np.array([cx, cy])) ** 2, axis=1)# 找到最小值的索引closest_point_index = np.argmin(distances)closest_point = contour[closest_point_index][0]# 在图中画出这个最短距离的线段cv2.line(image, (cx, cy), (int(closest_point[0]), int(closest_point[1])), (0, 0, 255), 3)cv2.line(image, (cx, cy), (int(closest_point[0]), int(closest_point[1])), (255, 0, 0), 1)# 输出计算结果cv2.putText(image, f"S= {area}", (max(int(cx - 20), 0), cy + 20), **shared_params)cv2.putText(image, f"C= {perimeter}", (max(int(cx - 20), 0), cy + 35), **shared_params)cv2.putText(image, f"S= {area}", (max(int(cx - 20), 0), cy + 20), **shared_params2)cv2.putText(image, f"C= {perimeter}", (max(int(cx - 20), 0), cy + 35), **shared_params2)# 计算每个中心点到轮廓的最短距离dist = abs(int(cv2.pointPolygonTest(contour, (cx, cy), True)))cv2.putText(image, f"D_min= {dist}", (max(int(cx - 20), 0), cy + 50), **shared_params)cv2.putText(image, f"D_min= {dist}", (max(int(cx - 20), 0), cy + 50), **shared_params2)# 凸包检测hull = cv2.convexHull(contour)cv2.drawContours(image, [hull], 0, (0, 0, 255), 1)# 显示结果
cv2.imshow('Contours', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Contours

二、轮廓的距

在目标检测中,轮廓不仅仅是一个形状的抽象表示,还包含了有关形状的重要信息,其中之一就是轮廓的距。距是用于描述轮廓形状和结构的一种度量,它可以分为几何距、中心距和Hu距。

2.1 几何距

几何距是通过计算轮廓到某个点的距离的方式来度量轮廓的形状。常见的几何距包括轮廓的面积、周长等,它们提供了关于轮廓整体尺寸的信息。
以下是几何距的一些常见计算公式:

  1. 轮廓面积(Area): 轮廓包围的区域的面积。

Area = ∫ ∫ D 1 d x d y \text{Area} = \int\int_D 1 \,dx\,dy Area=D1dxdy

其中 D D D 是轮廓包围的区域。

  1. 轮廓周长(Perimeter): 轮廓的周长,即轮廓上所有点到一个参考点的距离之和。

Perimeter = ∮ C d s \text{Perimeter} = \oint_C ds Perimeter=Cds

其中 C C C 是轮廓的曲线, d s ds ds 是轮廓上一点到下一点的弧长元素。

这些几何距的计算提供了有关轮廓形状和结构的基本信息,是轮廓分析中的重要工具。在使用OpenCV进行轮廓分析时,可以利用cv2.contourArea()cv2.arcLength()函数分别计算轮廓的面积和周长。

2.2 中心距

中心距是通过计算轮廓中心到轮廓上所有点的距离来度量轮廓形状的一种距离。中心距包括一阶中心距、二阶中心距等,它们对于形状的平移和旋转具有不变性。以下是中心距的一些常见计算公式:

  1. 一阶中心距(m_10 和 m_01): 描述形状的平移。

m 10 = ∫ ∫ D x d x d y m_{10} = \int\int_D x \,dx\,dy m10=Dxdxdy
m 01 = ∫ ∫ D y d x d y m_{01} = \int\int_D y \,dx\,dy m01=Dydxdy

其中 D D D 是轮廓包围的区域, x x x y y y 是图像坐标。

  1. 二阶中心距(m_20、m_02 和 m_11): 描述形状的旋转。

m 20 = ∫ ∫ D ( x − x ˉ ) 2 d x d y m_{20} = \int\int_D (x - \bar{x})^2 \,dx\,dy m20=D(xxˉ)2dxdy
m 02 = ∫ ∫ D ( y − y ˉ ) 2 d x d y m_{02} = \int\int_D (y - \bar{y})^2 \,dx\,dy m02=D(yyˉ)2dxdy
m 11 = ∫ ∫ D ( x − x ˉ ) ( y − y ˉ ) d x d y m_{11} = \int\int_D (x - \bar{x})(y - \bar{y}) \,dx\,dy m11=D(xxˉ)(yyˉ)dxdy

其中 x ˉ \bar{x} xˉ y ˉ \bar{y} yˉ 是轮廓的质心坐标。

在OpenCV中,可以使用cv2.moments()函数计算轮廓的矩,进而得到一阶中心距和二阶中心距。给定一个轮廓,该函数返回一个字典,其中包含轮廓的一些矩的信息,如质心、面积等。
这个函数的语法如下:

moments = cv2.moments(contour)

其中,contour是输入的轮廓,而moments是包含轮廓矩信息的字典。

返回的字典包含以下键值对:

  • 'm00': 轮廓的面积。
  • 'm10', 'm01': 分别是x和y方向上的一阶矩。
  • 'm20', 'm02', 'm11': 分别是x和y方向上的二阶矩和xy方向上的一阶矩。
  • 'm30', 'm03', 'm21', 'm12': 分别是x和y方向上的三阶矩,xy方向上的二阶矩和一阶矩。
  • 'mu20', 'mu02', 'mu11': 中心矩,是二阶矩关于质心的矩。
  • 'mu30', 'mu03', 'mu21', 'mu12': 中心矩,是三阶矩关于质心的矩。
  • 'nu20', 'nu02', 'nu11': 归一化中心矩,是中心矩除以面积的二阶矩。
  • 'nu30', 'nu03', 'nu21', 'nu12': 归一化中心矩,是中心矩除以面积的三阶矩。

2.3 Hu距

Hu距是一种通过中心距来构建的轮廓描述符,具有平移、旋转和缩放不变性。Hu距是一组七个独立的距离,通过对中心距的组合计算而得。以下是Hu距的计算公式:

  1. Hu距 1-7:
    Hu1 = η 20 + η 02 Hu2 = ( η 20 − η 02 ) 2 + 4 η 11 2 Hu3 = ( η 30 − 3 η 12 ) 2 + ( 3 η 21 − η 03 ) 2 Hu4 = ( η 30 + η 12 ) 2 + ( η 21 + η 03 ) 2 Hu5 = ( η 30 − 3 η 12 ) ( η 30 + η 12 ) [ ( η 30 + η 12 ) 2 − 3 ( η 21 + η 03 ) 2 ] Hu6 = ( η 20 − η 02 ) [ ( η 30 + η 12 ) 2 − ( η 21 + η 03 ) 2 ] + 4 η 11 ( η 30 + η 12 ) ( η 21 + η 03 ) Hu7 = ( 3 η 21 − η 03 ) ( η 30 + η 12 ) [ ( η 30 + η 12 ) 2 − 3 ( η 21 + η 03 ) 2 ] − ( η 30 − 3 η 12 ) ( η 21 + η 03 ) [ 3 ( η 30 + η 12 ) 2 − ( η 21 + η 03 ) 2 ] \begin{split} & \text{Hu1} = \eta_{20} + \eta_{02} \\ & \text{Hu2} = (\eta_{20} - \eta_{02})^2 + 4\eta_{11}^2 \\ & \text{Hu3} = (\eta_{30} - 3\eta_{12})^2 + (3\eta_{21} - \eta_{03})^2 \\ & \text{Hu4} = (\eta_{30} + \eta_{12})^2 + (\eta_{21} + \eta_{03})^2 \\ & \text{Hu5} = (\eta_{30} - 3\eta_{12})(\eta_{30} + \eta_{12})[(\eta_{30} + \eta_{12})^2 - 3(\eta_{21} + \eta_{03})^2] \\ & \text{Hu6} = (\eta_{20} - \eta_{02})[(\eta_{30} + \eta_{12})^2 - (\eta_{21} + \eta_{03})^2] + 4\eta_{11}(\eta_{30} + \eta_{12})(\eta_{21} + \eta_{03}) \\ & \text{Hu7} = (3\eta_{21} - \eta_{03})(\eta_{30} + \eta_{12})[(\eta_{30} + \eta_{12})^2 - 3(\eta_{21} + \eta_{03})^2] - (\eta_{30} - 3\eta_{12})(\eta_{21} + \eta_{03})[3(\eta_{30} + \eta_{12})^2 - (\eta_{21} + \eta_{03})^2] \\ \end{split} Hu1=η20+η02Hu2=(η20η02)2+4η112Hu3=(η303η12)2+(3η21η03)2Hu4=(η30+η12)2+(η21+η03)2Hu5=(η303η12)(η30+η12)[(η30+η12)23(η21+η03)2]Hu6=(η20η02)[(η30+η12)2(η21+η03)2]+4η11(η30+η12)(η21+η03)Hu7=(3η21η03)(η30+η12)[(η30+η12)23(η21+η03)2](η303η12)(η21+η03)[3(η30+η12)2(η21+η03)2]

    其中 η p q \eta_{pq} ηpq 是归一化中心距,计算公式如下:

η p q = μ p q μ 00 1 + p + q 2 \eta_{pq} = \frac{\mu_{pq}}{\mu_{00}^{1+\frac{p+q}{2}}} ηpq=μ001+2p+qμpq

其中 μ p q \mu_{pq} μpq 是轮廓的中心距。

这七个Hu距对于图像的平移、旋转和缩放具有不变性,是图像识别和匹配中常用的特征描述符。在OpenCV中,可以使用cv2.HuMoments()函数计算Hu距。

2.4 代码实现

下面是使用OpenCV实现轮廓的距的代码示例:

import cv2
import numpy as np
import random# 创建一个空白的图像
width, height = 800, 800
image = np.zeros((height, width, 3), dtype=np.uint8)# 定义绘制椭圆的函数
def draw_random_ellipse(img):# 生成随机椭圆的参数center = (random.randint(0, width - 1), random.randint(0, height - 1))axes = (random.randint(10, 100), random.randint(10, 100))angle = random.randint(0, 360)color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))# 绘制椭圆cv2.ellipse(img, center, axes, angle, 0, 360, color, -1)# 定义检查椭圆是否重叠的函数
def check_overlap(new_ellipse, existing_ellipses):for existing_ellipse in existing_ellipses:# 获取椭圆的掩码new_mask = np.zeros_like(image, dtype=np.uint8)existing_mask = np.zeros_like(image, dtype=np.uint8)cv2.ellipse(new_mask, new_ellipse[0], new_ellipse[1], new_ellipse[2], 0, 360, (255, 255, 255), -1)cv2.ellipse(existing_mask, existing_ellipse[0], existing_ellipse[1], existing_ellipse[2], 0, 360,(255, 255, 255), -1)# 检查是否有重叠的部分overlap = cv2.bitwise_and(new_mask, existing_mask)if np.sum(overlap) > 0:return Truereturn False# 生成不重叠的椭圆
num_ellipses = 5
ellipses = []
for _ in range(num_ellipses):while True:new_ellipse = ((random.randint(0, width - 1), random.randint(0, height - 1)),(random.randint(10, 100), random.randint(10, 100)),random.randint(0, 360),(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))if not check_overlap(new_ellipse, ellipses):ellipses.append(new_ellipse)break# 绘制不重叠的椭圆
for ellipse in ellipses:cv2.ellipse(image, ellipse[0], ellipse[1], ellipse[2], 0, 360, ellipse[3], -1)# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 使用Canny边缘检测
edges = cv2.Canny(gray, 50, 150)# 查找轮廓
_, contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 假设contours是你的轮廓列表
for contour in contours:# 计算轮廓的面积和周长area = cv2.contourArea(contour)perimeter = cv2.arcLength(contour, True)print(f'轮廓面积: {area}, 周长: {perimeter}')# 计算轮廓的中心位置moments = cv2.moments(contour)cx = int(moments['m10'] / moments['m00'])cy = int(moments['m01'] / moments['m00'])print(f'中心位置: ({cx}, {cy})')# 计算二阶中心距mu = cv2.moments(contour)nu20 = mu['nu20']nu02 = mu['nu02']nu11 = mu['nu11']print(f'二阶中心距: nu20={nu20}, nu02={nu02}, nu11={nu11}')# 计算Hu距hu_moments = cv2.HuMoments(mu)print(f'Hu距: \n {hu_moments}')

这段代码演示了如何计算轮廓的面积、周长以及中心位置,并且计算了二阶中心距和Hu距。通过这些距离的计算,我们可以更全面地了解轮廓的形状特征,为目标检测提供更多信息。

三、点集拟合

在目标检测中,点集拟合是一项重要的任务,它通过数学模型来逼近一组离散的点,从而更好地理解和描述目标的形状。在OpenCV中,我们通常使用最小包围三角形、最小包围圆形等方法进行点集拟合。

最小包围三角形

最小包围三角形是将一组点拟合为一个最小的包围三角形,该三角形能够包含所有的离散点。

import cv2
import numpy as np
import random# 创建一个空白的图像
width, height = 800, 800
image = np.zeros((height, width, 3), dtype=np.uint8)# 定义绘制椭圆的函数
def draw_random_ellipse(img):# 生成随机椭圆的参数center = (random.randint(0, width - 1), random.randint(0, height - 1))axes = (random.randint(10, 100), random.randint(10, 100))angle = random.randint(0, 360)color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))# 绘制椭圆cv2.ellipse(img, center, axes, angle, 0, 360, color, -1)# 定义检查椭圆是否重叠的函数
def check_overlap(new_ellipse, existing_ellipses):for existing_ellipse in existing_ellipses:# 获取椭圆的掩码new_mask = np.zeros_like(image, dtype=np.uint8)existing_mask = np.zeros_like(image, dtype=np.uint8)cv2.ellipse(new_mask, new_ellipse[0], new_ellipse[1], new_ellipse[2], 0, 360, (255, 255, 255), -1)cv2.ellipse(existing_mask, existing_ellipse[0], existing_ellipse[1], existing_ellipse[2], 0, 360,(255, 255, 255), -1)# 检查是否有重叠的部分overlap = cv2.bitwise_and(new_mask, existing_mask)if np.sum(overlap) > 0:return Truereturn False# 生成不重叠的椭圆
num_ellipses = 15
ellipses = []
for _ in range(num_ellipses):while True:new_ellipse = ((random.randint(0, width - 1), random.randint(0, height - 1)),(random.randint(10, 100), random.randint(10, 100)),random.randint(0, 360),(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))if not check_overlap(new_ellipse, ellipses):ellipses.append(new_ellipse)break# 绘制不重叠的椭圆
for ellipse in ellipses:cv2.ellipse(image, ellipse[0], ellipse[1], ellipse[2], 0, 360, ellipse[3], -1)# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 使用Canny边缘检测
edges = cv2.Canny(gray, 10, 150)# 查找轮廓
_, contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 在原图上绘制轮廓并输出面积和周长信息
for contour in contours:# 寻找最小包围三角形retval, triangle = cv2.minEnclosingTriangle(contour)# 绘制最小包围三角形cv2.polylines(image, [np.int32(triangle)], isClosed=True, color=(0, 255, 0), thickness=2)# 计算最小包围三角形的中心坐标center = np.mean(triangle, axis=0, dtype=np.int32)# 将 retval 的值显示在三角形的中心font = cv2.FONT_HERSHEY_SIMPLEXcv2.putText(image, f"Area: {int(retval)}", tuple(center[0]), font, 0.8, (0, 0, 255), 3, cv2.LINE_AA)cv2.putText(image, f"Area: {int(retval)}", tuple(center[0]), font, 0.8, (0, 255, 0), 2, cv2.LINE_AA)# 显示结果
cv2.imshow('Contours', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Minimum Enclosing Triangle

最小包围圆形

最小包围圆形是将一组点拟合为一个最小的包围圆形,该圆形能够包含所有的离散点。

import cv2
import numpy as np
import random# 创建一个空白的图像
width, height = 800, 800
image = np.zeros((height, width, 3), dtype=np.uint8)# 定义绘制椭圆的函数
def draw_random_ellipse(img):# 生成随机椭圆的参数center = (random.randint(0, width - 1), random.randint(0, height - 1))axes = (random.randint(10, 100), random.randint(10, 100))angle = random.randint(0, 360)color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))# 绘制椭圆cv2.ellipse(img, center, axes, angle, 0, 360, color, -1)# 定义检查椭圆是否重叠的函数
def check_overlap(new_ellipse, existing_ellipses):for existing_ellipse in existing_ellipses:# 获取椭圆的掩码new_mask = np.zeros_like(image, dtype=np.uint8)existing_mask = np.zeros_like(image, dtype=np.uint8)cv2.ellipse(new_mask, new_ellipse[0], new_ellipse[1], new_ellipse[2], 0, 360, (255, 255, 255), -1)cv2.ellipse(existing_mask, existing_ellipse[0], existing_ellipse[1], existing_ellipse[2], 0, 360,(255, 255, 255), -1)# 检查是否有重叠的部分overlap = cv2.bitwise_and(new_mask, existing_mask)if np.sum(overlap) > 0:return Truereturn False# 生成不重叠的椭圆
num_ellipses = 15
ellipses = []
for _ in range(num_ellipses):while True:new_ellipse = ((random.randint(0, width - 1), random.randint(0, height - 1)),(random.randint(10, 100), random.randint(10, 100)),random.randint(0, 360),(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))if not check_overlap(new_ellipse, ellipses):ellipses.append(new_ellipse)break# 绘制不重叠的椭圆
for ellipse in ellipses:cv2.ellipse(image, ellipse[0], ellipse[1], ellipse[2], 0, 360, ellipse[3], -1)# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 使用Canny边缘检测
edges = cv2.Canny(gray, 10, 150)# 查找轮廓
_, contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 在原图上绘制轮廓并输出面积和周长信息
for contour in contours:# 寻找最小包围三角形(x, y), radius = cv2.minEnclosingCircle(contour)# 绘制一个圆cv2.circle(image, (int(x), int(y)), int(radius), (255, 255, 255), 2)# 显示结果
cv2.imshow('Minimum Enclosing Circle', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Minimum Enclosing Circle

四、二维码检测

我们使用qrcode库生成并解码二维码
首先,检查两个库

pip install --upgrade opencv-python==3.4.4.19
pip install qrcode[pil]

qrcode库的使用方式,这里不做深入探讨,但是需要注意的是:生成的二维码是PIL(Python Imaging Library)图像对象。在后续的代码中,我们需要将PIL图像转换为OpenCV图像。

其次,使用cv2.QRCodeDetector()进行解码,这是OpenCV的二维码解码器。在解码过程中,获取了二维码的信息、顶点坐标和直线形式的二维码。确保理解这些输出的含义,以便根据需要进行处理。

codeinfo, points, straight_qrcode = qr_detector.detectAndDecode(img_read)
import cv2
import qrcode
import numpy as np# 获取OpenCV版本信息
cv_version = cv2.__version__# 将版本字符串转换为数字列表
version_numbers = [int(num) for num in cv_version.split('.')]# 检查版本是否小于3.4.4
if version_numbers < [3, 4, 4]:print("OpenCV版本过低,请升级至3.4.4或更高版本。 pip install --upgrade opencv-python==3.4.4.19")# 随机生成一个二维码
data = "Hello, QR Code!"
qr = qrcode.QRCode(version=1,error_correction=qrcode.constants.ERROR_CORRECT_L,box_size=10,border=4,
)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color=( 0 , 0 , 0 ), back_color=( 255 , 255 , 255 ))# 将PIL图像转换为OpenCV格式
img_np = np.array(img)
img_read = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)# 使用cv2.QRCodeDetector()进行解码
qr_detector = cv2.QRCodeDetector()
# 解码
codeinfo, points, straight_qrcode = qr_detector.detectAndDecode(img_read)# 描绘轮廓
cv2.drawContours(img_read, [np.int32(points)], 0, (0, 0, 255), 2)
print("QR Code: %s" % codeinfo)
# 添加文字
cv2.putText(img_read, "QR Code:" + str(codeinfo), (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.imshow("QR Code", img_read)cv2.waitKey(0)
cv2.destroyAllWindows()

QR Code


总结

首先,我们简单介绍了图像轮廓的概念,以及常用的轮廓检测算法。
接着,我们了解了轮廓检测函数,包括轮廓发现、轮廓面积、轮廓周长等功能的使用说明。代码实现部分提供了实际的操作指南,使其能够在实际项目中灵活运用所学知识。
然后,我们学习了轮廓的距,包括几何距、中心距和Hu距等,文章提供了更深层次的图像特征描述方法。
最后,通过点集拟合和二维码检测两个具体案例,我们展示了如何在实际应用中灵活运用轮廓检测技术,更好地理解其实际应用场景。

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

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

相关文章

ssh远程连接不了虚拟机ubuntu

直奔主题 1. 确保linux安装了ssh2.查看网络适配器是否启用3.连接成功 1. 确保linux安装了ssh sudo apt-get install openssh-server2.查看网络适配器是否启用 3.连接成功

VBA技术资料MF85:将工作簿批量另存为PDF文件

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

如何实现车机体验”遥遥领先”?头部玩家已经给出答案

车机与手机的深度融合&#xff0c;通过跨终端互联互通实现全场景、沉浸式的用户体验&#xff0c;正在成为各大高端智能汽车品牌的新战场。 此前&#xff0c;已经有华为、苹果几大手机巨头已经纷纷开启“造车”业务&#xff0c;同时吉利等车企也反向进入手机领域&#xff0c;各…

TransmittableThreadLocal - 线程池中也可以传递参数了

一、InheritableThreadLocal的不足 InheritableThreadLocal可以用于主子线程之间传递参数&#xff0c;但是它必须要求在主线程中手动创建的子线程才可以获取到主线程设置的参数&#xff0c;不能够通过线程池的方式调用。 但是现在我们实际的项目开发中&#xff0c;一般都是采…

快来看看你的手表是否有忘记关闭的应用在后台偷偷耗电?

在这个智能化的时代&#xff0c;手表已经不仅仅是用来看时间的工具&#xff0c;它更是我们生活的助手&#xff0c;从消息提醒到健康监测&#xff0c;它似乎无所不能。但与此同时&#xff0c;你是否注意到手表电量的续航有时长&#xff0c;有时慢&#xff1f;有可能&#xff0c;…

C语言每日一题(33)随机链表的复制

力扣138 随机链表的复制 题目描述 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都…

解锁OpenAI潜力:OpenAI 全面解析与最佳实践

该项目是由OpenAI公司提供的一个大型代码库&#xff0c;其中包含了各类与OpenAI API相关的代码示例和最佳实践。 此项目名为OpenAI Cookbook&#xff0c;目的是为了帮助使用者更有效地利用OpenAI API&#xff0c;将其应用于自己的工作和生活中。具体来说&#xff0c;可以解决一…

打码平台之图鉴的使用步骤

打码平台之图鉴 背景&#xff1a; ​ 今天给大家推荐一个我一直使用的验证码识别平台&#xff0c;图鉴&#xff0c;我没有收费&#xff0c;我只是觉得这个网站使用方便&#xff0c;支持验证码种类多&#xff0c;好了&#xff0c;话不多说&#xff0c;上教程&#xff01; 注册…

SmartX 超融合 5.1 版本有哪些新特性和技术提升?

近日&#xff0c;SmartX 正式发布了超融合产品组合 SmartX HCI 5.1 版本&#xff0c;以全面升级的超融合软件、分布式块存储、容器管理与服务、软件定义的网络与安全等组件&#xff0c;为虚拟化和容器负载在计算、存储、网络和管理层面提供统一的架构和生产级别的能力支持。本期…

2024年测试工程师必看文章系列之python+pytest接口自动化(1)-接口测试

一般我们所说的接口即API&#xff0c;那什么又是API呢&#xff0c;百度给的定义如下&#xff1a; API&#xff08;Application Programming Interface&#xff0c;应用程序接口&#xff09;是一些预先定义的接口&#xff08;如函数、HTTP接口&#xff09;&#xff0c;或指软件系…

如何在AIX操作系统上修改Java环境变量

AIX操作系统是IBM的Unix操作系统&#xff0c;通常用于企业级应用和服务器环境。在AIX上配置Java环境变量是执行Java应用程序和开发Java代码的重要步骤。本文将详细介绍如何在AIX上修改Java环境变量&#xff0c;并提供具体示例来帮助你完成这个任务。 步骤1&#xff1a;确定Java…

matlab如何实现任意长序列所有排列方式

最近被问到一个问题&#xff0c;如何计算一个由3个0和3个1组成的序列的所有组合情况&#xff0c;处理这个问题我没有找到特别恰当的函数&#xff08;如果有能直接做的函数欢迎评论告知&#xff09;&#xff0c;所以采用比较接近需求的perms函数来解决这个问题 首先看perms函数…

许战海战略文库|三步成就技术品牌:奥迪如何打造Quattro技术品牌?

引言&#xff1a;在当前全球化和信息化快速发展的背景下,技术品牌的打造不仅是企业竞争力提升的重要途径,也是企业实现长远发展的基石。技术品牌的建设并非一蹴而就的过程,而是需要企业准确把握市场趋势发掘自身核心竞争力,并通过长期的积累和推广逐渐在市场中树立起良好的技术…

视频监控管理平台EasyCVR告警查询拖动条无法显示,该如何解决?

视频汇聚/视频云存储/集中存储/视频监控管理平台EasyCVR能在复杂的网络环境中&#xff0c;将分散的各类视频资源进行统一汇聚、整合、集中管理&#xff0c;实现视频资源的鉴权管理、按需调阅、全网分发、云存储、智能分析等&#xff0c;视频智能分析平台EasyCVR融合性强、开放度…

JSON.toJSONString/JSONObject.toJSONString将实体类对象转换成JSON字符串时,多出了params字符串[记录贴]

我这里是给与了实体类一些固定的默认值&#xff0c;转换莫名其妙多出了params参数&#xff0c;回头深挖一下 **光看代码了 被偷了后方&#xff0c;忘记继承了还 ** 将实体类转换成JSON格式&#xff0c;三种写法都是一样的&#xff0c;内核都是阿里巴巴的 System.out.println(…

[Docker]七.配置 Docker 网络

一.Docker0 网络 1.多个容器之间如何通信,是否可以直接连接 默认启动的所有容器都会加入到docker0这个网络中,所有各个容器件是可以直接通信的 先看看网卡信息: 启动几个容器来演示一下: #启动mycentos这个容器 [rootlocalhost zph]# docker run -it -d --name mycentos d757…

OSG文字-各种文字效果(边框、阴影及颜色倾斜)示例(2)

各种文字效果(边框、阴影及颜色倾斜)示例 各种文字效果(边框、阴影及颜色倾斜)示例的代码如程序清单9-2所示&#xff1a; 1. /* 各种文字效果(边框、阴影及颜色倾斜)示例 */ 2. osg::ref_ptr<osg::Camera> createAllKindText(const string &strDataFolder) 3. {…

家政保洁预约小程序app开发特点有哪些?

家政预约服务小程序APP开发的特点介绍&#xff1b; 1. 低成本&#xff1a;用户通过手机APP下单&#xff0c;省去了中介费用&#xff0c;降低了雇主的雇佣成本。 2. 高收入&#xff1a;家政服务人员通过手机APP接单&#xff0c;省去了中介费用&#xff0c;从而提高了服务人员的…

京东优惠券查询API接口接入方案,item_search_coupon - 京东优惠券查询接口演示

要接入京东优惠券查询API接口&#xff08;item_search_coupon&#xff09;&#xff0c;您可以按照以下步骤进行操作&#xff1a; 注册并获取API密钥&#xff1a;首先&#xff0c;您需要在京东开放平台上注册并获取API密钥。这将为您提供唯一的标识符和密钥&#xff0c;用于访问…

图形编辑器开发:自定义光标管理

大家好&#xff0c;我是前端西瓜哥。 今天来讲讲如何在图形编辑器中使用自定义光标&#xff0c;并对光标其进行管理。 编辑器 github 地址&#xff1a; https://github.com/F-star/suika 线上体验&#xff1a; https://blog.fstars.wang/app/suika/ 自定义光标的意义是什么&am…