图片分割之_训练模型和预测

1. 说明

 本篇使用Mask R-CNN算法,以及十几张从网络上下载的香蕉图片,训练一个模型。用于识别图像中的香蕉,不同于苹果,桔子,香蕉从不同的角度看差异很大,尤其是三五根香蕉放连在一起,或者整把香蕉的形态和单根香蕉差异很大。可以算是一种识别起来相对困难的水平。

 下图是用训练好的模型识别出的香蕉图片,可以看到,基本识别正确。

 操作步骤可分为:安装工具,标注图片,修改源码,模型训练和模型预测。我的工作环境是Ubuntu,硬件有GPU支持,操作过程中使用了python,图片标注工具,以及shell脚本。

2. 安装工具

(1) 下载程序源码

$ git clone https://github.com/matterport/Mask_RCNN.git # (大概200多M)

(2) 下载相关软件

$ sudo pip install opencv-python
$ sudo pip install tensorflow
$ sudo pip install scikit-image
$ sudo pip install keras==2.0.8
$ sudo pip install labelme # 标注工具

3. 标注图片

(1) 收集图片

 香蕉图片可以从网上下载,也可用手机拍照,图片分辨率不用太高,1000x1000以下即可,如果分辨率太高,可用linux中的convert命令缩放。我使用的15张图片如下图所示:

 需要注意的是,图片需要包括香蕉的各个角度,以及常见的多根组合的几种形态。

(2) 用软件标注图片

$ labelme 图片文件名.jpg

 labelme为一个图形化的标注工具,使用左侧面板中的create polygons,将图片中所需识别的香蕉圈出来,如果某个点画错了,用Backspace可删除最后设置的点(用法类似于photoshop中的多边形套锁工具),标注完加入填入label名,这个名字后面在程序中会到,标注完注意保存文件,文件名默认为:图片名.json。锚点的细密程度请参考下图:

 标注无需粒度过细,Labelme工具比较智能,只要位置相近,就能把锚点自动贴近边界。好的工具让标注事半功倍,一般情况下,十几张图片半个多小时即可标注完成,另外,一个图中也可标注多个区域,label名都设置为banana即可。

(3) 解析和拆分标注文件

 使用labelme自带的 labelme_json_to_dataset 命令工具,可将 json 文件拆分成目录,目录中数据如下:

 一条命令可以转换一个图片,当图片多时,建议使用 shell 脚本处理,shell 脚本示例如下,请根据环境调整。

for file in `ls *.json`
do
    echo labelme_json_to_dataset $file
    labelme_json_to_dataset $file
done
mkdir ../labelme_json/
mv *_json ../labelme_json/

(4) mask文件转码

 由于不同版本的 labelme 生成的文件格式不同,有的mask是24位色,有的是8位色,用以下python程序看一下图片格式:

from PIL import Image
img = Image.open('label.png')
print(img.mode)

 如果image.mode是P,即8位彩色图像,直接使用即可,如果是其它格式,使用以下程序将其转换成8位图片:

Img_8 = img.convert("P") 
Img_8.save('xxx.png')

 将转换后的图片复制到另一文件夹即可,复制方法请参考以下shell脚本

mkdir ../cv2_mask
cd ../cv2_mask
for file in `ls ../labelme_json`
do
    echo 'cp ../labelme_json/'$file'/label.png '$file.png 
    cp '../ labelme_json /'$file'/label.png' $file.png 
done

(5) 调整目录结构

 把上述的原图放在pic目录中, 标注文件放在 json 目录中, 拆分后的标注文件放在 labelme_json 目录中,掩码mask放在cv2_mask目录中, 调整之后的目录结构如下图所示:

 其中的 mine 本例中所有程序和数据,数据放在data目录中,训练好的模型放在models目录中。

4. 训练和预测

(1) 训练模型

源码

import os
import sys
sys.path.append(xxxx) # 加入Mask_RCNN源码所在目录
import random
import math
import re
import time
import numpy as np
import cv2
import matplotlib
import matplotlib.pyplot as plt
import tensorflow as tf
from mrcnn.config import Config
from mrcnn import model as modellib,utils
from mrcnn import visualize
import yaml
from mrcnn.model import log
from PIL import Image

ROOT_DIR = os.getcwd()
MODEL_DIR = os.path.join(ROOT_DIR, "models")
iter_num=0
COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")

# 从网上下载训练好的基础模型
if not os.path.exists(COCO_MODEL_PATH):
    utils.download_trained_weights(COCO_MODEL_PATH)

# 配置
class ShapesConfig(Config):
    NAME = "shapes" # 命名
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1
    NUM_CLASSES = 1 + 1  # 背景一类,香蕉一类,共两类
    IMAGE_MIN_DIM = 320
    IMAGE_MAX_DIM = 384
    RPN_ANCHOR_SCALES = (8 * 6, 16 * 6, 32 * 6, 64 * 6, 128 * 6)
    TRAIN_ROIS_PER_IMAGE = 100 # Aim to allow ROI sampling to pick 33% positive ROIs
    STEPS_PER_EPOCH = 100
    VALIDATION_STEPS = 50

config = ShapesConfig()
config.display()

# 重写数据集
class DrugDataset(utils.Dataset):
    def get_obj_index(self, image):
        n = np.max(image)
        return n

    # 获取标签
    def from_yaml_get_class(self, image_id):
        info = self.image_info[image_id]
        with open(info['yaml_path']) as f:
            temp = yaml.load(f.read())
            labels = temp['label_names']
            del labels[0]
        return labels

    # 填充mask
    def draw_mask(self, num_obj, mask, image,image_id):
        info = self.image_info[image_id]
        for index in range(num_obj):
            for i in range(info['width']):
                for j in range(info['height']):
                    at_pixel = image.getpixel((i, j))
                    if at_pixel == index + 1:
                        mask[j, i, index] = 1
        return mask
    
    # 读入训练图片及其配置文件
    def load_shapes(self, count, img_floder, mask_floder, imglist, dataset_root_path):
        self.add_class("shapes", 1, "banana") # 自定义标签 
        for i in range(count):
            filestr = imglist[i].split(".")[0]
            mask_path = mask_floder + "/" + filestr + "_json.png"
            yaml_path = dataset_root_path + "labelme_json/" + filestr + "_json/info.yaml"
            cv_img = cv2.imread(dataset_root_path + "labelme_json/" + filestr + "_json/img.png")
            self.add_image("shapes", image_id=i, path=img_floder + "/" + imglist[i],
                           width=cv_img.shape[1], height=cv_img.shape[0], mask_path=mask_path, yaml_path=yaml_path)

    # 读取标签和配置 
    def load_mask(self, image_id):
        global iter_num
        print("image_id",image_id)
        info = self.image_info[image_id]
        count = 1  # number of object
        img = Image.open(info['mask_path'])
        num_obj = self.get_obj_index(img)
        mask = np.zeros([info['height'], info['width'], num_obj], dtype=np.uint8)
        mask = self.draw_mask(num_obj, mask, img,image_id)
        occlusion = np.logical_not(mask[:, :, -1]).astype(np.uint8)
        for i in range(count - 2, -1, -1):
            mask[:, :, i] = mask[:, :, i] * occlusion
            occlusion = np.logical_and(occlusion, np.logical_not(mask[:, :, i]))
        labels = []
        labels = self.from_yaml_get_class(image_id)
        labels_form = []
        for i in range(len(labels)):
            if labels[i].find("banana") != -1: # 自定义标签
                labels_form.append("banana")
        class_ids = np.array([self.class_names.index(s) for s in labels_form])
        return mask, class_ids.astype(np.int32)

#基础设置
dataset_root_path="data/"
img_floder = dataset_root_path + "pic" # 基本图片目录
mask_floder = dataset_root_path + "cv2_mask" # mask图片目录
imglist = os.listdir(img_floder)
count = len(imglist)

# 构造训练集
dataset_train = DrugDataset()
dataset_train.load_shapes(count, img_floder, mask_floder, imglist, dataset_root_path)
dataset_train.prepare()

# 构造验证集
dataset_val = DrugDataset()
dataset_val.load_shapes(7, img_floder, mask_floder, imglist, dataset_root_path)
dataset_val.prepare()

# 建立模型
model = modellib.MaskRCNN(mode="training", config=config,
                          model_dir=MODEL_DIR)

# 定义模式
init_with = "coco"  # imagenet, coco, or last

if init_with == "imagenet":
    model.load_weights(model.get_imagenet_weights(), by_name=True)
elif init_with == "coco":
    model.load_weights(COCO_MODEL_PATH, by_name=True,
                       exclude=["mrcnn_class_logits", "mrcnn_bbox_fc",
                                "mrcnn_bbox", "mrcnn_mask"])
elif init_with == "last":
    model.load_weights(model.find_last()[1], by_name=True)

model.train(dataset_train, dataset_val,
            learning_rate=config.LEARNING_RATE,
            epochs=10,
            layers='heads')

model.train(dataset_train, dataset_val,
            learning_rate=config.LEARNING_RATE / 10,
            epochs=30,
            layers="all")

运行程序

$ python train.py

 在程序运行过程中,如果因为tensorflow版本与mask_rcnn不匹配,引起找不到keepdims问题,需要修改 Mask_RCNN/mrcnn/model.py,将其中的keepdims改为keep_dims即可。
 我的机器训练完不到15分钟,如果把两次训练的迭代次数分别设成1和2则2分钟完成训练。

(2) 预测模型
源码

# -*- coding: utf-8 -*-

import os
import sys
sys.path.append(os.path.dirname(os.getcwd())) # 注意:加mask_rcnn目录
import skimage.io
from mrcnn.config import Config
from datetime import datetime 
import mrcnn.model as modellib
from mrcnn import visualize

ROOT_DIR = os.getcwd()
sys.path.append(ROOT_DIR)
MODEL_DIR = os.path.join(ROOT_DIR, "models")

# 配置,同train
class ShapesConfig(Config):
    NAME = "shapes"
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1
    NUM_CLASSES = 1 + 1
    IMAGE_MIN_DIM = 320
    IMAGE_MAX_DIM = 384
    RPN_ANCHOR_SCALES = (8 * 6, 16 * 6, 32 * 6, 64 * 6, 128 * 6)
    TRAIN_ROIS_PER_IMAGE =100
    STEPS_PER_EPOCH = 100
    VALIDATION_STEPS = 50

class InferenceConfig(ShapesConfig):
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1

config = InferenceConfig()
model = modellib.MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=config)
model.load_weights('models/shapes20190117T1428/mask_rcnn_shapes_0001.h5', by_name=True) # 注意换成你模型的路径
#model.load_weights('models/shapes20190117T1428/mask_rcnn_shapes_0030.h5', by_name=True) # 注意换成你模型的路径
#model.load_weights('mask_rcnn_coco.h5', by_name=True) # 注意换成你模型的路径

class_names = ['BG', 'banana']
image = skimage.io.imread('/tmp/banana.jpg') # 注意事换成你要识别的图片

a=datetime.now() 
results = model.detect([image], verbose=1)
b=datetime.now() 
print("@@ detect duration",(b-a).seconds, 'second')
r = results[0]
# 画图
visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'], 
                            class_names, r['scores'])

运行程序

$ python test.py

5. 分析总结

  • 自动标注:当图片数量很多时,可以先训练少量图片,生成模型,让模型自动标注,人为检查标注是否正确,对于不正确的人工重新标注。

  • 建议使用GPU:相比GPU,我用4核的CPU计算,速度目测差了50倍左右。个人觉得没有GPU,训练速度几乎是无法接受的。

  • 迭代次数:迭代次数可以调整,如果同一个图,用网上下载的基础模型完全识别不出。而用第1次迭代和第30次迭代结果差不太多,以后再训练就可以减少迭代次数,以节约时间。

  • 生成模型:由于迭代训练了30次,models目录下产生了30个模型文件,占空间比较大,不用的可以删除掉。

  • 更多例程请参考原代码中的 Mask_RCNN/samples/ 目录。

6. 问题及解决方法

  • 问题: 在CPU上运行时可能报错 SVD did not converge ,
    分析及解决:该问题发生成resize图片时,代码mrcnn/utils.py计算resize的scale里用两个int型相除,结果scale变成0,导致resize出错,解决访问是添加:scale = float(max_dim) / image_max 强制类型转型即可。

7. 参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容