森林护卫者:无人机航拍图像火情监测
本项目旨在将无人机技术与计算机视觉技术相结合,开发一套应用于无人机森林火灾监测的高效林火识别与火灾区域分割系统。
>一、项目背景
森林火灾是一种突发性强、处置救助较为困难的自然灾害,具有火情隐蔽且易热累积等特点。其破坏性巨大,对森林生态系统与人类健康生活有较强的危害。据相关资料显示,近年来全世界每年平均发生森林火灾20余万次,烧毁的森林覆盖总面积数百万公顷,造成大量的人员伤亡和巨额的经济损失。为应对森林火灾,森林消防员时刻承担着巨大的生命危险。然而,森林环境复杂,火情易受天气原因突变。因此,对森林火情态势迅速、准确地检测预警,以进行及时资源调度、采取扑救措施,有着十分重要的意义。
| |
| |
目前,森林火灾的主要监测手段以消防瞭望塔、人工巡检以及视频监控系统为主。消防瞭望塔与人工巡检在传统森林防火中扮演重要角色,主要依靠人工经验观察,主观性强,准确性低。且其还受地形和天气的限制,覆盖范围小,检测中存在盲点和缺口。如遇雾霾或夜晚,则观测受阻。视频监控系统是传统城市监控方式的延伸,依赖于有限监控设备的高密度布设,但对于大森林环境,监控领域有限,难以覆盖全境。近年来新提出的卫星森林火警系统,虽然其监控范围广,但星载观测设备的空间分辨率较低,难以精确定位,易受天气、云层以及轨道周期的影响。以上几种森林防火对热积累、弱明火区域的快速检测可能性较小,有效性受限。
近年来,由于越来越多科研、生产力量的投入,无人机研产加速,消费级市场的不断开拓创新,被更多行业市场认可。作为飞行平台,无人机可以搭载多种设备,对各种行业应用都具有较高可适性。随着飞控技术的发展,无人机以其结构简单、飞行灵活、成本低的优势逐渐应用于各种应急救援领域。
二、应用场景
本项目面向无人机平台,同时可以搭载热红外相机等机载设备进行协同工作,实时监测森林火情。
- 该系统机动性能强,能够在空中俯瞰监测区域,监测范围广,通过规划合理的巡检航拍轨迹,能快速全面获取被测森林的实时监测图像。
- 基于深度学习方法的火情识别分析,系统能捕捉到隐藏于复杂环境深处的微弱明火区域,从而实现初期火情的有效预警,降低损失,具备更高的时效性。
|
|
三、数据集
本项目基于北亚利桑那大学学者公开的森林火情航拍图像数据集FLAME,该数据集拍摄的地点是在亚利桑那州松树林,由研究人员操作无人机在当地规定的燃烧堆积杂物期间收集。该数据集包含的内容比较丰富,原始数据为视频格式,作者又进一步从视频中分割图像帧并进行标注,最终形成两部分图像数据集,可分别用于图像分类和分割研究。。
|
|
FLAME分类数据集结构如下图4所示。其中分类数据集包含训练集和测试集两部分,训练集包括Fire和No_Fire两类共计39375张jpg格式图片,其中Fire类25018帧,No_Fire类14357帧。测试集数量为8617,解压后用于分类模型训练的数据集同级目录下包括Training、Test两个文件夹。
FLAME分割数据集包含训练集、验证集和测试集三部分,数量比为501:102:1400,共计2003张带标注图像。
|
|
四、模型介绍
本项目面向无人机移动平台应用,因此我们优先选择轻量级模型进行开发,目前通过结果对比我们最终选择使用EfficientNetB0和BiSeNetV2。
接下来将分别展示火情图像分类和火灾区域分割两部分内容。
五、火情识别
相关库安装
# 安装paddleclas以及相关三方包
!git clone https://gitee.com/paddlepaddle/PaddleClas.git -b release/2.2
5.1 数据处理
解压Classification数据集到PaddleClas/dataset文件夹
- 需要注意的是:
- 数据集存放的位置与生成的数据列表文件中的数据路径需要与配置文件对应,这也是初学者时常出现问题的地方。
- 数据列表文件中路径与标签之间的分割符号,行与行之间的换行符号
- 有些特定字符转义之后出现问题
PaddleClas下载并安装相关依赖库
!mkdir /home/aistudio/PaddleClas/dataset/Fire
!unzip -q data/data106443/Training.zip
!mv Training PaddleClas/dataset/Fire/Training
!unzip -q data/data106443/Test.zip
!mv Test PaddleClas/dataset/Fire
数据集划分
原始数据集:训练集:39375 测试集:8617
按照4:1重新划分训练集:验证集 = 31395 :7980
#生成对应训练集、验证集及测试集路径对应文本文件
import codecs
import os
import random
import shutil
from PIL import Imagetrain_ratio = 4.0 / 5
data_dir = 'PaddleClas/dataset/Fire/'
all_file_dir= 'PaddleClas/dataset/Fire/Training'
class_list = [c for c in os.listdir(all_file_dir) if os.path.isdir(os.path.join(all_file_dir ,c)) and not c.endswith('Set') and not c.startswith('.')]
class_list.sort()
print(class_list) #['Fire', 'No_Fire']train_image_dir = os.path.join(data_dir, "trainImageSet")
if not os.path.exists(train_image_dir):os.makedirs(train_image_dir)eval_image_dir = os.path.join(data_dir, "evalImageSet")
if not os.path.exists(eval_image_dir):os.makedirs(eval_image_dir)train_file = codecs.open(os.path.join(data_dir, "train.txt"), 'w')
eval_file = codecs.open(os.path.join(data_dir, "eval.txt"), 'w')
label_id = 1with codecs.open(os.path.join(data_dir, "label_list.txt"), "w") as label_list:for class_dir in class_list: label_list.write("{0}\t{1}\n".format(label_id, class_dir))index = 0image_path_pre = os.path.join(all_file_dir, class_dir)print(image_path_pre)for file in os.listdir(image_path_pre):try:img = Image.open(os.path.join(image_path_pre, file))index = index + 1if random.uniform(0, 1) <= train_ratio:shutil.copyfile(os.path.join(image_path_pre, file), os.path.join(train_image_dir, file))train_file.write("{0} {1}\n".format(os.path.join("trainImageSet", file), label_id))else:shutil.copyfile(os.path.join(image_path_pre, file), os.path.join(eval_image_dir, file))eval_file.write("{0} {1}\n".format(os.path.join("evalImageSet", file), label_id))except Exception as e:pass label_id = label_id - 1print(index)train_file.close()
eval_file.close()
#images number 39375:8617
['Fire', 'No_Fire']
PaddleClas/dataset/Fire/Training/Fire
25018
PaddleClas/dataset/Fire/Training/No_Fire
14357
#写一个测试集txt作为批量评估结果
import codecs
import os
import random
import shutil
from PIL import Imagedata_dir = 'dataset/Fire/'
all_file_dir= 'dataset/Fire/Test'
class_list = [c for c in os.listdir(all_file_dir) if os.path.isdir(os.path.join(all_file_dir ,c)) and not c.endswith('Set') and not c.startswith('.')]
class_list.sort()
print(class_list) #['Fire', 'No_Fire']test_file = codecs.open(os.path.join(data_dir, "test.txt"), 'w')
label_id = 1with codecs.open(os.path.join(data_dir, "label_list.txt"), "r") as label_list:for class_dir in class_list:index = 0 #label_list.write("{0}\t{1}\n".format(label_id, class_dir))image_path_pre = os.path.join(all_file_dir, class_dir)print(image_path_pre)for file in os.listdir(image_path_pre):try:#img = Image.open(os.path.join(image_path_pre, file))index = index + 1test_file.write("{0} {1}\n".format(os.path.join(image_path_pre, file), label_id))except Exception as e:passprint(label_id, index)label_id = label_id - 1test_file.close()
#images number 39375:8617
['Fire', 'No_Fire']
dataset/Fire/Test/Fire
1 5137
dataset/Fire/Test/No_Fire
0 3480
5.2 模型训练
一些尝试用过的模型
!python3 tools/train.py \-c ./ppcls/configs/quick_start/professional/ResNet50_vd_CIFAR100.yaml \-o Global.output_dir="output_Fire" \-o Arch.pretrained=True \-o Arch.use_ssld=True!python3 tools/train.py \-c ./ppcls/configs/quick_start/ResNet50_vd.yaml \-o Arch.pretrained=False \-o Global.device=gpu!python3 tools/train.py \-c ./ppcls/configs/quick_start/Res2Net200_vd.yaml \-o Global.output_dir="output_Fire_mix" \-o Global.checkpoints='./output_Fire_mix/Res2Net200_vd_26w_4s/latest'
修改配置文件:最基本的是修改路径、num_worker、epoch等
# global configs
Global:checkpoints: nullpretrained_model: nulloutput_dir: ./output/device: gpusave_interval: 50eval_during_train: Trueeval_interval: 1epochs: 400print_batch_step: 10use_visualdl: False# used for static mode and model exportimage_shape: [3, 224, 224]save_inference_dir: ./inference# model architecture
Arch:name: EfficientNetB0class_num: 2# loss function config for traing/eval process
Loss:Train:- CELoss:weight: 1.0epsilon: 0.1Eval:- CELoss:weight: 1.0Optimizer:name: RMSPropmomentum: 0.9rho: 0.9epsilon: 0.001lr:name: Cosinelearning_rate: 0.032regularizer:name: 'L2'coeff: 0.00001# data loader for train and eval
DataLoader:Train:dataset:name: ImageNetDatasetimage_root: ./dataset/Fire/cls_label_path: ./dataset/Fire/train.txttransform_ops:- DecodeImage:to_rgb: Truechannel_first: False- RandCropImage:size: 224- RandFlipImage:flip_code: 1- AutoAugment:- NormalizeImage:scale: 1.0/255.0mean: [0.485, 0.456, 0.406]std: [0.229, 0.224, 0.225]order: ''sampler:name: DistributedBatchSamplerbatch_size: 64drop_last: Falseshuffle: Trueloader:num_workers: 0use_shared_memory: TrueEval:dataset: name: ImageNetDatasetimage_root: ./dataset/Fire/cls_label_path: ./dataset/Fire/eval.txttransform_ops:- DecodeImage:to_rgb: Truechannel_first: False- ResizeImage:resize_short: 256- CropImage:size: 224- NormalizeImage:scale: 1.0/255.0mean: [0.485, 0.456, 0.406]std: [0.229, 0.224, 0.225]order: ''sampler:name: DistributedBatchSamplerbatch_size: 128drop_last: Falseshuffle: Falseloader:num_workers: 0use_shared_memory: TrueInfer:infer_imgs: docs/images/whl/demo.jpgbatch_size: 10transforms:- DecodeImage:to_rgb: Truechannel_first: False- ResizeImage:resize_short: 256- CropImage:size: 224- NormalizeImage:scale: 1.0/255.0mean: [0.485, 0.456, 0.406]std: [0.229, 0.224, 0.225]order: ''- ToCHWImage:PostProcess:name: Topktopk: 1class_id_map_file: ./dataset/Fire/label_list.txtMetric:Train:- TopkAcc:topk: [1]Eval:- TopkAcc:topk: [1]
#环境设置
%cd PaddleClas
import os
os.environ['PYTHONPATH']="/home/aistudio/PaddleClas"
!python3 tools/train.py \-c ./ppcls/configs/quick_start/efficientB0.yaml \-o Global.output_dir="output_Fire" \-o Arch.pretrained=True \-o Arch.use_ssld=True \-o Global.checkpoints='./output_Fire/EfficientNetB0/latest'
5.3 模型验证
!python3 tools/eval.py \-c dataset/Fire/testeff.yaml \-o Global.pretrained_model=./output_Fire/EfficientNetB0/best_model
#[2021/10/26 17:09:22] root INFO: [Eval][Epoch 0][Avg]CELoss: 0.66194, loss: 0.66194, top1: 0.71069 best_model
#[2021/10/19 16:21:07] root INFO: [Eval][Epoch 0][Avg]CELoss: 0.6413, loss: 0.65825, top1: 0.72786 best_model
#[2021/10/26 17:20:31] root INFO: [Eval][Epoch 0][Avg]CELoss: 0.65331, loss: 0.65331, top1: 0.70721 latest
#[2021/10/27 08:11:27] root INFO: [Eval][Epoch 0][Avg]CELoss: 0.65825, loss: 0.65825, top1: 0.71777 best_model
一些模型记录
#开始评估
!python3 tools/eval.py \-c dataset/Fire/test.yaml \-o Global.pretrained_model=./output/ResNet50_vd/best_model
#[2021/10/06 12:34:49] root INFO: [Eval][Epoch 0][Avg]CELoss: 1.27037, loss: 1.27037, top1: 0.65394#开始评估
!python3 tools/eval.py \-c /home/aistudio/work/eval.yaml \-o Global.pretrained_model=./output_Fire_mix/MobileNetV3_large_x1_0/best_model
#[2021/10/13 23:31:57] root INFO: [Eval][Epoch 0][Avg]CELoss: 0.75095, loss: 0.75095, top1: 0.40385#开始评估
!python3 tools/eval.py \-c dataset/Fire/testplus.yaml \-o Global.pretrained_model=./output_Fire/ResNet50_vd/best_model
#[2021/10/13 23:27:52] root INFO: [Eval][Epoch 0][Avg]CELoss: 1.86942, loss: 1.86942, top1: 0.61181!python3 tools/eval.py \-c dataset/Fire/testres2net.yaml \-o Global.pretrained_model=./output_Fire_mix/Res2Net200_vd_26w_4s/best_model
#[2021/10/13 23:26:13] root INFO: [Eval][Epoch 0][Avg]CELoss: 0.68440, loss: 0.68440, top1: 0.54033
!python3 tools/infer.py \-c ./ppcls/configs/quick_start/efficientB0.yaml \-o Infer.infer_imgs=/home/aistudio/work/testimg/no/resized_test_nofire_frame21.jpg \-o Global.pretrained_model=./output_Fire/EfficientNetB0/best_model
5.4 模型导出
#模型推理-1 导出
!python tools/export_model.py \-c ./ppcls/configs/quick_start/efficientB0.yaml \-o Global.pretrained_model=./output_Fire/EfficientNetB0/best_model \-o Global.output_dir=/home/aistudio/work/classinf/ \-o Global.save_inference_dir=/home/aistudio/work/classinf/
5.5 结果展示
六、火灾区域分割
PaddleSeg下载并安装相关依赖库
%cd /home/aistudio/
# 安装PaddleSeg
! pip install paddleseg
#下载PaddleSeg代码
!git clone https://gitee.com/PaddlePaddle/PaddleSeg.git
!pip install -r PaddleSeg/requirements.txt
6.1 数据处理
解压Segmentation数据集及Masks
!rar x data/data106593/Images01.rar PaddleSeg/data/FireSeg/train/
!rar x data/data106593/Images02.rar PaddleSeg/data/FireSeg/val/
!rar x data/data106593/Images03.rar PaddleSeg/data/FireSeg/test/!unzip -d /home/aistudio/PaddleSeg/data/FireSeg/ /home/aistudio/255Masks.zip
数据可视化
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
from PIL import Image
import cv2
import pandas as pd
import matplotlib.pyplot as plt
imgpath = '/home/aistudio/PaddleSeg/data/FireSeg/Masks/image_1008.png'print(imgpath)
img = Image.open(imgpath)
img = img.convert('L')plt.figure("Image")
# 这里必须加 cmap='gray' ,否则尽管原图像是灰度图(下图1),但是显示的是伪彩色图像(下图2)(如果不加的话)
plt.imshow(img,cmap='gray')
plt.axis('on')
plt.title('image')
plt.show()
/home/aistudio/PaddleSeg/data/FireSeg/Masks/image_1008.png
生成数据集索引列表
!pwd
%cd PaddleSeg
/home/aistudio
/home/aistudio/PaddleSeg
import os
from tqdm import tqdm
from random import shuffledataset = 'data/FireSeg'
train_txt = os.path.join(dataset, 'train_list.txt')
val_txt = os.path.join(dataset, 'val_list.txt')
test_txt = os.path.join(dataset, 'test_list.txt')
lbl_txt = os.path.join(dataset, 'labels.txt')classes = ['Fire','background']with open(lbl_txt, 'w') as f:for l in classes:f.write(l+'\n')xml_base = 'Masks'
train_img_base = 'train'
eval_img_base = 'val'
test_img_base = 'test'trains = [v for v in os.listdir(os.path.join(dataset, train_img_base)) if v.endswith('.jpg')]
#xmls.sort(key=lambda x: int(x[6:-4]))
shuffle(trains) #随机打乱vals = [v for v in os.listdir(os.path.join(dataset, eval_img_base)) if v.endswith('.jpg')]
#xmls.sort(key=lambda x: int(x[6:-4]))
shuffle(vals) #随机打乱tests = [v for v in os.listdir(os.path.join(dataset, test_img_base)) if v.endswith('.jpg')]
#xmls.sort(key=lambda x: int(x[6:-4]))
shuffle(tests) #随机打乱with open(train_txt, 'w') as f:for x in tqdm(trains):m = x[:-4]+'.png'xml_path = os.path.join(xml_base, m)img_path = os.path.join(train_img_base, x)f.write('{} {}\n'.format(img_path, xml_path))with open(val_txt, 'w') as f:for x in tqdm(vals):m = x[:-4]+'.png'xml_path = os.path.join(xml_base, m)img_path = os.path.join(eval_img_base, x)f.write('{} {}\n'.format(img_path, xml_path))with open(test_txt, 'w') as f:for x in tqdm(tests):m = x[:-4]+'.png'xml_path = os.path.join(xml_base, m)img_path = os.path.join(test_img_base, x)f.write('{} {}\n'.format(img_path, xml_path))
100%|██████████| 501/501 [00:00<00:00, 21850.10it/s]
100%|██████████| 102/102 [00:00<00:00, 192278.21it/s]
100%|██████████| 1400/1400 [00:00<00:00, 261211.10it/s]
6.2 模型训练
修改配置文件
batch_size: 4
iters: 200train_dataset:type: OpticDiscSegdataset_root: data/Firesegtransforms:- type: Resizetarget_size: [512, 512]- type: RandomHorizontalFlip- type: Normalizemode: trainval_dataset:type: OpticDiscSegdataset_root: data/Firesegtransforms:- type: Resizetarget_size: [512, 512]- type: Normalizemode: valoptimizer:type: sgdmomentum: 0.9weight_decay: 4.0e-5lr_scheduler:type: PolynomialDecaylearning_rate: 0.01end_lr: 0power: 0.9loss:types:- type: CrossEntropyLosscoef: [1, 1, 1, 1, 1]model:type: BiSeNetV2pretrained: Null
!python train.py \--config /home/aistudio/work/seg/bisenet_optic_disc_512x512_1k.yml \--do_eval \ #表示一遍训练一遍验证--save_interval 200 \ #表示每经过200个iters,进行一个模型的保存--save_dir /home/aistudio/v1output #模型保存路径
#--resume_model output/iter_6000 \
6.3 模型验证
!python val.py \--config /home/aistudio/work/seg/bisenet_optic_disc_512x512_1k.yml \--model_path /home/aistudio/v1output/best_model/model.pdparams
6.3 模型预测
! python predict.py --config /home/aistudio/work/seg/bisenet_optic_disc_512x512_1k.yml \--model_path /home/aistudio/v1output/best_model/model.pdparams \--image_path data/FireSeg/test/image_1088.jpg \--save_dir /home/aistudio/segresult/
6.3 模型导出
为了方便用户进行工业级的部署,PaddleSeg提供了一键动转静的功能,即将训练出来的动态图模型文件转化成静态图形式。
!python /home/aistudio/PaddleSeg/export.py -h
!python export.py \--config /home/aistudio/work/seg/bisenet_optic_disc_512x512_1k.yml \--model_path /home/aistudio/v1output/best_model/model.pdparams \--save_dir /home/aistudio/v1output/inference
- 结果文件
output├── deploy.yaml # 部署相关的配置文件├── model.pdiparams # 静态图模型参数├── model.pdiparams.info # 参数额外信息,一般无需关注└── model.pdmodel # 静态图模型文件
6.3 模型推理
! python deploy/python/infer.py \--config output/deploy.yaml \--image_path /home/aistudio/work/image_577.jpg \
无需关注└── model.pdmodel # 静态图模型文件
6.3 模型推理
! python deploy/python/infer.py \--config output/deploy.yaml \--image_path /home/aistudio/work/image_577.jpg \--save_dir /home/aistudio/seg_infer_result/
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/setuptools/depends.py:2: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative usesimport imp
Traceback (most recent call last):File "deploy/python/infer.py", line 215, in <module>main(args)File "deploy/python/infer.py", line 206, in mainraise RuntimeError("The installed Paddle doesn't support GPU."
RuntimeError: The installed Paddle doesn't support GPU.Please change to use CPU or reinstall Paddle.
6.4 部分结果展示
七、总结
本项目设计了一套面向无人机应用的火情监测系统,提出基于百度EdgeBoard作为端侧部署平台,同时搭载EfficientNetB0和BiSeNetV2模型,实现针对无人机航拍森林图像的火灾识别和分割功能。在这个项目中我们使用PaddleClas分类套件和 PaddleSeg 分割套件对航拍森林图像进行火灾识别和火灾区域分割,现在的两个模型准确率还有较大的提升空间,后续可以进一步优化改进。