python yolov8半自动标注

首先标注一部分图片,进行训练,生成模型,标注文件为xml方便后面统一做处理。
1、标注数据(文件为xml, 转为txt用于训练,保留xml标签文件)
2、模型训练(训练配置、训练代码、)
3、使用训练好的模型进行预标注 (生成标注文件 xml)
4、检测标注文件工具:
单类别拆分、
合并所有类别xml、
合并指定几个类别、
固定矩形位置增加类别与固定位置绘制矩形检测是否重合删除类别(增删类别)、
固定矩形位置生成xml、

1、标注数据

注意:标注使用voc在这里插入图片描述
这样标注文件是xml文件。
生成文件结构:放入当前目录下运行

import os

# 创建文件夹结构
# 创建单层目录,如果目录存在则报错
path = "./VOCdevkit"
os.mkdir(path)
# 创建多级目录,如果目录存在则报错
p1 = os.path.join(path, "VOC2007")
os.makedirs(p1)
p2 = os.path.join(p1, "Annotations")
os.makedirs(p2)
p3 = os.path.join(p1, "YOLOLabels")
os.makedirs(p3)

xml转txt标注文件:
将xml文件放入:\VOCdevkit\VOC2007\Annotations 文件夹中
将图片放入:\VOCdevkit\VOC2007\JPEGImages 文件夹中
可指定训练与测试的百分比

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
import random
from shutil import copyfile

classes = ['clascc1', 'class2']  # 注意与自己的类对应,对应,对应,不然转好的txt文件是空的
# classes=["ball"]

TRAIN_RATIO = 80  # 按自己的要求划分,这里代表是train:test=82

# 创建文件夹结构
# 创建单层目录,如果目录存在则报错
path = "./VOCdevkit"
os.mkdir(path)
# 创建多级目录,如果目录存在则报错
p1 = os.path.join(path, "VOC2007")
os.makedirs(p1)
p2 = os.path.join(p1, "Annotations")
os.makedirs(p2)
p3 = os.path.join(p1, "YOLOLabels")
os.makedirs(p3)


def clear_hidden_files(path):
    dir_list = os.listdir(path)
    for i in dir_list:
        abspath = os.path.join(os.path.abspath(path), i)
        if os.path.isfile(abspath):
            if i.startswith("._"):
                os.remove(abspath)
        else:
            clear_hidden_files(abspath)


def convert(size, box):
    dw = 1. / size[0]
    dh = 1. / size[1]
    x = (box[0] + box[1]) / 2.0 - 1
    y = (box[2] + box[3]) / 2.0 - 1
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return (x, y, w, h)


def convert_annotation(image_id):
    in_file = open('VOCdevkit/VOC2007/Annotations/%s.xml' % image_id, encoding='utf-8')
    out_file = open('VOCdevkit/VOC2007/YOLOLabels/%s.txt' % image_id, 'w', encoding='utf-8')
    tree = ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        # difficult = obj.find('difficult').text
        cls = obj.find('name').text
        # if cls not in classes or int(difficult) == 1:
        #     continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
             float(xmlbox.find('ymax').text))
        bb = convert((w, h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
    in_file.close()
    out_file.close()


wd = os.getcwd()
data_base_dir = os.path.join(wd, "VOCdevkit/")
if not os.path.isdir(data_base_dir):
    os.mkdir(data_base_dir)
work_sapce_dir = os.path.join(data_base_dir, "VOC2007/")
if not os.path.isdir(work_sapce_dir):
    os.mkdir(work_sapce_dir)
annotation_dir = os.path.join(work_sapce_dir, "Annotations/")
if not os.path.isdir(annotation_dir):
    os.mkdir(annotation_dir)
clear_hidden_files(annotation_dir)
image_dir = os.path.join(work_sapce_dir, "JPEGImages/")
if not os.path.isdir(image_dir):
    os.mkdir(image_dir)
clear_hidden_files(image_dir)
yolo_labels_dir = os.path.join(work_sapce_dir, "YOLOLabels/")
if not os.path.isdir(yolo_labels_dir):
    os.mkdir(yolo_labels_dir)
clear_hidden_files(yolo_labels_dir)
yolov5_images_dir = os.path.join(data_base_dir, "images/")
if not os.path.isdir(yolov5_images_dir):
    os.mkdir(yolov5_images_dir)
clear_hidden_files(yolov5_images_dir)
yolov5_labels_dir = os.path.join(data_base_dir, "labels/")
if not os.path.isdir(yolov5_labels_dir):
    os.mkdir(yolov5_labels_dir)
clear_hidden_files(yolov5_labels_dir)
yolov5_images_train_dir = os.path.join(yolov5_images_dir, "train/")
if not os.path.isdir(yolov5_images_train_dir):
    os.mkdir(yolov5_images_train_dir)
clear_hidden_files(yolov5_images_train_dir)
yolov5_images_test_dir = os.path.join(yolov5_images_dir, "val/")
if not os.path.isdir(yolov5_images_test_dir):
    os.mkdir(yolov5_images_test_dir)
clear_hidden_files(yolov5_images_test_dir)
yolov5_labels_train_dir = os.path.join(yolov5_labels_dir, "train/")
if not os.path.isdir(yolov5_labels_train_dir):
    os.mkdir(yolov5_labels_train_dir)
clear_hidden_files(yolov5_labels_train_dir)
yolov5_labels_test_dir = os.path.join(yolov5_labels_dir, "val/")
if not os.path.isdir(yolov5_labels_test_dir):
    os.mkdir(yolov5_labels_test_dir)
clear_hidden_files(yolov5_labels_test_dir)

train_file = open(os.path.join(wd, "yolov5_train.txt"), 'w')
test_file = open(os.path.join(wd, "yolov5_val.txt"), 'w')
train_file.close()
test_file.close()
train_file = open(os.path.join(wd, "yolov5_train.txt"), 'a')
test_file = open(os.path.join(wd, "yolov5_val.txt"), 'a')
list_imgs = os.listdir(image_dir)  # list image files
prob = random.randint(1, 100)
print("Probability: %d" % prob)
for i in range(0, len(list_imgs)):
    path = os.path.join(image_dir, list_imgs[i])
    if os.path.isfile(path):
        image_path = image_dir + list_imgs[i]
        voc_path = list_imgs[i]
        (nameWithoutExtention, extention) = os.path.splitext(os.path.basename(image_path))
        (voc_nameWithoutExtention, voc_extention) = os.path.splitext(os.path.basename(voc_path))
        annotation_name = nameWithoutExtention + '.xml'
        annotation_path = os.path.join(annotation_dir, annotation_name)
        label_name = nameWithoutExtention + '.txt'
        label_path = os.path.join(yolo_labels_dir, label_name)
    prob = random.randint(1, 100)
    print("Probability: %d" % prob)
    if (prob < TRAIN_RATIO):  # train dataset
        if os.path.exists(annotation_path):
            train_file.write(image_path + '\n')
            convert_annotation(nameWithoutExtention)  # convert label
            copyfile(image_path, yolov5_images_train_dir + voc_path)
            copyfile(label_path, yolov5_labels_train_dir + label_name)
    else:  # test dataset
        if os.path.exists(annotation_path):
            test_file.write(image_path + '\n')
            convert_annotation(nameWithoutExtention)  # convert label
            copyfile(image_path, yolov5_images_test_dir + voc_path)
            copyfile(label_path, yolov5_labels_test_dir + label_name)
train_file.close()
test_file.close()

2、模型训练

将下面文件结构复制到训练的路径(服务器)

yol;ov8 数据集文件结构:当前文件结构可以直接用于训练
在这里插入图片描述
训练配置:
配置图片的路径

train: /data/libowen/yolov8_1/ultralytics/data/VOCdevkit/images/train
val: /data/libowen/yolov8_1/ultralytics/data/VOCdevkit/images/val
# number of classes
nc: 2
# class names
names: ['class1', 'class2']

训练代码:

from ultralytics import YOLO

# Load a model
# model = YOLO("yolov8n.yaml")  # build a new model from YAML
# 目标检测 n s m l x 
model = YOLO("yolov8m.pt")  # load a pretrained model (recommended for training)
# 图像分类
# model = YOLO("yolov8n-cls.pt")  # load a pretrained model (recommended for training)
# model = YOLO("dataset.yaml").load("yolov8n.pt")  # build from YAML and transfer weights

# Train the model
results = model.train(data="dataset.yaml", epochs=40, imgsz=640)  # 40次 输入图像缩放大小640
# results = model.train(data="D:/yolo_/mu_biao_gen_zong/data", epochs=40, imgsz=640)

3、预标注–生成标注文件 xml

参数:读取图片的路径、保存xml的路径、需要标注的类别、需要标注的文件后缀、模型的路径+名称、图像的分辨率信息、

import cv2
from ultralytics import YOLO
import numpy as np
import time
import os
import xml.etree.ElementTree as ET
from xml.dom import minidom


def save_xml_add(root, lei_bie_name, xmin, ymin, xmax, ymax):
    object_elem1 = ET.SubElement(root, "object")
    ET.SubElement(object_elem1, "name").text = lei_bie_name
    bndbox1 = ET.SubElement(object_elem1, "bndbox")
    ET.SubElement(bndbox1, "xmin").text = str(xmin)
    ET.SubElement(bndbox1, "ymin").text = str(ymin)
    ET.SubElement(bndbox1, "xmax").text = str(xmax)
    ET.SubElement(bndbox1, "ymax").text = str(ymax)


def create_xml_annotation(image_path, coord1, xml_save_path, size_3):
    root = ET.Element("annotation")

    folder = os.path.dirname(image_path)
    ET.SubElement(root, "folder").text = folder

    filename = os.path.basename(image_path)
    ET.SubElement(root, "filename").text = filename

    size = ET.SubElement(root, "size")
    ET.SubElement(size, "width").text = size_3[0]
    ET.SubElement(size, "height").text = size_3[1]
    ET.SubElement(size, "depth").text = size_3[2]

    for i in coord1:
        save_xml_add(root, str(i[0]), str(i[1]), str(i[2]), str(i[3]), str(i[4]))

    with open(xml_save_path, 'w', encoding='utf-8') as xml_file:
        # 将 XML 元素树转换为字节串,编码为 utf-8
        rough_string = ET.tostring(root, 'utf-8')
        # 使用 minidom 模块解析生成的字节串,得到一个可操作的 XML 对象
        reparsed = minidom.parseString(rough_string)
        # 将重新解析后的 XML 对象转换为格式打印(pretty-print)的字符串形式,
        # 其中 indent="  "表示使用两个空格作为缩进
        string_ = reparsed.toprettyxml(indent="  ")
        xml_file.write(string_)
    print("已保存:", xml_save_path)


    

class Yolov8Detector():
    def __init__(self, mode_name):
        # 加载目标检测模型
        # self.model = YOLO(mode_name)
        self.model = YOLO(mode_name)
        self.model_cls_name = self.model.names

    def run(self, frame, image_size):
        # 判断图像类型是否正确
        if (isinstance(frame, np.ndarray)):
            boxes_list = []
            # 目标检测
            results = self.model.predict(frame, imgsz=image_size)
            # results = self.model(frame)
            result = results[0]
            # print(result, "这是")
            boxes = result.boxes.cpu()  # Boxes对象用于边界框输出
            for cls_, box in zip(boxes.cls, boxes):
                cls_1 = int(np.array(cls_))
                x1 = int(np.array(box.xyxy)[0][0])
                y1 = int(np.array(box.xyxy)[0][1])
                x2 = int(np.array(box.xyxy)[0][2])
                y2 = int(np.array(box.xyxy)[0][3])
                boxes_list.append((self.model_cls_name[cls_1], x1, y1, x2, y2))
            
            # 返回格式
            # [(类别1, x1, y1, x2, y2), (类别2, x1, y1, x2, y2)]
            return boxes_list
    
    def model_cls_name(self):
        # 获取模型内类别名
        return self.model_cls_name

# 加载检测模型
v8D = Yolov8Detector("zhayaoku_20240828.pt")


# 图片路径
img_path = "./img_lab_1000/img_1000/"
# 保存xml的路径
xml_save_path = "./img_lab_1000/img_xml/"


# 图片分辨率信息
size_list = ["1920", "1080", "3"]

# 需要标注的类别  # head_no_hat  head_with_hat
class_name_list_xml_save = ['helmet']

for img_name in os.listdir(img_path):

    if img_name.endswith(".jpg"):
        image_path = os.path.join(img_path, img_name)

        frame = cv2.imread(image_path)
        coord1 = v8D.run(frame, 640) 
        coord2 = []

        for i_ in coord1:
            for name_i in class_name_list_xml_save:
                if i_[0] == name_i:
                    coord2.append((i_[0], i_[1], i_[2], i_[3], i_[4]))
        # xml文件名
        xml_save_path_name = os.path.join(xml_save_path, os.path.splitext(img_name)[0] + ".xml")
        # 保存xml
        create_xml_annotation(image_path, coord2, xml_save_path_name, size_list)

检测标注文件工具:

单类别拆分

参数:全部类别名、xml读取路径、xml保存路径、

import xml.etree.ElementTree as ET
import os
from xml.dom import minidom

# 全部类别
list_class = ['helmet', 'human_backward', 'human_forward',
              'closed_door', 'opened_door', 'covered_door',
              'head_with_hat', 'head_no_hat']
# xml标签路径 (绝对路径)
path_lab = "E:/zyk_lab/炸药库ataset240820/ce/lab/"

# 拆分保存xml的路径 (绝对路径)
save_xml_path = "E:/zyk_lab/炸药库ataset240820/ce/ce/"


for xml_name in os.listdir(path_lab):
    # xml_name = 'Camera12_20231001_30.xml'

    # 1. 读取XML文档
    tree = ET.parse(path_lab + xml_name)
    root = tree.getroot()

    # 存储 字典
    dict_class = {}
    for i in list_class:
        dict_class[i] = []
        # 创建单个文件夹
        folder_name = save_xml_path + "/" + i + "/"
        if not os.path.exists(folder_name):
            os.mkdir(folder_name)
        #     print(f"文件夹 '{folder_name}' 创建成功。")
        # else:
        #     print(f"文件夹 '{folder_name}' 已存在。")

    # for i, j in dict_class.items():
    #     print(i, j)

    size_find_0 = root.find("size")
    size_w = size_find_0.find("width")
    size_h = size_find_0.find("height")
    size_d = size_find_0.find("depth")
    #
    # print(size_w.text)
    # print(size_h.text)
    # print(size_d.text)

    # 分离文件名 与 文件后缀
    name_lab, xml_ = os.path.splitext(xml_name)

    folder_jpg = root.find("folder")
    # print(folder_jpg.text)

    path_jpg = root.find("path")
    # print(path_jpg.text)

    filename_jpg = root.find("filename")
    # print(filename_jpg.text)

    # 2. 查找 object 全部
    objects = root.findall('object')
    for object_find_0 in objects:
        # print('Tag:', child.tag)
        # print('Text:', child.text)
        # print('Attributes:', child.attrib)

        class_name = object_find_0.find("name")
        class_bndbox = object_find_0.find("bndbox")
        class_bndbox_xmin = class_bndbox.find("xmin")
        class_bndbox_ymin = class_bndbox.find("ymin")
        class_bndbox_xmax = class_bndbox.find("xmax")
        class_bndbox_ymax = class_bndbox.find("ymax")

        # print(class_name.text)
        # print(class_bndbox_xmin.text)
        # print(class_bndbox_ymin.text)
        # print(class_bndbox_xmax.text)
        # print(class_bndbox_ymax.text)

        dict_class[class_name.text].append(
            (class_name.text,
             class_bndbox_xmin.text,
             class_bndbox_ymin.text,
             class_bndbox_xmax.text,
             class_bndbox_ymax.text,
             )
        )

    for ob_class, ob_list in dict_class.items():

        # 创建根元素
        root = ET.Element("annotation")

        folder_save = ET.SubElement(root, "folder")
        folder_save.text = folder_jpg.text

        filename_jpg_save = ET.SubElement(root, "filename")
        filename_jpg_save.text = filename_jpg.text

        path_save_xml = ET.SubElement(root, "path")
        path_save_xml.text = path_jpg.text

        # 创建子元素
        size_save = ET.SubElement(root, "size")
        # 创建二级子元素 只需输入参数不同即可
        size_w_save = ET.SubElement(size_save, "width")
        size_w_save.text = size_w.text
        size_h_save = ET.SubElement(size_save, "height")
        size_h_save.text = size_h.text
        size_d_save = ET.SubElement(size_save, "depth")
        size_d_save.text = size_d.text

        for ob_list_i in ob_list:
            object_save = ET.SubElement(root, "object")
            name_save = ET.SubElement(object_save, "name")
            name_save.text = str(ob_list_i[0])

            bndbox_save = ET.SubElement(object_save, "bndbox")
            xmin_save = ET.SubElement(bndbox_save, "xmin")
            xmin_save.text = str(ob_list_i[1])

            ymin_save = ET.SubElement(bndbox_save, "ymin")
            ymin_save.text = str(ob_list_i[2])

            xmax_save = ET.SubElement(bndbox_save, "xmax")
            xmax_save.text = str(ob_list_i[3])

            ymax_save = ET.SubElement(bndbox_save, "ymax")
            ymax_save.text = str(ob_list_i[4])

        # 写入文件
        if len(dict_class[ob_class]) != 0:
            if ob_class in list_class:
                path_save_i = save_xml_path + "/" + ob_class + "/" + xml_name
                print(path_save_i)
                # tree.write(path_save_i, encoding="utf-8", xml_declaration=True)
                with open(path_save_i, 'w', encoding='utf-8') as xml_file:
                    # 将 XML 元素树转换为字节串,编码为 utf-8
                    rough_string = ET.tostring(root, 'utf-8')
                    # 使用 minidom 模块解析生成的字节串,得到一个可操作的 XML 对象
                    reparsed = minidom.parseString(rough_string)
                    # 将重新解析后的 XML 对象转换为格式打印(pretty-print)的字符串形式,
                    # 其中 indent="  "表示使用两个空格作为缩进
                    string_ = reparsed.toprettyxml(indent="  ")
                    xml_file.write(string_)

合并所有类别xml

参数:全部类别、读取图片路径、单类别拆分的总路径、合并保存的路径、

import xml.etree.ElementTree as ET
import os
from xml.dom import minidom

list_class = ['helmet', 'human_backward', 'human_forward',
              'closed_door', 'opened_door', 'covered_door',
              'head_with_hat', 'head_no_hat']
# 图片路径
path_img = "E:/zyk_lab/炸药库ataset240820/ce/img/"
# 拆分的总路径
path_lab = "E:/zyk_lab/炸药库ataset240820/ce/ce/"
# 合并后保存的路径
path_lab_save = "E:/zyk_lab/炸药库ataset240820/ce/lab_ce/"

for img_name in os.listdir(path_img):
    # img_name = "Camera12_20231001_31"
    img_name = os.path.splitext(img_name)[0]

    dict_class = {}
    for i in list_class:
        dict_class[i] = []

    dict_class["width"] = 0
    dict_class["height"] = 0
    dict_class["depth"] = 0
    dict_class["folder"] = "null"
    dict_class["path"] = "null"
    dict_class["filename"] = "null"

    for file_1 in os.listdir(path_lab):
        path_i = os.path.join(path_lab, file_1)
        for xml_name in os.listdir(path_i):
            if img_name == os.path.splitext(xml_name)[0]:

                # 1. 读取XML文档
                xml_path = os.path.join(path_i, xml_name)
                tree = ET.parse(xml_path)
                root = tree.getroot()

                size_find_0 = root.find("size")
                size_w = size_find_0.find("width").text
                size_h = size_find_0.find("height").text
                size_d = size_find_0.find("depth").text

                folder_jpg = root.find("folder").text
                # print(folder_jpg.text)
                try:
                    path_jpg = root.find("path").text
                    # print(path_jpg.text)
                except BaseException:
                    path_jpg = "path_jpg"

                filename_jpg = root.find("filename").text
                # print(filename_jpg.text)

                dict_class["width"] = size_w
                dict_class["height"] = size_h
                dict_class["depth"] = size_d
                dict_class["folder"] = folder_jpg
                dict_class["path"] = path_jpg
                dict_class["filename"] = filename_jpg

                # 2. 查找 object 全部
                try:
                    objects = root.findall('object')
                    for object_find_0 in objects:
                        # print('Tag:', child.tag)
                        # print('Text:', child.text)
                        # print('Attributes:', child.attrib)

                        class_name = object_find_0.find("name")
                        class_bndbox = object_find_0.find("bndbox")
                        class_bndbox_xmin = class_bndbox.find("xmin")
                        class_bndbox_ymin = class_bndbox.find("ymin")
                        class_bndbox_xmax = class_bndbox.find("xmax")
                        class_bndbox_ymax = class_bndbox.find("ymax")

                        # print(class_name.text)
                        # print(class_bndbox_xmin.text)
                        # print(class_bndbox_ymin.text)
                        # print(class_bndbox_xmax.text)
                        # print(class_bndbox_ymax.text)

                        dict_class[class_name.text].append(
                            (class_name.text,
                             class_bndbox_xmin.text,
                             class_bndbox_ymin.text,
                             class_bndbox_xmax.text,
                             class_bndbox_ymax.text,
                             )
                        )
                except BaseException:
                    print("读取 object 失败")

    # 保存
    # 创建根元素
    root = ET.Element("annotation")

    folder_save = ET.SubElement(root, "folder")
    if dict_class["folder"] != "null":
        folder_save.text = dict_class["folder"]

    filename_jpg_save = ET.SubElement(root, "filename")
    if dict_class["filename"] != "null":
        filename_jpg_save.text = dict_class["filename"]

    path_save_xml = ET.SubElement(root, "path")
    if dict_class["path"] != "null":
        path_save_xml.text = dict_class["path"]

    # 创建子元素
    size_save = ET.SubElement(root, "size")
    # 创建二级子元素 只需输入参数不同即可
    size_w_save = ET.SubElement(size_save, "width")
    if dict_class["width"] != "null":
        size_w_save.text = dict_class["width"]
    size_h_save = ET.SubElement(size_save, "height")
    if dict_class["height"] != "null":
        size_h_save.text = dict_class["height"]
    size_d_save = ET.SubElement(size_save, "depth")
    if dict_class["depth"] != "null":
        size_d_save.text = dict_class["depth"]

    for ob_class, ob_list in dict_class.items():
        print(ob_class, ob_list)
        if ob_class in ["folder", "filename", "path", "size", "width", "height", "depth"]:
            continue
        for ob_list_i in ob_list:
            # print(ob_list_i)
            object_save = ET.SubElement(root, "object")
            name_save = ET.SubElement(object_save, "name")
            name_save.text = str(ob_list_i[0])

            bndbox_save = ET.SubElement(object_save, "bndbox")
            xmin_save = ET.SubElement(bndbox_save, "xmin")
            xmin_save.text = str(ob_list_i[1])

            ymin_save = ET.SubElement(bndbox_save, "ymin")
            ymin_save.text = str(ob_list_i[2])

            xmax_save = ET.SubElement(bndbox_save, "xmax")
            xmax_save.text = str(ob_list_i[3])

            ymax_save = ET.SubElement(bndbox_save, "ymax")
            ymax_save.text = str(ob_list_i[4])

    # 写入文件
    # if len(dict_class[ob_class]) != 0:
    #     if ob_class in list_class:
    path_save_i = path_lab_save + img_name + ".xml"
    print(path_save_i)

    # tree.write(path_save_i, encoding="utf-8", xml_declaration=True)
    with open(path_save_i, 'w', encoding='utf-8') as xml_file:
        # 将 XML 元素树转换为字节串,编码为 utf-8
        rough_string = ET.tostring(root, 'utf-8')
        # 使用 minidom 模块解析生成的字节串,得到一个可操作的 XML 对象
        reparsed = minidom.parseString(rough_string)
        # 将重新解析后的 XML 对象转换为格式打印(pretty-print)的字符串形式,
        # 其中 indent="  "表示使用两个空格作为缩进
        string_ = reparsed.toprettyxml(indent="  ")
        xml_file.write(string_)

合并指定几个类别

注意:合并后检测完毕后,需要到原本拆分成单类别目录,将检测前的xml文件删除,把检查后的xml放入其中一个类别文件夹即可。
参数:全部类别、需要合并的类别、读取图像路径、拆分总目录、保存xml路径、

import xml.etree.ElementTree as ET
import os
from xml.dom import minidom

# 全部类别
# list_class = ['class1', 'class2', 'class3']
list_class = [
              'class1', 'class2'
              ]

# 图像路径
path_lab = "E:/zyk_lab/ataset240820/ce/img/"

# 拆分后的总路径
xml_path_all = "E:/zyk_lab/ataset240820/ce/ce/"
# xml 保存的路径
xml_save_path = "E:/zyk_lab/ataset240820/ce/xml_ce/"


for xml_name in os.listdir(path_lab):
    # xml_name = 'Camera12_20231001_30.xml'

    # 存储 字典
    dict_class = {}
    for i in list_class:
        dict_class[i] = []

    dict_class["width"] = 0
    dict_class["height"] = 0
    dict_class["depth"] = 0
    dict_class["folder"] = "null"
    dict_class["path"] = "null"
    dict_class["filename"] = "null"

    # 读取 拆分后的 单类别路径
    for list_class_i in list_class:
        class_path = os.path.join(xml_path_all, list_class_i)
        # 拆分.jpg后缀
        xml_name = os.path.splitext(xml_name)[0] + ".xml"
        path_xml_name = os.path.join(class_path, xml_name)
        # 检测文件是否存在
        if os.path.exists(path_xml_name):

            # 1. 读取XML文档
            tree = ET.parse(path_xml_name)
            root = tree.getroot()

            size_find_0 = root.find("size")
            size_w = size_find_0.find("width")
            size_h = size_find_0.find("height")
            size_d = size_find_0.find("depth")

            # print(size_w.text)
            # print(size_h.text)
            # print(size_d.text)


            folder_jpg = root.find("folder")
            # print(folder_jpg.text)
            try:
                path_jpg = root.find("path").text
                # print(path_jpg.text)
            except BaseException:
                path_jpg = "path_jpg"

            filename_jpg = root.find("filename")
            # print(filename_jpg.text)

            dict_class["width"] = size_w.text
            dict_class["height"] = size_h.text
            dict_class["depth"] = size_d.text
            dict_class["folder"] = folder_jpg.text
            dict_class["path"] = path_jpg
            dict_class["filename"] = filename_jpg.text

            # 2. 查找 object 全部
            try:
                objects = root.findall('object')
                for object_find_0 in objects:
                    # print('Tag:', child.tag)
                    # print('Text:', child.text)
                    # print('Attributes:', child.attrib)

                    class_name = object_find_0.find("name")
                    class_bndbox = object_find_0.find("bndbox")
                    class_bndbox_xmin = class_bndbox.find("xmin")
                    class_bndbox_ymin = class_bndbox.find("ymin")
                    class_bndbox_xmax = class_bndbox.find("xmax")
                    class_bndbox_ymax = class_bndbox.find("ymax")

                    # print(class_name.text)
                    # print(class_bndbox_xmin.text)
                    # print(class_bndbox_ymin.text)
                    # print(class_bndbox_xmax.text)
                    # print(class_bndbox_ymax.text)

                    if class_name.text in list_class:

                        dict_class[class_name.text].append(
                            (class_name.text,
                             class_bndbox_xmin.text,
                             class_bndbox_ymin.text,
                             class_bndbox_xmax.text,
                             class_bndbox_ymax.text,
                             )
                        )
            except BaseException:
                print("读取 object 失败")

            # for i, j in dict_class.items():
            #     print(i, j)
            # 创建根元素
            root = ET.Element("annotation")

            folder_save = ET.SubElement(root, "folder")
            folder_save.text = dict_class["folder"]

            filename_jpg_save = ET.SubElement(root, "filename")
            filename_jpg_save.text = dict_class["filename"]

            path_save_xml = ET.SubElement(root, "path")
            path_save_xml.text = dict_class["filename"]

            # 创建子元素
            size_save = ET.SubElement(root, "size")
            # 创建二级子元素 只需输入参数不同即可
            size_w_save = ET.SubElement(size_save, "width")
            size_w_save.text = dict_class["width"]
            size_h_save = ET.SubElement(size_save, "height")
            size_h_save.text = dict_class["height"]
            size_d_save = ET.SubElement(size_save, "depth")
            size_d_save.text = dict_class["depth"]

            for class_i in list_class:
                for ob_list_i in dict_class[class_i]:
                    object_save = ET.SubElement(root, "object")
                    name_save = ET.SubElement(object_save, "name")
                    name_save.text = str(ob_list_i[0])

                    bndbox_save = ET.SubElement(object_save, "bndbox")
                    xmin_save = ET.SubElement(bndbox_save, "xmin")
                    xmin_save.text = str(ob_list_i[1])

                    ymin_save = ET.SubElement(bndbox_save, "ymin")
                    ymin_save.text = str(ob_list_i[2])

                    xmax_save = ET.SubElement(bndbox_save, "xmax")
                    xmax_save.text = str(ob_list_i[3])

                    ymax_save = ET.SubElement(bndbox_save, "ymax")
                    ymax_save.text = str(ob_list_i[4])

                    # 写入文件
                    path_save_i = os.path.join(xml_save_path, xml_name)
                    print(path_save_i)
                    # tree.write(path_save_i, encoding="utf-8", xml_declaration=True)
                    with open(path_save_i, 'w', encoding='utf-8') as xml_file:
                        # 将 XML 元素树转换为字节串,编码为 utf-8
                        rough_string = ET.tostring(root, 'utf-8')
                        # 使用 minidom 模块解析生成的字节串,得到一个可操作的 XML 对象
                        reparsed = minidom.parseString(rough_string)
                        # 将重新解析后的 XML 对象转换为格式打印(pretty-print)的字符串形式,
                        # 其中 indent="  "表示使用两个空格作为缩进
                        string_ = reparsed.toprettyxml(indent="  ")
                        xml_file.write(string_)

增删类别

注意:不会新建xml
在xml文件内,想在固定位置绘制一个矩形框作为标签。
在xml文件内,指定左上角与右下角坐标检查某类别的标注矩形是否有重合,有则删除。

import xml.etree.ElementTree as ET
import os
from xml.dom import minidom


# 矩形重合检测

def is_rectangles_overlap(x1, y1, x2, y2, x1_, y1_, x2_, y2_):
    stat = False

    num_x2 = x2_ - x1_
    for i in range(num_x2):
        x1_i = x1_ + i
        # print(x1_i)

        if (x1 < x1_i < x2) and (y1 < y1_ < y2):
            # print("x上面线内的点有重合")
            stat = True
            break
        if (x1 < x1_i < x2) and (y1 < y2_ < y2):
            # print("x下面线内的点有重合")
            stat = True
            break

    num_y1 = y2_ - y1_
    for j in range(num_y1):
        y1_i = y1_ + j
        if (x1 < x1_ < x2) and (y1 < y1_i < y2):
            # print("y左边线内有重合")
            stat = True
            break
        if (x1 < x2_ < x2) and (y1 < y1_i < y2):
            # print("y右边线内有重合")
            stat = True
            break

    # 矩形完全重合
    if x1 == x1_ and y1 == y1_ and x2 == x2_ and y2 == y2_:
        stat = True

    return stat


def save_xml_add(name, xmin, ymin, xmax, ymax):
    object_save = ET.SubElement(root, "object")
    name_save = ET.SubElement(object_save, "name")
    name_save.text = str(name)

    bndbox_save = ET.SubElement(object_save, "bndbox")
    xmin_save = ET.SubElement(bndbox_save, "xmin")
    xmin_save.text = str(xmin)

    ymin_save = ET.SubElement(bndbox_save, "ymin")
    ymin_save.text = str(ymin)

    xmax_save = ET.SubElement(bndbox_save, "xmax")
    xmax_save.text = str(xmax)

    ymax_save = ET.SubElement(bndbox_save, "ymax")
    ymax_save.text = str(ymax)


# 添加标注的信息
# 根据分辨率的不同设置不同类别和坐标
dict_add = {}
# 704 只是为了提醒自己在多大分辨率的图上绘制
# dict_add = {704: [("covered_door", 184, 33, 331, 330), ]
#             }


# 删除标注信息
# dict_del = [("closed_door", 184, 33, 331, 330)]
# dle 不管什么类别 只要重合就删除
dict_del = [("dle", 184, 33, 331, 330)]
# dict_del = []

# 读取路径 拆分后单类别的路径
path_i = r"E:\zyk_lab\炸药库ataset240820\ce\ce\closed_door"

# 保存路径 自己新建路径
path_lab_save = r"E:\zyk_lab\炸药库ataset240820\ce\xml_ce"

# xml_name = "Camera12_20231001_30.xml"
for xml_name in os.listdir(path_i):
    # 存储字典 全部类别
    list_class = ['helmet', 'human_backward', 'human_forward',
                  'closed_door', 'opened_door', 'covered_door',
                  'head_with_hat', 'head_no_hat']
    dict_class = {}
    for i in list_class:
        dict_class[i] = []

    # 1. 读取XML文档
    xml_path = os.path.join(path_i, xml_name)
    print(xml_path)
    tree = ET.parse(xml_path)
    root = tree.getroot()

    size_find_0 = root.find("size")
    size_w = size_find_0.find("width").text
    size_h = size_find_0.find("height").text
    size_d = size_find_0.find("depth").text

    folder_jpg = root.find("folder").text
    # print(folder_jpg.text)
    path_jpg = root.find("path").text
    # print(path_jpg.text)
    filename_jpg = root.find("filename").text
    # print(filename_jpg.text)

    dict_class["width"] = size_w
    dict_class["height"] = size_h
    dict_class["depth"] = size_d
    dict_class["folder"] = folder_jpg
    dict_class["path"] = path_jpg
    dict_class["filename"] = filename_jpg

    # 2. 查找 object 全部
    objects = root.findall('object')
    for object_find_0 in objects:

        # print('Tag:', child.tag)
        # print('Text:', child.text)
        # print('Attributes:', child.attrib)

        class_name = object_find_0.find("name")
        class_bndbox = object_find_0.find("bndbox")
        class_bndbox_xmin = class_bndbox.find("xmin")
        class_bndbox_ymin = class_bndbox.find("ymin")
        class_bndbox_xmax = class_bndbox.find("xmax")
        class_bndbox_ymax = class_bndbox.find("ymax")

        # print(class_name.text)
        # print(class_bndbox_xmin.text)
        # print(class_bndbox_ymin.text)
        # print(class_bndbox_xmax.text)
        # print(class_bndbox_ymax.text)

        con_1 = False

        if dict_del:
            for list_con in dict_del:

                x1_, y1_, x2_, y2_ = int(class_bndbox_xmin.text), int(class_bndbox_ymin.text), int(
                    class_bndbox_xmax.text), int(class_bndbox_ymax.text)
                # print(f"{list_con[1]}, {list_con[2]}, {list_con[3]}, {list_con[4]},{x1_}, {y1_}, {x2_}, {y2_}")

                # 制定类别
                if class_name == list_con[0] or list_con[0] == "dle":
                    # 框若有重合 则跳过
                    if is_rectangles_overlap(list_con[1], list_con[2], list_con[3], list_con[4], x1_, y1_, x2_, y2_):
                        print("有重合")
                        print(f"{list_con[1]}, {list_con[2]}, {list_con[3]}, {list_con[4]},{x1_}, {y1_}, {x2_}, {y2_}")
                        con_1 = True
                    else:
                        print("无重合")
                        print(f"{list_con[1]}, {list_con[2]}, {list_con[3]}, {list_con[4]},{x1_}, {y1_}, {x2_}, {y2_}")
        # print(con_1)
        if con_1:
            continue

        dict_class[class_name.text].append(
            (class_name.text,
             class_bndbox_xmin.text,
             class_bndbox_ymin.text,
             class_bndbox_xmax.text,
             class_bndbox_ymax.text,
             )
        )

    # 保存
    # 创建根元素
    root = ET.Element("annotation")

    folder_save = ET.SubElement(root, "folder")
    if dict_class["folder"] != "null":
        folder_save.text = dict_class["folder"]

    filename_jpg_save = ET.SubElement(root, "filename")
    if dict_class["filename"] != "null":
        filename_jpg_save.text = dict_class["filename"]

    path_save_xml = ET.SubElement(root, "path")
    if dict_class["path"] != "null":
        path_save_xml.text = dict_class["path"]

    # 创建子元素
    size_save = ET.SubElement(root, "size")
    # 创建二级子元素 只需输入参数不同即可
    size_w_save = ET.SubElement(size_save, "width")
    if dict_class["width"] != "null":
        size_w_save.text = dict_class["width"]
    size_h_save = ET.SubElement(size_save, "height")
    if dict_class["height"] != "null":
        size_h_save.text = dict_class["height"]
    size_d_save = ET.SubElement(size_save, "depth")
    if dict_class["depth"] != "null":
        size_d_save.text = dict_class["depth"]

    for ob_class, ob_list in dict_class.items():
        # print(ob_class, ob_list)
        if ob_class in ["folder", "filename", "path", "size", "width", "height", "depth"]:
            continue
        for ob_list_i in ob_list:
            # print(ob_list_i)
            save_xml_add(ob_list_i[0], ob_list_i[1], ob_list_i[2], ob_list_i[3], ob_list_i[4])

    # 读取字典内容 添加新的标注
    if dict_add:
        for img_size, list_class_xyxy in dict_add.items():
            for class_xyxy_i in list_class_xyxy:
                save_xml_add(class_xyxy_i[0], class_xyxy_i[1], class_xyxy_i[2], class_xyxy_i[3], class_xyxy_i[4])

    # 写入文件
    # if len(dict_class[ob_class]) != 0:
    #     if ob_class in list_class:
    path_save_i = os.path.join(path_lab_save, xml_name)
    # print(path_save_i)

    # tree.write(path_save_i, encoding="utf-8", xml_declaration=True)
    with open(path_save_i, 'w', encoding='utf-8') as xml_file:
        # 将 XML 元素树转换为字节串,编码为 utf-8
        rough_string = ET.tostring(root, 'utf-8')
        # 使用 minidom 模块解析生成的字节串,得到一个可操作的 XML 对象
        reparsed = minidom.parseString(rough_string)
        # 将重新解析后的 XML 对象转换为格式打印(pretty-print)的字符串形式,
        # 其中 indent="  "表示使用两个空格作为缩进
        string_ = reparsed.toprettyxml(indent="  ")
        xml_file.write(string_)

固定矩形位置生成xml

参数:

import os
import xml.etree.ElementTree as ET
from xml.dom import minidom


def save_xml_add(root, lei_bie_name, xmin, ymin, xmax, ymax):
    object_elem1 = ET.SubElement(root, "object")
    ET.SubElement(object_elem1, "name").text = lei_bie_name
    bndbox1 = ET.SubElement(object_elem1, "bndbox")
    ET.SubElement(bndbox1, "xmin").text = str(xmin)
    ET.SubElement(bndbox1, "ymin").text = str(ymin)
    ET.SubElement(bndbox1, "xmax").text = str(xmax)
    ET.SubElement(bndbox1, "ymax").text = str(ymax)


def create_xml_annotation(image_path, coord1, xml_save_path, size_3, lei_bie_name):
    root = ET.Element("annotation")

    folder = os.path.dirname(image_path)
    ET.SubElement(root, "folder").text = folder

    filename = os.path.basename(image_path)
    ET.SubElement(root, "filename").text = filename

    size = ET.SubElement(root, "size")
    ET.SubElement(size, "width").text = size_3[0]
    ET.SubElement(size, "height").text = size_3[1]
    ET.SubElement(size, "depth").text = size_3[2]

    for i in coord1:
        save_xml_add(root, lei_bie_name, i[0], i[1], i[2], i[3])

    with open(xml_save_path, 'w', encoding='utf-8') as xml_file:
        # 将 XML 元素树转换为字节串,编码为 utf-8
        rough_string = ET.tostring(root, 'utf-8')
        # 使用 minidom 模块解析生成的字节串,得到一个可操作的 XML 对象
        reparsed = minidom.parseString(rough_string)
        # 将重新解析后的 XML 对象转换为格式打印(pretty-print)的字符串形式,
        # 其中 indent="  "表示使用两个空格作为缩进
        string_ = reparsed.toprettyxml(indent="  ")
        xml_file.write(string_)
    print("已保存:", xml_save_path)


def xml_save(folder_path, xml_save_path, coord1, size_list, lei_bie_name):
    for filename in os.listdir(folder_path):
        if filename.endswith(".jpg"):
            image_path = os.path.join(folder_path, filename)
            xml_save_path_name = os.path.join(xml_save_path, os.path.splitext(filename)[0] + ".xml")

            create_xml_annotation(image_path, coord1, xml_save_path_name, size_list, lei_bie_name)


# 图片路径
folder_path = r"E:\zyk_lab\ataset240820\ce\img"
# 保存xml的路径
xml_save_path = r"E:\zyk_lab\ataset240820\ce\xml_ce"
# 绘制坐标
coord1 = [(221, 47, 330, 325)]
# 绘制类别
lei_bie_name = "covered_door"
# 图片分辨率信息
size_list = ["704", "576", "3"]

xml_save(folder_path, xml_save_path, coord1, size_list, lei_bie_name)

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

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

相关文章

一个将.Geojson文件转成shapefile和kml文件的在线页面工具

最近需要读取.geojson格式的流域边界文件。在谷歌地球桌面版和globalMapper中均无法正常读取。下面我发现的一个在线的平台可以很好实现这一功能。 GeoJSON to SHP Converter Online - MyGeodata Cloud ❤️欢迎点赞收藏❤️

关于WPF(Windows Presentation Foundation)中Grid控件

本文将从Grid控件的基础概念开始&#xff0c;逐步深入探讨其特性、用法、实例代码&#xff0c;以及最佳实践。 1. WPF和布局简介 WPF是一种用于构建Windows桌面应用程序的UI框架&#xff0c;它通过XAML&#xff08;Extensible Application Markup Language&#xff09;使开…

Oracle中解决select into值集为空的报错情况

先看为空的情况 procedure test is n number; begin select 1 into n from CUX_2_OM_RELEASE_LIMIT_V cov where cov.Customer_Idnull; end; CUX_2_OM_RELEASE_LIMIT_V中没有id是空的&#xff0c;因此返回的结果一定是空集 运行结果: 有时候我…

QT事件与网络通信

闹钟 头文件 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QTimer> #include <QTextToSpeech> // 添加此行以引入QTextToSpeech类QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACEclass MainWin…

大数据ETL数据提取转换和加载处理

什么是 ETL&#xff1f; 提取转换加载&#xff08;英语&#xff1a;Extract, transform, load&#xff0c;简称ETL&#xff09;&#xff0c;用来描述将资料从来源端经过抽取、转置、加载至目的端的过程。ETL一词较常用在数据仓库&#xff0c;但其对象并不限于数据仓库。 ETL&…

下载Edge/Chrome浏览器主题的背景图片

当我们为Edge安装了心仪的主题后&#xff0c;希望把对应的背景图片下载保存要怎么做呢&#xff0c;以下图的“湖心小屋”主题为例。如下图&#xff0c;我们已经在应用商店中按照了该主题。 当打开新标签页后&#xff0c;可以欣赏这个主题内置的背景图片。 如果想要下载这个背景…

【人工智能/计算机工程/大数据】第五届人工智能与计算工程国际学术会议(ICAICE 2024,2024年11月8-10日)

The 5th International Conference on Artificial Intelligence and Computer Engineering 第五届人工智能与计算工程国际学术会议&#xff08;ICAICE 2024&#xff09; 会议官网&#xff1a;www.event-icaice.org The 5th International Conference on Artificial Intellige…

外包干了5天,技术明显退步

我是一名本科生&#xff0c;自2019年起&#xff0c;我便在南京某软件公司担任功能测试的工作。这份工作虽然稳定&#xff0c;但日复一日的重复性工作让我逐渐陷入了舒适区&#xff0c;失去了前进的动力。两年的时光匆匆流逝&#xff0c;我却在原地踏步&#xff0c;技术没有丝毫…

【Vue】Vue扫盲(二)指令:v-for 、v-if、v-else-if、v-else、v-show

【Vue】Vue扫盲&#xff08;一&#xff09;事件标签、事件修饰符&#xff1a;click.prevent click.stop click.stop.prevent、按键修饰符、及常用指令 文章目录 一、v-for遍历数组数组角标遍历对象&#xff1a;Key作用介绍 二、v-if、v-show基本用法&#xff1a;案例&#xff1…

Linux--多路转接之epoll

上一篇:Linux–多路转接之select epoll epoll 是 Linux 下多路复用 I/O 接口 select/poll 的增强版本&#xff0c;它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。它是 Linux 下多路复用 API 的一个选择&#xff0c;相比 select 和 poll&#xff0c…

用于病理图像诊断的跨尺度多实例学习|文献速递-基于深度学习的医学影像分类,分割与多模态应用

Title 题目 Cross-scale multi-instance learning for pathological image diagnosis 用于病理图像诊断的跨尺度多实例学习 01 文献速递介绍 病理学是诊断炎症性肠病&#xff08;如克罗恩病&#xff09;的金标准&#xff08;Gubatan等&#xff0c;2021&#xff1b;Yeshi等…

机器学习:opencv--人脸检测以及微笑检测

目录 前言 一、人脸检测的原理 1.特征提取 2.分类器 二、代码实现 1.图片预处理 2.加载分类器 3.进行人脸识别 4.标注人脸及显示 三、微笑检测 前言 人脸检测是计算机视觉中的一个重要任务&#xff0c;旨在自动识别图像或视频中的人脸。它可以用于多种应用&#xff0…

Vxe UI vue vxe-table select 下拉框选项列表数据量超大过大时卡顿解决方法

Vxe UI vue vxe-table vxe-grid select 下拉框选项列表数据量超大过大时卡顿解决方法 查看 github vxe-table 官网 vxe-table 本身支持虚拟滚动&#xff0c;数据量大也是支持的&#xff0c;但是如果在可编辑表格中使用下拉框&#xff0c;下拉框的数据量超大时&#xff0c;可能…

int QSqlQuery::size() const

返回结果的大小&#xff08;返回的行数&#xff09; 或者返回-1 &#xff08;如果大小不能被决定 或者 数据库不支持报告查询的大小信息&#xff09; 注意&#xff1a;对于非查询语句&#xff0c;将返回-1&#xff08;isSelect()返回false&#xff09; 如果查询不是活跃的&…

JS 分支语句

目录 1. 表达式与语句 1.1 表达式 1.2 语句 1.3 区别 2. 程序三大流控制语句 3. 分支语句 3.1 if 分支语句 3.2 双分支 if 语句 3.3 双分支语句案例 3.3.1 案例一 3.3.2 案例二 3.4 多分支语句 1. 表达式与语句 1.1 表达式 1.2 语句 1.3 区别 2. 程序三大流控制语…

基于方块编码的图像压缩matlab仿真,带GUI界面

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 编码单元的表示 4.2编码单元的编码 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 下图是随着方块大小的变化&#xff0c;图像的压缩率以及对应的图像质量指标PSN…

初始爬虫13(js逆向)

为了解决网页端的动态加载&#xff0c;加密设置等&#xff0c;所以需要js逆向操作。 JavaScript逆向可以分为三大部分&#xff1a;寻找入口&#xff0c;调试分析和模拟执行。 1.chrome在爬虫中的作用 1.1preserve log的使用 默认情况下&#xff0c;页面发生跳转之后&#xf…

echarts显示隐藏柱状图柱子的背景色

showBackground: true, //控制是否显示背景色backgroundStyle: {// color: rgba(180, 180, 180, 0.4) //背景色的颜色color: red} 关键代码是 showBackground: true, //控制是否显示背景色 设置为false或者直接而不写就是不显示背景色&#xff0c;默认是不显示背景色 true的时…

windows客户端SSH连接ubuntu/linux服务器,三种网络连接:局域网,内网穿透(sakuraftp),虚拟局域网(zerotier)

windows客户端SSH连接ubuntu/linux服务器&#xff0c;三种网络连接&#xff1a;局域网&#xff0c;内网穿透&#xff08;sakuraftp&#xff09;&#xff0c;虚拟局域网&#xff08;zerotier&#xff09; 目录 SSH简述、三种网络连接特点SSH简述局域网内连接内网穿透&#xff08…

商业级免费OCR利器!Surya OCR:支持90+种语言识别,复杂布局识别,表格解析全覆盖!

❤️ 如果你也关注大模型与 AI 的发展现状&#xff0c;且对大模型应用开发非常感兴趣&#xff0c;我会快速跟你分享最新的感兴趣的 AI 应用和热点信息&#xff0c;也会不定期分享自己的想法和开源实例&#xff0c;欢迎关注我哦&#xff01; 微信订阅号&#xff5c;搜一搜&…