史上最全AP、mAP详解与代码实现

文章目录

  • 前言
  • 一、mAP原理
    • 1、mAP概念
    • 2、准确率
    • 3、精确率
    • 4、召回率
    • 5、AP: Average Precision
  • 二、mAP0.5与mAP0.5:0.95
    • 1、mAP0.5
    • 2、mAP0.5:0.95
  • 三、mAP代码实现
    • 1、真实标签json文件格式
    • 2、模型预测标签json文件格式
    • 3、mAP代码实现
    • 4、mAP结果显示
  • 四、模型集成mAP代码
    • 1、模型main函数
    • 2、模型mAP计算代码


前言

我们在深度学习的论文经常看到实验对比指标mAP,比较mAP@0.5与mAP@0.5:0.95指标。然,又有很多博客并未完全说明清楚,特别说结合代码解释该指标。为此,本文章将梳理mAP指标,主要内容分为mAP原理解释,如何使用代码获得mAP指标,进一步探讨如何结合模型而获得mAP指标。


一、mAP原理

1、mAP概念

mAP,其中代表P(Precision)精确率。AP(Average precision)单类标签平均(各个召回率中最大精确率的平均数)的精确率,mAP(Mean Average Precision)所有类标签的平均精确率。

2、准确率

准确率=预测正确的样本数/所有样本数,即预测正确的样本比例(包括预测正确的正样本和预测正确的负样本,不过在目标检测领域,没有预测正确的负样本这一说法,所以目标检测里面没有用Accuracy的)。
在这里插入图片描述

3、精确率

精确率也称查准率,Precision针对的是某一类样本,如果没有说明类别,那么Precision是毫无意义的(有些地方不说明类别,直接说Precision,是因为二分类问题通常说的Precision都是正样本的Precision)。
在这里插入图片描述

4、召回率

Recall和Precision一样,脱离类别是没有意义的。说道Recall,一定指的是某个类别的Recall。Recall表示某一类样本,预测正确的与所有Ground Truth的比例。
Recall计算的时候,分母是Ground Truth中某一类样本的数量,而Precision计算的时候,是预测出来的某一类样本数。
在这里插入图片描述

5、AP: Average Precision

AP指单个类别平均精确度,而mAP是所有类别的平均精确度,AP是Precision-Recall Curve曲线下面的面积,以Recall为横轴,Precision为纵轴,就可以画出一条PR曲线,PR曲线下的面积就定义为AP,如下。
在这里插入图片描述
由于计算积分相对困难,因此引入插值法,计算AP公式如下:
在这里插入图片描述

在这里插入图片描述

计算面积:
在这里插入图片描述
计算方法如下:
在这里插入图片描述

二、mAP0.5与mAP0.5:0.95

1、mAP0.5

mAP@0.5: mean Average Precision(IoU=0.5)
即将IoU设为0.5时,计算每一类的所有图片的AP,然后所有类别求平均,即mAP。

2、mAP0.5:0.95

mAP@.5:.95(mAP@[.5:.95])
表示在不同IoU阈值(从0.5到0.95,步长0.05)(0.5、0.55、0.6、0.65、0.7、0.75、0.8、0.85、0.9、0.95)上的平均mAP。

三、mAP代码实现

实现mAP计算,我们需要有已知真实标签与模型预测标签,我将介绍2部分,第一部分如何使用有标记的真实数据产生coco json格式与如何使用模型预测结果产生预测json格式,第二部分如何使用代码计算map。

1、真实标签json文件格式

真实数据json格式实际是coco json 格式,我将以图方式说明。
整体json格式内容,包含images、type、annotations、categories,如下图:
在这里插入图片描述
images内容如下图:
在这里插入图片描述

annotations内容如下图:
在这里插入图片描述
categories格式为:
在这里插入图片描述

以上为真实数据转换为json的格式。

2、模型预测标签json文件格式

预测结果数据json为列表,列表保存为字典,一个字典记录一个目标,整体json格式如下图:
在这里插入图片描述

列表中字典内容如下图显示:
在这里插入图片描述
特别注意:image id 对应真实coco json图像的image-id,类别id也是对应真实coco json中的类别id。

3、mAP代码实现

我们调用pycocotool库中集成map方法,按照以上给出gt与pred的json格式,可实现map计算,其详细代码如下:

from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOevalif __name__ == "__main__":cocoGt = COCO('coco_json_format.json')        #标注文件的路径及文件名,json文件形式cocoDt = cocoGt.loadRes('predect_format.json')  #自己的生成的结果的路径及文件名,json文件形式cocoEval = COCOeval(cocoGt, cocoDt, "bbox")cocoEval.evaluate()cocoEval.accumulate()cocoEval.summarize()

4、mAP结果显示

我使用gt与pred为相同信息,为此结果为1。
在这里插入图片描述

四、模型集成mAP代码

最后,我是将mAP代码集成多个检测模型,我也在这里大致介绍如何集成模型预测mAP。

1、模型main函数

以下代码展示,检测模型输出后如何集成Computer_map()函数计算mAP方法,我已在代码中有解释,其详情如下:

def computer_main(data_root, model):'''data_root:任何文件夹,但必须保证每个图片与对应xml必须放在同一个文件夹中model:模型,用于预测'''C = Computer_map()img_root_lst = C.get_img_root_lst(data_root)  # 获得图片绝对路径与图片产生image_id映射关系# 在self.coco_json中保存categories,便于产生coco_json和predetect_jsoncategories = model.CLASSES  # 可以给txt路径读取,或直接给列表  #*********************得到classes,需要更改的地方***********##C.get_categories(categories)# 产生coco_json格式xml_root_lst = [name[:-3] + 'xml' for name in img_root_lst]for xml_root in xml_root_lst: C.xml2cocojson(xml_root)  # 产生coco json 并保存到self.coco_json中# 产生预测的jsonfor img_path in img_root_lst:parse_result = predict(model, img_path)  ####**********************需要更改的地方***********************####result, classes = parse_result['result'], parse_result['classes']# restult 格式为列表[x1,y1,x2,y2,score,label],若无结果为空img_name = C.get_strfile(img_path)C.detect2json(result, img_name)C.computer_map()  # 计算map

2、模型mAP计算代码

以下代码为Computer_map()函数计算mAP方法,我已在代码中有解释,其详情如下:

class Computer_map():'''主代码样列def computer_main(data_root, model):#data_root:任何文件夹,但必须保证每个图片与对应xml必须放在同一个文件夹中,model:模型,用于预测C = Computer_map()img_root_lst = C.get_img_root_lst(data_root)  # 获得图片绝对路径与图片产生image_id映射关系# 在self.coco_json中保存categories,便于产生coco_json和predetect_jsoncategories = model.CLASSES  # 可以给txt路径读取,或直接给列表  #*********************得到classes,需要更改的地方***********##C.get_categories(categories)# 产生coco_json格式xml_root_lst = [name[:-3] + 'xml' for name in img_root_lst]for xml_root in xml_root_lst: C.xml2cocojson(xml_root)  # 产生coco json 并保存到self.coco_json中# 产生预测的jsonfor img_path in img_root_lst:parse_result = predict(model, img_path)  ####**********************需要更改的地方***********************####result, classes = parse_result['result'], parse_result['classes']# restult 格式为列表[x1,y1,x2,y2,score,label],若无结果为空img_name = C.get_strfile(img_path)C.detect2json(result, img_name)C.computer_map()  # 计算map'''def __init__(self):self.img_format = ['png', 'jpg', 'JPG', 'PNG', 'bmp', 'jpeg']self.coco_json = {'images': [], 'type': 'instances', 'annotations': [], 'categories': []}self.predetect_json = []  # 保存字典self.image_id = 10000000  # 图像的id,每增加一张图片便+1self.anation_id = 10000000self.imgname_map_id = {}  # 图片名字映射iddef read_txt(self, file_path):with open(file_path, 'r') as f:content = f.read().splitlines()return contentdef get_categories(self, categories):'''categories:为字符串,指绝对路径;为列表,指类本身return:将categories存入coco json中'''if isinstance(categories, str):categories = self.read_txt(categories)elif isinstance(categories, list or tuple):categories = list(categories)category_json = [{"supercategory": cat, "id": i + 1, "name": cat} for i, cat in enumerate(categories)]self.coco_json['categories'] = category_jsondef computer_map(self, coco_json_path=None, predetect_json_path=None):from pycocotools.coco import COCOfrom pycocotools.cocoeval import COCOevalfrom collections import defaultdictimport timeimport jsonfrom pycocotools import mask as maskUtilsimport numpy as np# 继承修改coco json文件class COCO_modify(COCO):def __init__(self, coco_json_data=None):"""Constructor of Microsoft COCO helper class for reading and visualizing annotations.:param annotation_file (str): location of annotation file:param image_folder (str): location to the folder that hosts images.:return:"""# load datasetself.dataset, self.anns, self.cats, self.imgs = dict(), dict(), dict(), dict()self.imgToAnns, self.catToImgs = defaultdict(list), defaultdict(list)if coco_json_data is not None:print('loading annotations into memory...')tic = time.time()if isinstance(coco_json_data, str):with open(coco_json_data, 'r') as f:dataset = json.load(f)assert type(dataset) == dict, 'annotation file format {} not supported'.format(type(dataset))print('Done (t={:0.2f}s)'.format(time.time() - tic))else:dataset = coco_json_dataself.dataset = datasetself.createIndex()def loadRes(self, predetect_json_data):import copy"""Load result file and return a result api object.:param   resFile (str)     : file name of result file:return: res (obj)         : result api object"""res = COCO_modify()res.dataset['images'] = [img for img in self.dataset['images']]print('Loading and preparing results...')tic = time.time()if isinstance(predetect_json_data, str):with open(predetect_json_data, 'r') as f:anns = json.load(f)print('Done (t={:0.2f}s)'.format(time.time() - tic))else:anns = predetect_json_dataassert type(anns) == list, 'results in not an array of objects'annsImgIds = [ann['image_id'] for ann in anns]assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), \'Results do not correspond to current coco set'if 'caption' in anns[0]:imgIds = set([img['id'] for img in res.dataset['images']]) & set([ann['image_id'] for ann in anns])res.dataset['images'] = [img for img in res.dataset['images'] if img['id'] in imgIds]for id, ann in enumerate(anns):ann['id'] = id + 1elif 'bbox' in anns[0] and not anns[0]['bbox'] == []:res.dataset['categories'] = copy.deepcopy(self.dataset['categories'])for id, ann in enumerate(anns):bb = ann['bbox']x1, x2, y1, y2 = [bb[0], bb[0] + bb[2], bb[1], bb[1] + bb[3]]if not 'segmentation' in ann:ann['segmentation'] = [[x1, y1, x1, y2, x2, y2, x2, y1]]ann['area'] = bb[2] * bb[3]ann['id'] = id + 1ann['iscrowd'] = 0elif 'segmentation' in anns[0]:res.dataset['categories'] = copy.deepcopy(self.dataset['categories'])for id, ann in enumerate(anns):# now only support compressed RLE format as segmentation resultsann['area'] = maskUtils.area(ann['segmentation'])if not 'bbox' in ann:ann['bbox'] = maskUtils.toBbox(ann['segmentation'])ann['id'] = id + 1ann['iscrowd'] = 0elif 'keypoints' in anns[0]:res.dataset['categories'] = copy.deepcopy(self.dataset['categories'])for id, ann in enumerate(anns):s = ann['keypoints']x = s[0::3]y = s[1::3]x0, x1, y0, y1 = np.min(x), np.max(x), np.min(y), np.max(y)ann['area'] = (x1 - x0) * (y1 - y0)ann['id'] = id + 1ann['bbox'] = [x0, y0, x1 - x0, y1 - y0]print('DONE (t={:0.2f}s)'.format(time.time() - tic))res.dataset['annotations'] = annsres.createIndex()return rescoco_json_data = coco_json_path if coco_json_path is not None else self.coco_jsoncocoGt = COCO_modify(coco_json_data)  # 标注文件的路径及文件名,json文件形式predetect_json_data = predetect_json_path if predetect_json_path is not None else self.predetect_jsoncocoDt = cocoGt.loadRes(predetect_json_data)  # 自己的生成的结果的路径及文件名,json文件形式cocoEval = COCOeval(cocoGt, cocoDt, "bbox")cocoEval.evaluate()cocoEval.accumulate()cocoEval.summarize()def get_img_root_lst(self, root_data):import osimg_root_lst = []for dir, file, names in os.walk(root_data):img_lst = [os.path.join(dir, name) for name in names if name[-3:] in self.img_format]img_root_lst = img_root_lst + img_lstfor na in img_lst:  # 图片名字映射image_idself.image_id += 1self.imgname_map_id[self.get_strfile(na)] = self.image_idreturn img_root_lst  # 得到图片绝对路径def get_strfile(self, file_str, pos=-1):'''得到file_str / or \\ 的最后一个名称'''endstr_f_filestr = file_str.split('\\')[pos] if '\\' in file_str else file_str.split('/')[pos]return endstr_f_filestrdef read_xml(self, xml_root):''':param xml_root: .xml文件:return: dict('cat':['cat1',...],'bboxes':[[x1,y1,x2,y2],...],'whd':[w ,h,d])'''import xml.etree.ElementTree as ETimport osdict_info = {'cat': [], 'bboxes': [], 'box_wh': [], 'whd': []}if os.path.splitext(xml_root)[-1] == '.xml':tree = ET.parse(xml_root)  # ET是一个xml文件解析库,ET.parse()打开xml文件。parse--"解析"root = tree.getroot()  # 获取根节点whd = root.find('size')whd = [int(whd.find('width').text), int(whd.find('height').text), int(whd.find('depth').text)]xml_filename = root.find('filename').textdict_info['whd'] = whddict_info['xml_filename'] = xml_filenamefor obj in root.findall('object'):  # 找到根节点下所有“object”节点cat = str(obj.find('name').text)  # 找到object节点下name子节点的值(字符串)bbox = obj.find('bndbox')x1, y1, x2, y2 = [int(bbox.find('xmin').text),int(bbox.find('ymin').text),int(bbox.find('xmax').text),int(bbox.find('ymax').text)]b_w = x2 - x1 + 1b_h = y2 - y1 + 1dict_info['cat'].append(cat)dict_info['bboxes'].append([x1, y1, x2, y2])dict_info['box_wh'].append([b_w, b_h])else:print('[inexistence]:{} suffix is not xml '.format(xml_root))return dict_infodef xml2cocojson(self, xml_root):'''处理1个xml,将其真实json保存到self.coco_json中'''assert len(self.coco_json['categories']) > 0, 'self.coco_json[categories] must exist v'categories = [cat_info['name'] for cat_info in  self.coco_json['categories']]xml_info = self.read_xml(xml_root)if len(xml_info['cat']) > 0:xml_filename = xml_info['xml_filename']xml_name = self.get_strfile(xml_root)img_name = xml_name[:-3] + xml_filename[-3:]# 转为coco json时候,若add_file为True则在coco json文件的file_name增加文件夹名称+图片名字image_id = self.imgname_map_id[img_name]w, h, d = xml_info['whd']# 构建json文件字典image_json = {'file_name': img_name, 'height': h, 'width': w, 'id': image_id}ann_json = []for i, category in enumerate(xml_info['cat']):# 表示有box存在,可以添加images信息category_id = categories.index(category) + 1  # 给出box对应标签索引为类self.anation_id = self.anation_id + 1xmin, ymin, xmax, ymax = xml_info['bboxes'][i]o_width, o_height = xml_info['box_wh'][i]if (xmax <= xmin) or (ymax <= ymin):print('code:[{}] will be abandon due to  {} min of box w or h more than max '.format(category,xml_root))  # 打印错误的boxelse:ann = {'area': o_width * o_height, 'iscrowd': 0, 'image_id': image_id,'bbox': [xmin, ymin, o_width, o_height],'category_id': category_id, 'id': self.anation_id, 'ignore': 0,'segmentation': []}ann_json.append(ann)if len(ann_json) > 0:  # 证明存在 annotationfor ann in ann_json:  self.coco_json['annotations'].append(ann)self.coco_json['images'].append(image_json)def detect2json(self, predetect_result, img_name,score_thr=-1):'''predetect_result:为列表,每个列表中包含[x1, y1, x2, y2, score, label]img_name: 图片的名字'''if len(predetect_result) > 0:categories = [cat_info['name'] for cat_info in  self.coco_json['categories']]for result in predetect_result:x1, y1, x2, y2, score, label = resultif score>score_thr:w, h = int(x2 - x1), int(y2 - y1)x1, y1 = int(x1), int(y1)img_name_new = self.get_strfile(img_name)image_id = self.imgname_map_id[img_name_new]category_id = list(categories).index(label) + 1detect_json = {"area": w * h,"iscrowd": 0,"image_id": image_id,"bbox": [x1,y1,w,h],"category_id": category_id,"id": image_id,"ignore": 0,"segmentation": [],"score": score}self.predetect_json.append(detect_json)def write_json(self,out_dir):import osimport jsoncoco_json_path=os.path.join(out_dir,'coco_json_data.json')with open(coco_json_path, 'w') as f:json.dump(self.coco_json, f, indent=4)  # indent表示间隔长度predetect_json_path=os.path.join(out_dir,'predetect_json_data.json')with open(predetect_json_path, 'w') as f:json.dump(self.predetect_json, f, indent=4)  # indent表示间隔长度

原理参考博客点击这里

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

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

相关文章

比较器的工作原理及性能指标介绍

一、什么是比较器 比较器的功能是比较两个或更多数据项&#xff0c;以确定它们是否相等&#xff0c;或者确定它们之间的大小关系和排列顺序&#xff0c;这称为比较。可以实现此比较功能的电路或设备称为比较器。比较器是将模拟电压信号与参考电压进行比较的电路。比较器的两个…

解读GIS软件:从ArcGIS到山海鲸可视化的全方位介绍

在现代社会&#xff0c;地理信息系统&#xff08;GIS&#xff09;的应用已经渗透到了各个领域&#xff0c;为我们提供了丰富的地理数据分析和可视化工具。下面介绍几款常见的GIS工具软件&#xff0c;一起来了解它们的特点和优势。 1. ArcGIS: ArcGIS由Esri公司开发&#xff0c;…

使用Spring Boot和Kafka实现消息订阅和发送

文章目录 一&#xff0c;新建Spring Boot1&#xff0c;Maven配置2&#xff0c;无法识别为SpringBoot项目3&#xff0c;无效的源发行版4&#xff0c;无法访问SpringApplication5&#xff0c;运行直接Finish6&#xff0c;服务运行成功 二&#xff0c;安装启动Kafka1&#xff0c;下…

20 MySQL(下)

文章目录 视图视图是什么定义视图查看视图删除视图视图的作用 事务事务的使用 索引查询索引创建索引删除索引聚集索引和非聚集索引影响 账户管理&#xff08;了解非DBA&#xff09;授予权限 与 账户的相关操作 MySQL的主从配置 视图 视图是什么 通俗的讲&#xff0c;视图就是…

Web开发模式、API接口、restful规范、序列化和反序列化、drf安装和快速使用、路由转换器(复习)

一 Web开发模式 1. 前后端混合开发模式 前后端混合开发模式是一种开发方式&#xff0c;将前端和后端的开发工作结合在一起&#xff0c;以加快项目的开发速度和 提高协作效率。这种模式通常用于快速原型开发、小型项目或敏捷开发中。在前后端混合开发模式中&#xff0c;前端和…

CATIA Composer R2023安装教程

软件下载 软件&#xff1a;CATIA Composer版本&#xff1a;2023语言&#xff1a;简体中文大小&#xff1a;1.82G安装环境&#xff1a;Win11/Win10/Win8/Win7硬件要求&#xff1a;CPU2.60GHz 内存8G(或更高&#xff09;下载通道①百度网盘丨64位下载链接&#xff1a;https://pa…

汇编-内中断

中断的意思是指, CPU不再接着(刚执行完的指令) 向下执行, 而是转去处理这个特殊信息。 内中断的产生 8086CPU,当CPU内部有下面的情况发生的时候, 将产生相应的中断信息: (1)除法错误, 比如, 执行div指令产生的除法溢出; (2)单步执行;   (3)执行into指令; (4)执…

文件上传漏洞-upload靶场3-4(全网最详细解读)

文件上传漏洞-upload靶场3-4关通关笔记&#xff08;全网最详细解读&#xff09; upload 第三关&#xff08;特殊后缀&#xff09; 思路 按照第一关和第二关的思路&#xff0c;先随便上传一个文件用burpsuite工具抓包&#xff0c;看它到底是前段验证还是后端验证。 上传一个we…

社招中级前端笔试面试题总结

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 typeof null 的结果是什么&#xff0c;为什么&#xff1f; typeof null 的结果是Object。 在 JavaScript 第一个版本中&#xff0c;所有值都存储在 32…

网络编程 day 3

1、UDP下载 #include<myhead.h>#define ERR_MSG(msg) do{\fprintf(stderr, "__%d__:", __LINE__); \perror(msg);\ }while(0)#define SER_PORT 8888 //端口号&#xff0c;范围1024~49151 #define SET_IP "192.168.114.85" //本机…

javaee spring 静态代理

静态代理 package com.test.staticProxy;public interface IUsersService {public void insert(); }package com.test.staticProxy;//目标类 public class UsersService implements IUsersService {Overridepublic void insert() {System.out.println("添加用户");…

全网首发,人体姿态估计算法在OK3588上部署应用(十三)

一、主机模型转换 采用FastDeploy来部署应用深度学习模型到OK3588板卡上 进入主机Ubuntu的虚拟环境 conda activate ok3588 主机环境搭建可以参考上一篇 《OK3588板卡实现人像抠图&#xff08;十二&#xff09;》 生成onnx文件 cd FastDeploy # 下载Paddle静态图模型并解压…

Vision Transformer(vit)原理分析以及特征可视化

目录 Vit简介 Vit model结构图 vit输入处理 图像分块 class token与position的添加 特征提取 vit代码 Vit简介 Vision Transformer&#xff08;ViT&#xff09;是一种基于Transformer架构的深度学习模型&#xff0c;用于图像识别和计算机视觉任务。与传统的卷积神经网络…

WebGL模型矩阵

前言&#xff1a;依赖矩阵库 WebGL矩阵变换库_山楂树の的博客-CSDN博客 先平移&#xff0c;后旋转的模型变换&#xff1a; 1.将三角形沿着X轴平移一段距离。 2.在此基础上&#xff0c;旋转三角形。 先写下第1条&#xff08;平移操作&#xff09;中的坐标方程式。 等式1&am…

如何将 PDF 转换为 Word:前 5 个应用程序

必须将 PDF 转换为 Word 才能对其进行编辑和自定义。所以这里有 5 种很棒的方法 PDF 文件被广泛使用&#xff0c;因为它非常稳定且难以更改。这在处理法律合同、财务文件和推荐信等重要文件时尤其重要。但是&#xff0c;有时您可能需要编辑 PDF 文件。最好的方法是使用应用程序…

openGauss学习笔记-54 openGauss 高级特性-MOT

文章目录 openGauss学习笔记-54 openGauss 高级特性-MOT54.1 MOT特性及价值54.2 MOT关键技术54.3 MOT应用场景54.4 不支持的数据类型54.5 使用MOT54.6 将磁盘表转换为MOT openGauss学习笔记-54 openGauss 高级特性-MOT openGauss引入了MOT&#xff08;Memory-Optimized Table&…

读书笔记——《万物有灵》

前言 上一本书是《走出荒野》&#xff0c;太平洋步道女王提到了这本书《万物有灵》&#xff0c;她同样是看一点撕一点的阅读。我想&#xff0c;在她穿越山河森林&#xff0c;听见鸟鸣溪流的旅行过程中&#xff0c;是不是看这本描写动物有如何聪明的书——《万物有灵》&#xf…

vue中解决ajax跨域问题(no “access-control-allow-origin”)

文章目录 跨域报错信息产生原因举例解决方法方式一优缺点方式二优缺点 跨域报错信息 产生原因 跨域是是因为浏览器的同源策略限制&#xff0c;是浏览器的一种安全机制&#xff0c;服务端之间是不存在跨域的。 所谓同源指的是两个页面具有相同的协议、主机和端口&#xff0c;三…

R语言空气污染数据的地理空间可视化和分析:颗粒物2.5(PM2.5)和空气质量指数(AQI)...

原文链接&#xff1a;http://tecdat.cn/?p23800 由于空气污染对公众健康的不利影响&#xff0c;人们一直非常关注。世界各国的环境部门都通过各种方法&#xff08;例如地面观测网络&#xff09;来监测和评估空气污染问题&#xff08;点击文末“阅读原文”获取完整代码数据&…

可拖动表格

支持行拖动&#xff0c;列拖动 插件&#xff1a;sortablejs UI: elementUI <template><div><hr style"margin: 30px 0;"><div><!-- 数据里面要有主键id&#xff0c; 否则拖拽异常 --><h2 style"margin-bottom: 30px&qu…