【完整源码+数据集+部署教程】遥感温室图像分割系统: yolov8-seg-slimneck
背景意义
研究背景与意义
随着全球气候变化的加剧,温室农业作为一种高效的农业生产方式,越来越受到重视。温室能够为作物提供一个相对稳定的生长环境,从而提高作物的产量和质量。然而,温室内的环境监测与管理仍然面临诸多挑战,尤其是在作物生长状态的实时监测和病虫害的早期识别方面。传统的人工监测方法不仅效率低下,而且容易受到主观因素的影响,难以满足现代农业的需求。因此,基于遥感技术的智能监测系统应运而生,成为提高温室管理效率的重要手段。
在这一背景下,图像分割技术作为计算机视觉领域的重要研究方向,逐渐被应用于温室农业的各个环节。图像分割能够将图像中的目标物体与背景进行有效区分,为后续的分析和处理提供基础数据。尤其是实例分割技术,能够对同一类别的多个实例进行分离,为温室内不同作物的生长状态监测提供了可能性。YOLO(You Only Look Once)系列模型因其高效的实时检测能力,已成为图像分割领域的重要工具。YOLOv8作为该系列的最新版本,进一步提升了检测精度和速度,适合于复杂环境下的图像处理任务。
本研究旨在基于改进的YOLOv8模型,构建一个高效的遥感温室图像分割系统。我们使用的实例分割数据集“instanceseg128”包含2800幅图像,专注于温室这一特定类别。通过对该数据集的深入分析,我们能够提取出温室内作物的生长特征和环境信息,从而为温室管理提供科学依据。数据集中仅包含一个类别(温室),使得模型的训练和测试更加集中,有助于提高分割的准确性和效率。
本研究的意义在于,通过改进YOLOv8模型,提升温室图像的分割精度,为智能农业的发展提供新的技术支持。首先,精准的图像分割能够为温室内作物的生长监测提供实时数据,帮助农民及时发现生长异常、病虫害等问题,从而采取相应的管理措施。其次,基于遥感技术的监测系统能够减少人工成本,提高工作效率,推动农业的智能化进程。此外,研究成果不仅可以应用于温室农业,还可推广至其他领域,如城市绿化、生态监测等,具有广泛的应用前景。
综上所述,基于改进YOLOv8的遥感温室图像分割系统的研究,不仅具有重要的理论价值,还有助于推动农业生产方式的转型升级,为实现可持续发展目标贡献力量。通过深入探索图像分割技术在温室农业中的应用,我们期待为智能农业的发展提供新的思路和解决方案。
图片效果
数据集信息
数据集信息展示
在本研究中,我们采用了名为“instanceseg128”的数据集,以训练和改进YOLOv8-seg的遥感温室图像分割系统。该数据集专注于遥感图像中的温室分割任务,具有重要的应用价值,尤其是在农业监测和资源管理领域。数据集的设计旨在提供高质量的标注数据,以支持深度学习模型在复杂环境中的表现。
“instanceseg128”数据集的类别数量为1,具体类别为“greenhouse”。这一类别的选择反映了研究的重点,即温室的检测与分割。温室作为现代农业的重要组成部分,其结构特征和生长环境对作物的生长有着直接影响。因此,准确识别和分割温室区域,不仅有助于实现精准农业,还能为后续的农业管理和决策提供数据支持。
数据集中的图像来源于不同的遥感平台,涵盖了多种气候条件和地理环境。这种多样性确保了模型在训练过程中能够学习到温室在不同背景下的特征,从而提高其泛化能力。每幅图像都经过精细的标注,确保温室的边界清晰可辨。这种高质量的标注对于训练深度学习模型至关重要,因为它直接影响到模型的学习效果和最终的分割精度。
在数据集的构建过程中,研究团队特别关注了图像的多样性和复杂性,以模拟实际应用中的各种情况。例如,数据集中包含了不同光照条件下的温室图像,涵盖了白天和夜晚的场景,确保模型能够在各种环境下稳定工作。此外,数据集还包括了不同类型的温室结构,如拱形温室和玻璃温室等,以增强模型的适应性。
为了进一步提高模型的性能,研究团队在数据预处理阶段采用了多种增强技术,如随机裁剪、旋转、翻转等。这些技术不仅增加了数据集的多样性,还有效提升了模型的鲁棒性,使其能够更好地应对现实世界中可能遇到的各种挑战。
在训练过程中,YOLOv8-seg模型将利用“instanceseg128”数据集进行多轮迭代,以优化其参数设置和网络结构。通过对比实验,研究团队将评估模型在不同训练阶段的表现,分析其在温室分割任务中的准确性和效率。最终,期望通过本研究实现对遥感温室图像的高效分割,为农业生产提供更为精准的监测手段。
综上所述,“instanceseg128”数据集不仅为本研究提供了丰富的训练数据,也为遥感图像处理领域的进一步探索奠定了基础。通过对该数据集的深入分析和应用,研究团队希望能够推动温室图像分割技术的发展,进而为现代农业的可持续发展贡献力量。
核心代码
以下是代码中最核心的部分,并附上详细的中文注释:
import cv2
import numpy as np
import torch
def imread(filename: str, flags: int = cv2.IMREAD_COLOR):
“”"
从文件中读取图像。
参数:filename (str): 要读取的文件路径。flags (int, optional): 标志,可以取 cv2.IMREAD_* 的值。默认为 cv2.IMREAD_COLOR。返回:(np.ndarray): 读取的图像。
"""
# 使用 cv2.imdecode 读取图像,np.fromfile 以支持多语言文件路径
return cv2.imdecode(np.fromfile(filename, np.uint8), flags)
def imwrite(filename: str, img: np.ndarray, params=None):
“”"
将图像写入文件。
参数:filename (str): 要写入的文件路径。img (np.ndarray): 要写入的图像。params (list of ints, optional): 额外参数。参见 OpenCV 文档。返回:(bool): 如果文件写入成功则返回 True,否则返回 False。
"""
try:# 使用 cv2.imencode 将图像编码并写入文件,支持多语言文件路径cv2.imencode(Path(filename).suffix, img, params)[1].tofile(filename)return True
except Exception:return False
def imshow(winname: str, mat: np.ndarray):
“”"
在指定窗口中显示图像。
参数:winname (str): 窗口名称。mat (np.ndarray): 要显示的图像。
"""
# 使用 cv2.imshow 显示图像,窗口名称进行编码以避免 Unicode 错误
cv2.imshow(winname.encode('unicode_escape').decode(), mat)
def torch_save(*args, **kwargs):
“”"
使用 dill(如果存在)序列化 lambda 函数,因为 pickle 无法处理这些函数。
参数:*args (tuple): 传递给 torch.save 的位置参数。**kwargs (dict): 传递给 torch.save 的关键字参数。
"""
try:import dill as pickle # 尝试导入 dill 模块
except ImportError:import pickle # 如果没有,则使用标准的 pickle 模块# 如果没有指定 pickle_module,则使用导入的 pickle
if 'pickle_module' not in kwargs:kwargs['pickle_module'] = pickle
return torch.save(*args, **kwargs) # 调用原始的 torch.save 函数
代码核心部分说明:
imread: 读取图像文件,支持多语言文件路径,返回图像的 NumPy 数组。
imwrite: 将图像写入文件,支持多语言文件路径,返回写入成功与否的布尔值。
imshow: 在指定窗口中显示图像,处理窗口名称以避免 Unicode 错误。
torch_save: 扩展 PyTorch 的保存功能,支持序列化 lambda 函数,确保兼容性。
这个程序文件 ultralytics/utils/patches.py 主要用于对现有功能进行更新和扩展,特别是与图像处理和PyTorch相关的功能。文件中包含了一些对OpenCV和PyTorch函数的“猴子补丁”,以增强其功能或修复潜在的问题。
首先,文件导入了必要的库,包括 Path(用于处理文件路径)、cv2(OpenCV库,用于图像处理)、numpy(用于数值计算)和torch(用于深度学习)。接下来,定义了一些函数来替代或扩展OpenCV和PyTorch的原始功能。
在OpenCV部分,定义了三个函数:
imread:用于从文件中读取图像。该函数接受文件名和读取标志作为参数,使用 cv2.imdecode 从文件中读取图像数据,并返回一个NumPy数组。此函数的实现确保了在读取图像时不会因为路径问题而导致错误。
imwrite:用于将图像写入文件。它接受文件名、图像数据和可选的参数列表。该函数使用 cv2.imencode 将图像编码为指定格式,并通过 tofile 方法将其写入文件。如果写入成功,返回True;否则返回False。
imshow:用于在指定窗口中显示图像。它接受窗口名称和图像数据作为参数。为了避免递归错误,使用了 _imshow 作为原始 cv2.imshow 的别名,并在显示窗口名称时进行了编码处理,以确保兼容多语言环境。
在PyTorch部分,定义了一个函数 torch_save,用于保存模型或数据。这个函数的目的是使用 dill 库(如果可用)来序列化一些 lambda 函数,因为 pickle 在处理这些函数时可能会遇到问题。函数首先尝试导入 dill,如果导入失败,则使用标准的 pickle。在调用原始的 torch.save 函数之前,确保将 pickle_module 参数设置为适当的模块。
总的来说,这个文件通过提供更灵活和可靠的图像读写功能以及增强的模型保存功能,提升了Ultralytics YOLO项目的实用性和兼容性。
11.4 ultralytics\models\yolo\classify_init_.py
导入Ultralytics YOLO库中的分类模块
该模块用于图像分类任务,包括预测、训练和验证功能
从ultralytics.models.yolo.classify.predict导入分类预测器
from ultralytics.models.yolo.classify.predict import ClassificationPredictor
从ultralytics.models.yolo.classify.train导入分类训练器
from ultralytics.models.yolo.classify.train import ClassificationTrainer
从ultralytics.models.yolo.classify.val导入分类验证器
from ultralytics.models.yolo.classify.val import ClassificationValidator
定义模块的公开接口,包含分类预测器、训练器和验证器
all = ‘ClassificationPredictor’, ‘ClassificationTrainer’, ‘ClassificationValidator’
代码核心部分解释:
导入模块:
代码导入了三个主要的类:ClassificationPredictor、ClassificationTrainer 和 ClassificationValidator,它们分别用于图像分类的预测、训练和验证。
all 声明:
all 是一个特殊变量,用于定义模块的公共接口。当使用 from module import * 时,只有在 all 中列出的名称会被导入。这有助于控制模块的可见性,避免不必要的名称冲突。
总结:
这段代码是Ultralytics YOLO库中与图像分类相关的核心功能的引入部分,主要用于分类任务的预测、训练和验证。
这个程序文件是Ultralytics YOLO模型的一部分,主要用于分类任务。文件的开头包含了版权信息,表明该代码遵循AGPL-3.0许可证。
接下来,文件通过from语句导入了三个重要的类:ClassificationPredictor、ClassificationTrainer和ClassificationValidator。这些类分别负责分类任务中的预测、训练和验证功能。具体来说,ClassificationPredictor用于进行分类预测,ClassificationTrainer用于训练分类模型,而ClassificationValidator则用于验证模型的性能。
最后,__all__变量定义了当使用from module import *语句时,哪些类会被导入。在这里,它包含了之前导入的三个类的名称,确保这些类可以被外部模块访问。
总的来说,这个文件的主要功能是组织和暴露YOLO模型中与分类相关的功能模块,使得其他部分的代码能够方便地使用这些功能。
11.5 ultralytics\models\utils\loss.py
以下是经过简化并添加详细中文注释的核心代码部分:
import torch
import torch.nn as nn
import torch.nn.functional as F
from ultralytics.utils.loss import FocalLoss, VarifocalLoss
from ultralytics.utils.metrics import bbox_iou
from .ops import HungarianMatcher
class DETRLoss(nn.Module):
“”"
DETR (DEtection TRansformer) 损失类。该类计算并返回DETR目标检测模型的不同损失组件。
包括分类损失、边界框损失、GIoU损失,以及可选的辅助损失。
“”"
def __init__(self, nc=80, loss_gain=None, aux_loss=True, use_fl=True, use_vfl=False):"""初始化DETR损失函数。参数:nc (int): 类别数量。loss_gain (dict): 各种损失组件的系数。aux_loss (bool): 是否计算辅助损失。use_fl (bool): 是否使用FocalLoss。use_vfl (bool): 是否使用VarifocalLoss。"""super().__init__()if loss_gain is None:loss_gain = {'class': 1, 'bbox': 5, 'giou': 2}self.nc = nc # 类别数量self.loss_gain = loss_gain # 损失系数self.aux_loss = aux_loss # 是否使用辅助损失self.fl = FocalLoss() if use_fl else None # Focal Loss对象self.vfl = VarifocalLoss() if use_vfl else None # Varifocal Loss对象self.matcher = HungarianMatcher(cost_gain={'class': 2, 'bbox': 5, 'giou': 2}) # 匹配器def _get_loss_class(self, pred_scores, targets, gt_scores, num_gts):"""计算分类损失。"""bs, nq = pred_scores.shape[:2] # 获取批次大小和查询数量one_hot = torch.zeros((bs, nq, self.nc + 1), dtype=torch.int64, device=targets.device)one_hot.scatter_(2, targets.unsqueeze(-1), 1) # 创建one-hot编码one_hot = one_hot[..., :-1] # 去掉最后一类(背景类)gt_scores = gt_scores.view(bs, nq, 1) * one_hot # 计算目标分数# 使用Focal Loss或Varifocal Loss计算损失if self.fl:if num_gts and self.vfl:loss_cls = self.vfl(pred_scores, gt_scores, one_hot)else:loss_cls = self.fl(pred_scores, one_hot.float())loss_cls /= max(num_gts, 1) / nq # 标准化损失else:loss_cls = nn.BCEWithLogitsLoss(reduction='none')(pred_scores, gt_scores).mean(1).sum() # 计算BCE损失return {'loss_class': loss_cls.squeeze() * self.loss_gain['class']} # 返回分类损失def _get_loss_bbox(self, pred_bboxes, gt_bboxes):"""计算边界框损失和GIoU损失。"""loss = {}if len(gt_bboxes) == 0: # 如果没有真实边界框loss['loss_bbox'] = torch.tensor(0., device=self.device)loss['loss_giou'] = torch.tensor(0., device=self.device)return loss# 计算L1损失loss['loss_bbox'] = self.loss_gain['bbox'] * F.l1_loss(pred_bboxes, gt_bboxes, reduction='sum') / len(gt_bboxes)# 计算GIoU损失loss['loss_giou'] = 1.0 - bbox_iou(pred_bboxes, gt_bboxes, xywh=True, GIoU=True)loss['loss_giou'] = loss['loss_giou'].sum() / len(gt_bboxes)loss['loss_giou'] *= self.loss_gain['giou']return loss # 返回边界框损失和GIoU损失def _get_loss(self, pred_bboxes, pred_scores, gt_bboxes, gt_cls):"""获取总损失。"""match_indices = self.matcher(pred_bboxes, pred_scores, gt_bboxes, gt_cls) # 计算匹配索引idx, gt_idx = self._get_index(match_indices) # 获取索引pred_bboxes, gt_bboxes = pred_bboxes[idx], gt_bboxes[gt_idx] # 根据索引获取预测和真实边界框# 创建目标张量bs, nq = pred_scores.shape[:2]targets = torch.full((bs, nq), self.nc, device=pred_scores.device, dtype=gt_cls.dtype)targets[idx] = gt_cls[gt_idx] # 更新目标张量gt_scores = torch.zeros([bs, nq], device=pred_scores.device)if len(gt_bboxes):gt_scores[idx] = bbox_iou(pred_bboxes.detach(), gt_bboxes, xywh=True).squeeze(-1) # 计算真实分数# 计算分类损失和边界框损失loss = {}loss.update(self._get_loss_class(pred_scores, targets, gt_scores, len(gt_bboxes)))loss.update(self._get_loss_bbox(pred_bboxes, gt_bboxes))return loss # 返回总损失def forward(self, pred_bboxes, pred_scores, batch):"""前向传播,计算损失。参数:pred_bboxes (torch.Tensor): 预测的边界框。pred_scores (torch.Tensor): 预测的分数。batch (dict): 包含真实信息的字典。返回:(dict): 包含总损失的字典。"""gt_cls, gt_bboxes = batch['cls'], batch['bboxes'] # 获取真实类别和边界框total_loss = self._get_loss(pred_bboxes[-1], pred_scores[-1], gt_bboxes, gt_cls) # 计算总损失return total_loss # 返回总损失
代码说明:
类定义:DETRLoss类用于计算DETR模型的损失,包括分类损失和边界框损失。
初始化方法:初始化类的属性,包括类别数量、损失系数、是否使用辅助损失等。
损失计算方法:
_get_loss_class:计算分类损失,使用Focal Loss或BCE损失。
_get_loss_bbox:计算边界框损失和GIoU损失。
_get_loss:综合计算分类损失和边界框损失。
前向传播方法:接收预测的边界框和分数,计算并返回总损失。
这个程序文件定义了一个用于目标检测模型的损失计算类,主要是针对DETR(DEtection TRansformer)模型的损失函数。文件中包含了两个主要的类:DETRLoss和RTDETRDetectionLoss。DETRLoss类负责计算DETR模型的不同损失组件,包括分类损失、边界框损失和GIoU损失,而RTDETRDetectionLoss类则扩展了DETRLoss,增加了对去噪训练损失的支持。
在DETRLoss类的构造函数中,定义了一些属性,包括类别数量、损失增益系数、是否计算辅助损失、是否使用Focal Loss和Varifocal Loss等。通过这些参数,用户可以灵活地配置损失计算的方式。
_get_loss_class方法计算分类损失。它通过将目标值转换为one-hot编码形式,并根据预测分数和真实分数计算损失。如果启用了Focal Loss或Varifocal Loss,则使用相应的损失函数进行计算。
_get_loss_bbox方法计算边界框损失和GIoU损失。它首先检查是否有真实边界框,如果没有,则返回零损失。然后计算L1损失和GIoU损失,并根据预定义的损失增益系数进行缩放。
_get_loss_aux方法用于计算辅助损失,它在每个解码器层上计算损失,并将其汇总。这个方法在计算过程中会考虑到使用的匹配索引。
_get_loss方法是核心损失计算函数,它整合了分类损失和边界框损失的计算。它首先通过匈牙利匹配算法获得匹配索引,然后根据这些索引提取预测的边界框和真实的边界框,并计算损失。
forward方法是类的入口,接收预测的边界框、分数和真实标签,调用_get_loss和_get_loss_aux方法计算总损失。
RTDETRDetectionLoss类继承自DETRLoss,在其forward方法中,除了计算标准的检测损失外,还检查是否提供了去噪的元数据,如果有,则计算去噪损失并将其添加到总损失中。
总的来说,这个文件实现了一个灵活且功能强大的损失计算模块,能够适应不同的目标检测任务需求,并支持去噪训练的扩展。
12.系统整体结构(节选)
程序整体功能和构架概括
该程序是一个基于Ultralytics YOLO模型的计算机视觉项目,主要用于目标检测和分类任务。程序通过多个模块和文件组织了不同的功能,包括视频处理、模型训练、损失计算和图像处理等。整体架构设计合理,各个模块之间通过导入和函数调用进行协作,使得用户能够方便地进行模型的训练、推理和评估。
视频处理:demo_test_video.py 文件负责读取视频流,利用YOLO模型进行目标检测,并在视频帧上绘制检测结果。
模型接口:ultralytics/models/yolo/init.py 文件定义了YOLO模型的公共接口,导入了分类、检测、姿态估计和分割等功能模块。
图像处理工具:ultralytics/utils/patches.py 文件提供了对OpenCV和PyTorch的一些增强功能,确保图像读写和模型保存的兼容性。
分类功能:ultralytics/models/yolo/classify/init.py 文件组织了与分类相关的功能,包括预测、训练和验证。
损失计算:ultralytics/models/utils/loss.py 文件实现了目标检测模型的损失计算,支持DETR模型的多种损失组件。
文件功能整理表
文件路径 功能描述
demo_test_video.py 处理视频流,利用YOLO模型进行目标检测并绘制结果。
ultralytics/models/yolo/init.py 定义YOLO模型的公共接口,导入分类、检测、姿态估计和分割功能模块。
ultralytics/utils/patches.py 提供对OpenCV和PyTorch的增强功能,确保图像读写和模型保存的兼容性。
ultralytics/models/yolo/classify/init.py 组织与分类相关的功能,包括分类预测、训练和验证。
ultralytics/models/utils/loss.py 实现目标检测模型的损失计算,支持DETR模型的多种损失组件。
通过这种模块化的设计,程序能够高效地处理不同的计算机视觉任务,便于扩展和维护。
13.图片、视频、摄像头图像分割Demo(去除WebUI)代码
在这个博客小节中,我们将讨论如何在不使用WebUI的情况下,实现图像分割模型的使用。本项目代码已经优化整合,方便用户将分割功能嵌入自己的项目中。 核心功能包括图片、视频、摄像头图像的分割,ROI区域的轮廓提取、类别分类、周长计算、面积计算、圆度计算以及颜色提取等。 这些功能提供了良好的二次开发基础。
核心代码解读
以下是主要代码片段,我们会为每一块代码进行详细的批注解释:
import random
import cv2
import numpy as np
from PIL import ImageFont, ImageDraw, Image
from hashlib import md5
from model import Web_Detector
from chinese_name_list import Label_list
根据名称生成颜色
def generate_color_based_on_name(name):
…
计算多边形面积
def calculate_polygon_area(points):
return cv2.contourArea(points.astype(np.float32))
…
绘制中文标签
def draw_with_chinese(image, text, position, font_size=20, color=(255, 0, 0)):
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(image_pil)
font = ImageFont.truetype(“simsun.ttc”, font_size, encoding=“unic”)
draw.text(position, text, font=font, fill=color)
return cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
动态调整参数
def adjust_parameter(image_size, base_size=1000):
max_size = max(image_size)
return max_size / base_size
绘制检测结果
def draw_detections(image, info, alpha=0.2):
name, bbox, conf, cls_id, mask = info[‘class_name’], info[‘bbox’], info[‘score’], info[‘class_id’], info[‘mask’]
adjust_param = adjust_parameter(image.shape[:2])
spacing = int(20 * adjust_param)
if mask is None:x1, y1, x2, y2 = bboxaim_frame_area = (x2 - x1) * (y2 - y1)cv2.rectangle(image, (x1, y1), (x2, y2), color=(0, 0, 255), thickness=int(3 * adjust_param))image = draw_with_chinese(image, name, (x1, y1 - int(30 * adjust_param)), font_size=int(35 * adjust_param))y_offset = int(50 * adjust_param) # 类别名称上方绘制,其下方留出空间
else:mask_points = np.concatenate(mask)aim_frame_area = calculate_polygon_area(mask_points)mask_color = generate_color_based_on_name(name)try:overlay = image.copy()cv2.fillPoly(overlay, [mask_points.astype(np.int32)], mask_color)image = cv2.addWeighted(overlay, 0.3, image, 0.7, 0)cv2.drawContours(image, [mask_points.astype(np.int32)], -1, (0, 0, 255), thickness=int(8 * adjust_param))# 计算面积、周长、圆度area = cv2.contourArea(mask_points.astype(np.int32))perimeter = cv2.arcLength(mask_points.astype(np.int32), True)......# 计算色彩mask = np.zeros(image.shape[:2], dtype=np.uint8)cv2.drawContours(mask, [mask_points.astype(np.int32)], -1, 255, -1)color_points = cv2.findNonZero(mask)......# 绘制类别名称x, y = np.min(mask_points, axis=0).astype(int)image = draw_with_chinese(image, name, (x, y - int(30 * adjust_param)), font_size=int(35 * adjust_param))y_offset = int(50 * adjust_param)# 绘制面积、周长、圆度和色彩值metrics = [("Area", area), ("Perimeter", perimeter), ("Circularity", circularity), ("Color", color_str)]for idx, (metric_name, metric_value) in enumerate(metrics):......return image, aim_frame_area
处理每帧图像
def process_frame(model, image):
pre_img = model.preprocess(image)
pred = model.predict(pre_img)
det = pred[0] if det is not None and len(det)
if det:
det_info = model.postprocess(pred)
for info in det_info:
image, _ = draw_detections(image, info)
return image
if name == “main”:
cls_name = Label_list
model = Web_Detector()
model.load_model(“./weights/yolov8s-seg.pt”)
# 摄像头实时处理
cap = cv2.VideoCapture(0)
while cap.isOpened():ret, frame = cap.read()if not ret:break......# 图片处理
image_path = './icon/OIP.jpg'
image = cv2.imread(image_path)
if image is not None:processed_image = process_frame(model, image)......# 视频处理
video_path = '' # 输入视频的路径
cap = cv2.VideoCapture(video_path)
while cap.isOpened():ret, frame = cap.read()......
源码文件
源码获取
欢迎大家点赞、收藏、关注、评论啦 、查看👇🏻获取联系方式👇🏻