圖像語義分割:TensorFlow Deeplabv3+ 訓練自己數據集

本文主要介紹如何使用自己的數據集訓練DeepLabv3+分割算法,代碼使用的是官方源碼。

1、代碼簡介

當前使用TensorFlow版本的官方源碼,選擇它的原因是因爲代碼中的內容比較全面,除了代碼實現以外,還提供了許多文檔幫助理解與使用,同時還提供了模型轉換的代碼實現。

代碼地址:
【github】models/research/deeplab at master · tensorflow/models

接下來,先對這個代碼倉庫進行一下簡單的介紹,因爲自己在使用該代碼倉庫的時候只關心訓練代碼的實現,而忽略的其他的內容,走了不少彎路,到後面才發現我想要的內容,倉庫裏面早有(==)。

在當前的實現中,我們支持採用以下網絡主幹:

  • MobileNetv2MobileNetv3:一個爲移動設備設計的快速網絡結構
  • Xception:用於服務器端部署的強大網絡結構
  • ResNet-v1-{50, 101}:我們提供原始的ResNet-v1及其“ beta”變體,其中對“ stem”進行了修改以進行語義分割。
  • PNASNet: 一個通過神經體系結構搜索發現的強大網絡結構。
  • Auto-Deeplab(代碼中叫做HNASNet):通過神經體系結構搜索找到的特定於細分的網絡主幹。

該目錄包含TensorFlow 實現。我們提供的代碼使用戶可以訓練模型,根據mIOU(平均交叉點求和)評估結果以及可視化細分結果。我們以PASCAL VOC 2012Cityscapes語義分割基準爲例。

代碼中幾個重要文件:

  • datasets/:該文件夾下包含對於訓練數據集的處理代碼,主要針對 PASCAL VOC 2012Cityscapes數據集的處理。
  • g3doc/:該文件夾下包含多個Markdown文件,非常有用,如何安裝,常見問題FAQ等。
  • deeplab_demo.ipynb:該文件中給出瞭如果對一張圖像進行語義分割並顯示結果的Demo。
  • export_model.py:該文件提供了將訓練的checkpoint模型轉爲.pb文件的代碼實現。
  • train.py:訓練代碼文件,訓練時,需要指定提供的訓練參數。
  • eval.py:驗證代碼,輸出mIOU,用來評估模型的好壞。
  • vis.py:可視化代碼。

2、安裝

Deeplab依賴的庫有:

  • Numpy
  • Pillow 1.0
  • tf Slim (which is included in the “tensorflow/models/research/” checkout)
  • Jupyter notebook
  • Matplotlib
  • Tensorflow

2.1 添加庫到PYTHONPATH

本地運行的時候,tensorflow/models/research/目錄應該追加到PYTHONPATH中,如下:

# From tensorflow/models/research/
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim

# [Optional] for panoptic evaluation, you might need panopticapi:
# https://github.com/cocodataset/panopticapi
# Please clone it to a local directory ${PANOPTICAPI_DIR}
touch ${PANOPTICAPI_DIR}/panopticapi/__init__.py
export PYTHONPATH=$PYTHONPATH:${PANOPTICAPI_DIR}/panopticapi

注意:此命令需要在您啓動的每個新終端上運行。如果希望避免手動運行此命令,可以將它作爲新行添加到〜/ .bashrc文件的末尾。

2.2 測試是否安裝成功

通過運行model_test.py快速測試:

# From tensorflow/models/research/
python deeplab/model_test.py

PASCAL VOC 2012數據集上快速運行所有代碼:

# From tensorflow/models/research/deeplab
sh local_test.sh

3、數據集準備

最終目標: 生成TFRecord格式的數據

數據集目錄結構如下:

+dataset #數據集名稱
	+image
	+mask
	+index
		- train.txt
		- trainval.txt
		- val.txt
	+tfrecord
  • image: 原圖圖像,RGB彩色圖像
  • mask:像素值爲類別標籤的mask圖像,單通道,與原圖的名稱一致,後綴爲.jpg.png都可以,只要在代碼中讀取一致即可。VOC數據集默認原圖是.jpg,mask圖像爲.png
  • index:存放圖像文件名的txt文件(不加後綴)
  • tfrecord:存放轉爲tfrecord格式的圖像數據

數據集製作流程:

  1. 標註數據,製作符合要求的mask圖像
  2. 將數據集分割爲訓練集、驗證集和測試集
  3. 生成TFRecord格式的數據集

3.1 標註數據

訓練集數據包含兩部分,一是原圖,二是對應分類的標註值(本文中稱爲mask圖像)。

mask圖像的值是如何設置的?
根據圖像分割的分類個數來製作原圖對應的mask圖像。假如一共有N個類別(背景作爲一類),則mask圖像的值的範圍是[0~N)0值作爲背景值,其他分割類別的值依次設置爲1, 2, ..., N-1

注意:

  • ignore_label:從字面意思來講是忽略的標籤,即ignore_label是指沒有做標註的像素,即不需進行預測的像素值,因此,它不參與loss值的計算,在mask圖像中將其值記爲255
  • mask圖像是單通道的灰度圖像。
  • mask圖像的格式沒有限定,但所有的mask圖像採用同一種圖像格式,方便數據讀取。

小總結
mask圖像的值分爲三類:

  1. 背景:用0表示
  2. 分類類別:使用1, 2, ....., N-1表示
  3. ignore_label值:用255表示

如果分割的類別較少,則生成的mask圖像看上去是一片黑,因爲分類的值都較小,在0~255的範圍內不容易顯示出來。

3.2 分割數據集

這部分就是將準備的數據集進行分割,分爲訓練集、驗證集、測試集。
無需將具體的圖像文件分到三個文件夾中,只需要建立圖像的索引文件即可,通過添加相應的路徑+文件名即可獲取到具體的圖像。

假設原圖像和mask圖像的存放路徑如下:

  • 原圖:./dataset/images
  • mask圖像:./dataset/mask:此處存放的是2.1小節要求格式

原圖與mask圖像是一一對應的,包括圖像尺寸,圖像名(後綴可以不同)

索引文件存放路徑:./dataset/index,該路徑下生成:

  • train.txt
  • trainval.txt
  • val.txt

索引文件中,只需記錄文件名(不加後綴),這取決於代碼中數據集加載的方式。

目前爲止,數據集目錄結構如下:

#./dataset
+image
+mask
+index
	- train.txt
	- trainval.txt
	- val.txt

3.3 將數據打包爲TFRecord格式

TFRecord是谷歌推薦的一種二進制文件格式,理論上它可以保存任何格式的信息。
TFRecord內部使用了“Protocol Buffer”二進制數據編碼方案,它只佔用一個內存塊,只需要一次性加載一個二進制文件的方式即可,簡單,快速,尤其對大型訓練數據很友好。而且當我們的訓練數據量比較大的時候,可以將數據分成多個TFRecord文件,來提高處理效率。

那麼,如何將數據生成TFRecord格式呢?

在此,我們可以藉助 項目代碼中./datasets/build_voc2012_data.py文件來實現。給文件是VOC2012數據集處理的代碼,我們只需修改一下輸入參數即可。

參數:

  • image_folder:原圖文件夾名稱,./dataset/image
  • semantic_segmentation_folder:分割文件夾名稱, ./dataset/mask
  • list_folder:索引文件夾名稱,./dataset/index
  • output_dir:輸出路徑,即生成的tfrecord文件所在位置,./dataset/tfrecord

運行命令:

python ./datasets/build_voc2012_data.py --image_folder=./dataset/image
										--semantic_segmentation_folder=./dataset/mask
										--list_folder=./dataset/index
										--output_dir=./dataset/tfrecord

生成的文件如下:
在這裏插入圖片描述
注意: 可在代碼中調節參數_NUM_SHARDS (默認爲4),改變數據分塊的數目。(一些文件系統有最大單個文件大小的限制,如果數據集非常大,增加_NUM_SHARDS 可減小單個文件的大小)

該文件的核心代碼如下:

# dataset_split指的是train.txt, val.txt等
dataset = os.path.basename(dataset_split)[:-4]
filenames = [x.strip('\n') for x in open(dataset_split, 'r')] # 文件名列表

# 輸出tfrecord文件名
output_filename = os.path.join(
            FLAGS.output_dir,
            '%s-%05d-of-%05d.tfrecord' % (dataset, shard_id, _NUM_SHARDS))
with tf.python_io.TFRecordWriter(output_filename) as tfrecord_writer:
	for i in range(start_idx, end_idx): 
		image_filename = os.path.join(iamge_folder, filenames[i]+'.'+image_format)# 原圖路徑
		image_data = tf.gfile.GFile(image_filename, 'rb').read() #讀取原圖文件
	    height, width = image_reader.read_image_dims(image_data)
	    
	    seg_filename = os.path.join(semantic_segmentation_folder,
                    filenames[i] + '.' + label_format) # mask圖像路徑
	    seg_data = tf.gfile.GFile(seg_filename, 'rb').read() # 讀取分割圖像
	    seg_height, seg_width = label_reader.read_image_dims(seg_data)
	    
	    # 判斷原圖與mask圖像尺寸是否匹配
	    if height != seg_height or width != seg_width:
	        raise RuntimeError('Shape mismatched between image and label.')
	    # Convert to tf example.
	    example = build_data.image_seg_to_tfexample(
	        image_data, filenames[i], height, width, seg_data)
	    tfrecord_writer.write(example.SerializeToString())

至此,數據集的製作部分已經完成!!!

4、訓練

4.1 代碼修改

爲了訓練自己的數據集,需要修改以下幾處文件:

1 datasets/data_generator.py:增加數據集的註冊

該文件提供語義分割數據的包裝器

在該文件中,可以看到PASCAL_VOCCITYSCAPES以及ADE20K數據集的數據描述,如下:

_PASCAL_VOC_SEG_INFORMATION = DatasetDescriptor(
    splits_to_sizes={
        'train': 1464,
        'train_aug': 10582,
        'trainval': 2913,
        'val': 1449,
    },
    num_classes=21,
    ignore_label=255,
)

en,比着葫蘆畫瓢,增加我們自己數據集的描述信息,如下:

_PORTRAIT_INFORMATION = DatasetDescriptor(
    splits_to_sizes={
        'train': 17116,
        'trainval': 21395,
        'val': 4279,
    },
    num_classes=2,  # 類別數目,包括背景
    ignore_label=255,  # 忽略像素值
)

以人像分割任務爲例,只有兩類,即前景(人像)和背景(非人像)。

添加完描述信息後,需要將該數據集信息進行註冊,如下:

_DATASETS_INFORMATION = {
    'cityscapes': _CITYSCAPES_INFORMATION,
    'pascal_voc_seg': _PASCAL_VOC_SEG_INFORMATION,
    'ade20k': _ADE20K_INFORMATION,
    'portrait_seg': _PORTRAIT_INFORMATION, #增加此句
}

注意:此處的數據集名稱要與前面對應!

2 ./utils/train_utils.py修改

在函數get_model_init_fn中,修改爲如下代碼,增加logits層不加載預訓練模型權重:

	# Variables that will not be restored.
    exclude_list = ['global_step', 'logits']
    if not initialize_last_layer:
        exclude_list.extend(last_layers)

4.2 主要訓練參數

訓練文件train.pycommon.py文件中包含了訓練分割網絡所需要的所有參數。

  • model_variantDeeplab模型變量,可選值可見core/feature_extractor.py
    • 當使用mobilenet_v2時,設置變量strous_rates=decoder_output_stride=None
    • 當使用xception_65resnet_v1時,設置strous_rates=[6,12,18](output stride 16), decoder_output_stride=4
  • label_weights:此變量可以設置標籤的權重值,當數據集中出現類別不均衡時,可通過此變量來指定每個類別標籤的權重值,如label_weights=[0.1, 0.5]意味着標籤0的權重是0.1, 標籤1的權重是0.5。如果該值爲None,則所有的標籤具有相同的權重1.0
  • train_logdir:存放checkpointlogs的路徑。
  • log_steps:該值表示每隔多少步輸出日誌信息。
  • save_interval_secs:該值表示以秒爲單位,每隔多長時間保存一次模型文件到硬盤。
  • optimizer:優化器,可選值['momentum', 'adam']
  • learning_policy:學習率策略,可選值['poly', 'step']
  • base_learning_rate:基礎學習率,默認值0.0001
  • training_number_of_steps:模型訓練的迭代次數。
  • train_batch_size:模型訓練的批處理圖像數量。
  • train_crop_size:模型訓練時所使用的圖像尺寸,默認'513, 513'
  • tf_initial_checkpoint:預訓練模型。
  • initialize_last_layer:是否初始化最後一層。
  • last_layers_contain_logits_only:是否只考慮邏輯層作爲最後一層。
  • fine_tune_batch_norm:是否微調batch norm參數。
  • atrous_rates:默認值[6, 12, 18]
  • output_stride:默認值16,輸入和輸出空間分辨率的比值
    • 對於xception_65, 如果output_stride=8,則使用atrous_rates=[12, 24, 36]
    • 如果output_stride=16,則atrous_rates=[6, 12, 18]
    • 對於mobilenet_v2,使用None
    • 注意:在訓練和驗證階段可以使用不同的strous_ratesoutput_stride
  • dataset:所使用的分割數據集,此處與數據集註冊時的名稱一致。
  • train_split:使用哪個數據集來訓練,可選值即數據集註冊時的值,如train, trainval
  • dataset_dir:數據集存放的路徑。

針對訓練參數,下面幾點需要重點注意:

  1. 關於是否加載預訓練網絡的權重問題
    如果要在其他數據集上微調該網絡,需要關注以下幾個參數:

    • 使用預訓練網絡的權重,設置initialize_last_layer=True
    • 只使用網絡的backbone,設置initialize_last_layer=Falselast_layers_contain_logits_only=False
    • 使用所有的預訓練權重,除了logits,設置initialize_last_layer=Falselast_layers_contain_logits_only=True

    由於我的數據集分類與默認類別數不同,因此採取的參數值是:

    --initialize_last_layer=false
    --last_layers_contain_logits_only=true
    
  2. 如果資源有限,想要訓練自己數據集的幾條建議:

    • 設置output_stride=16或者甚至32(同時需要修改atrous_rates變量,例如,對於output_stride=32atrous_rates=[3, 6, 9]
    • 儘可能多的使用GPU,更改num_clone標誌,並將train_batch_size設置的儘可能大
    • 調整train_crop_size,可以將它設置的更小一些,例如513x513(甚至321x321),這樣就可以使用更大的batch_size
    • 使用較小的網絡主幹,如mobilenet_v2
  3. 關於是否微調batch_norm
    當訓練使用的批處理大小train_batch_size大於12(最好大於16)時,設置fine_tune_batch_norm=True。否則,設置fine_tune_batch_norm=False

4.3 預訓練模型

模型鏈接具體可見:models/model_zoo.md at master · tensorflow/models

提供了在幾個數據集上的預訓練模型,包括(1) PASCAL VOC 2012, (2) Cityscapes, (3) ADE20K

未解壓的目下包括:

  • 一個frozen inference graphforzen_inference_graph.pb)。默認情況下,所有凍結推理圖的輸出步長爲8,單個eval scale爲1.0,沒有左右翻轉,除非另外指定。基於MobileNet-v2的模型不包括解碼器模塊。
  • 一個checkpoint(model.ckpt.data-00000-of-00001, model.ckpt.index)

在這裏插入圖片描述

還提供了在ImageNet預訓練的checkpoints

未解壓文件包括:
一個model checkpoint (model.ckpt.data-00000-of-00001, model.ckpt.index)
ImageNet上預訓練模型

根據自己的情況進行下載

4.4 訓練模型

python train.py \
    --logtostderr \
    --training_number_of_steps=20000 \
    --train_split="train" \
    --model_variant="xception_65" \
    --train_crop_size="513,513" \
    --atrous_rates=6 \
    --atrous_rates=12 \
    --atrous_rates=18 \
    --output_stride=16 \
    --decoder_output_stride=4 \
    --train_batch_size=2 \
    --save_interval_secs=240 \
    --optimizer="momentum" \
    --leraning_policy="poly" \
    --fine_tune_batch_norm=false \
    --initialize_last_layer=false \
    --last_layers_contain_logits_only=true \
    --dataset="portrait_seg" \
    --tf_initial_checkpoint="./checkpoint/deeplabv3_pascal_trainval/model.ckpt" \
    --train_logdir="./train_logs" \
    --dataset_dir="./dataset/tfrecord"

4.5 驗證模型

驗證代碼: ./eval.py

# From tensorflow/models/research/
python deeplab/eval.py \
    --logtostderr \
    --eval_split="val" \
    --model_variant="xception_65" \
    --atrous_rates=6 \
    --atrous_rates=12 \
    --atrous_rates=18 \
    --output_stride=16 \
    --decoder_output_stride=4 \
    --eval_crop_size="513,513" \
    --dataset="portrait_seg" \ # 數據集名稱
    --checkpoint_dir=${PATH_TO_CHECKPOINT} \ # 預訓練模型
    --eval_logdir=${PATH_TO_EVAL_DIR} \ 
    --dataset_dir="./dataset/tfrecord" # 數據集路徑

得到的結果如下:
在這裏插入圖片描述

4.6 訓練過程可視化

可以使用Tensorboard檢查培訓和評估工作的進展。如果使用推薦的目錄結構,Tensorboard可以使用以下命令運行:

tensorboard --logdir=${PATH_TO_LOG_DIRECTORY}
# 文中log地址
tensorboard --logdir="./train_logs"

5、推理

5.1 模型導出

在訓練過程中,會保存模型文件到硬盤,如下:

其形式是TensorFlowcheckpoint格式,代碼中提供了一個腳本(export_model.py)可以將checkpoint轉換爲.pb格式。

export_model.py主要參數:

  • checkpoint_path:訓練保存的檢查點文件
  • export_path:模型導出路徑
  • num_classes:分類類別
  • crop_size:圖像尺寸,[513, 513]
  • atrous_rates12, 24, 36
  • output_stride8

生成的.pb文件如下:
在這裏插入圖片描述

5.2 單張圖像上推理

class DeepLabModel(object):
    """class to load deeplab model and run inference"""

    INPUT_TENSOR_NAME = 'ImageTensor:0'
    OUTPUT_TENSOR_NAME='SemanticPredictions:0'
    INPUT_SIZE = 513
    FROZEN_GRAPH_NAME= 'frozen_inference_graph'

    def __init__(self, pretrained_weights):
        """Creates and loads pretrained deeplab model."""
        self.graph = tf.Graph()
        graph_def = None
        # Extract frozen graph from tar archive
        if pretrained_weights.endswith('.tar.gz'):
            tar_file = tarfile.open(pretrained_weights)
            for tar_info in tar_file.getmembers():
                if self.FROZEN_GRAPH_NAME in os.path.basename(tar_info.name):
                    file_handle = tar_file.extractfile(tar_info)
                    graph_def = tf.GraphDef.FromString(file_handle.read())
                    break
            tar_file.close()
        else:
            with open(pretrained_weights, 'rb') as fd:
                graph_def = tf.GraphDef.FromString(fd.read())

        if graph_def is None:
            raise RuntimeError('Cannot find inference graph in tar archive.')

        with self.graph.as_default():
            tf.import_graph_def(graph_def, name='')

        gpu_options = tf.GPUOptions(allow_growth=True)
        config = tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False)
        self.sess = tf.Session(graph=self.graph, config=config)

    def run(self, image):
        """Runs inference on a single image.
        Args:
            image: A PIL.Image object, raw input image.
        Returns:
            resized_image:RGB image resized from original input image.
            seg_map:Segmentation map of 'resized_iamge'.
        """
        width, height = image.size
        resize_ratio = 1.0 * self.INPUT_SIZE/max(width, height)
        target_size = (int(resize_ratio*width), int(resize_ratio * height))
        resized_image = image.convert('RGB').resize(target_size, Image.ANTIALIAS)
        batch_seg_map = self.sess.run(
            self.OUTPUT_TENSOR_NAME,
            feed_dict={self.INPUT_TENSOR_NAME:[np.asarray(resized_image)]}
        )
        seg_map = batch_seg_map[0]

        return resized_image, seg_map
if __name__ == '__main__':
	pretrained_weights = './train_logs/frozen_inference_graph_20000.pb'
	MODEL = DeepLabModel(pretrained_weights) # 加載模型
	
	img_name = 'test.jpg'
	img = Image.open(img_name)
	resized_im, seg_map = MODEL.run(original_im) #獲取結果
	seg_map[seg_map==1]=255 #將人像的像素值置爲255
	seg_map.save('output.jpg') # 保存mask結果圖像

至此,整個訓練過程就結束了!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章