تقسیم بندی تصویر

مشاهده در TensorFlow.org در Google Colab اجرا شود مشاهده منبع در GitHub دانلود دفترچه یادداشت

این آموزش بر روی وظیفه تقسیم بندی تصویر با استفاده از U-Net اصلاح شده تمرکز دارد.

تقسیم بندی تصویر چیست؟

در یک کار طبقه بندی تصویر، شبکه یک برچسب (یا کلاس) به هر تصویر ورودی اختصاص می دهد. با این حال، فرض کنید می خواهید شکل آن شی را بدانید، کدام پیکسل متعلق به کدام شی است، و غیره. در این صورت می خواهید به هر پیکسل تصویر یک کلاس اختصاص دهید. این وظیفه به تقسیم بندی معروف است. یک مدل تقسیم بندی اطلاعات بسیار دقیق تری را در مورد تصویر برمی گرداند. بخش‌بندی تصویر کاربردهای زیادی در تصویربرداری پزشکی، خودروهای خودران و تصویربرداری ماهواره‌ای دارد.

این آموزش از مجموعه داده های حیوانات خانگی Oxford-IIIT استفاده می کند ( Parkhi et al, 2012 ). مجموعه داده شامل تصاویری از 37 نژاد حیوان خانگی، با 200 تصویر در هر نژاد است (هر کدام 100 تصویر در بخش های آموزشی و آزمایشی). هر تصویر شامل برچسب‌های مربوطه و ماسک‌های پیکسلی است. ماسک ها برچسب کلاس برای هر پیکسل هستند. به هر پیکسل یکی از سه دسته داده می شود:

  • کلاس 1: پیکسل متعلق به حیوان خانگی.
  • کلاس 2: پیکسل در حاشیه حیوان خانگی.
  • کلاس 3: هیچ یک از موارد فوق/یک پیکسل اطراف.
pip install git+https://github.com/tensorflow/examples.git
import tensorflow as tf

import tensorflow_datasets as tfds
from tensorflow_examples.models.pix2pix import pix2pix

from IPython.display import clear_output
import matplotlib.pyplot as plt

مجموعه داده Oxford-IIIT Pets را دانلود کنید

مجموعه داده از TensorFlow Datasets در دسترس است. ماسک های تقسیم بندی در نسخه 3+ گنجانده شده است.

dataset, info = tfds.load('oxford_iiit_pet:3.*.*', with_info=True)

علاوه بر این، مقادیر رنگ تصویر در محدوده [0,1] نرمال می شوند. در نهایت، همانطور که در بالا ذکر شد، پیکسل‌ها در ماسک تقسیم‌بندی با یا {1، 2، 3} برچسب‌گذاری می‌شوند. برای راحتی کار، 1 را از ماسک تقسیم بندی کم کنید، در نتیجه برچسب هایی به دست می آیند: {0، 1، 2}.

def normalize(input_image, input_mask):
  input_image = tf.cast(input_image, tf.float32) / 255.0
  input_mask -= 1
  return input_image, input_mask
def load_image(datapoint):
  input_image = tf.image.resize(datapoint['image'], (128, 128))
  input_mask = tf.image.resize(datapoint['segmentation_mask'], (128, 128))

  input_image, input_mask = normalize(input_image, input_mask)

  return input_image, input_mask

مجموعه داده از قبل شامل تقسیم‌های آموزشی و آزمایشی مورد نیاز است، بنابراین به استفاده از تقسیم‌های مشابه ادامه دهید.

TRAIN_LENGTH = info.splits['train'].num_examples
BATCH_SIZE = 64
BUFFER_SIZE = 1000
STEPS_PER_EPOCH = TRAIN_LENGTH // BATCH_SIZE
train_images = dataset['train'].map(load_image, num_parallel_calls=tf.data.AUTOTUNE)
test_images = dataset['test'].map(load_image, num_parallel_calls=tf.data.AUTOTUNE)

کلاس زیر با چرخاندن تصادفی یک تصویر، یک تقویت ساده را انجام می دهد. برای کسب اطلاعات بیشتر به آموزش تقویت تصویر بروید.

class Augment(tf.keras.layers.Layer):
  def __init__(self, seed=42):
    super().__init__()
    # both use the same seed, so they'll make the same random changes.
    self.augment_inputs = tf.keras.layers.RandomFlip(mode="horizontal", seed=seed)
    self.augment_labels = tf.keras.layers.RandomFlip(mode="horizontal", seed=seed)

  def call(self, inputs, labels):
    inputs = self.augment_inputs(inputs)
    labels = self.augment_labels(labels)
    return inputs, labels

خط لوله ورودی را بسازید، پس از دسته بندی ورودی ها، Augmentation را اعمال کنید.

train_batches = (
    train_images
    .cache()
    .shuffle(BUFFER_SIZE)
    .batch(BATCH_SIZE)
    .repeat()
    .map(Augment())
    .prefetch(buffer_size=tf.data.AUTOTUNE))

test_batches = test_images.batch(BATCH_SIZE)

یک نمونه تصویر و ماسک مربوط به آن را از مجموعه داده تجسم کنید.

def display(display_list):
  plt.figure(figsize=(15, 15))

  title = ['Input Image', 'True Mask', 'Predicted Mask']

  for i in range(len(display_list)):
    plt.subplot(1, len(display_list), i+1)
    plt.title(title[i])
    plt.imshow(tf.keras.utils.array_to_img(display_list[i]))
    plt.axis('off')
  plt.show()
for images, masks in train_batches.take(2):
  sample_image, sample_mask = images[0], masks[0]
  display([sample_image, sample_mask])
Corrupt JPEG data: 240 extraneous bytes before marker 0xd9
Corrupt JPEG data: premature end of data segment

png

png

2022-01-26 05:14:45.972101: W tensorflow/core/kernels/data/cache_dataset_ops.cc:768] The calling iterator did not fully read the dataset being cached. In order to avoid unexpected truncation of the dataset, the partially cached contents of the dataset  will be discarded. This can happen if you have an input pipeline similar to `dataset.cache().take(k).repeat()`. You should use `dataset.take(k).cache().repeat()` instead.

مدل را تعریف کنید

مدل مورد استفاده در اینجا یک U-Net اصلاح شده است. U-Net از یک رمزگذار (downsampler) و رمزگشا (upsampler) تشکیل شده است. به منظور یادگیری ویژگی های قوی و کاهش تعداد پارامترهای قابل آموزش، از یک مدل از پیش آموزش دیده - MobileNetV2 - به عنوان رمزگذار استفاده خواهید کرد. برای رمزگشا، از بلوک upsample استفاده خواهید کرد، که قبلاً در مثال pix2pix در مخزن TensorFlow Examples پیاده سازی شده است. ( pix2pix: ترجمه تصویر به تصویر را با آموزش GAN شرطی در یک نوت بوک بررسی کنید.)

همانطور که گفته شد، رمزگذار یک مدل از پیش آموزش دیده MobileNetV2 خواهد بود که آماده و آماده استفاده در tf.keras.applications است. رمزگذار از خروجی های خاصی از لایه های میانی در مدل تشکیل شده است. توجه داشته باشید که رمزگذار در طول فرآیند آموزش آموزش نمی بیند.

base_model = tf.keras.applications.MobileNetV2(input_shape=[128, 128, 3], include_top=False)

# Use the activations of these layers
layer_names = [
    'block_1_expand_relu',   # 64x64
    'block_3_expand_relu',   # 32x32
    'block_6_expand_relu',   # 16x16
    'block_13_expand_relu',  # 8x8
    'block_16_project',      # 4x4
]
base_model_outputs = [base_model.get_layer(name).output for name in layer_names]

# Create the feature extraction model
down_stack = tf.keras.Model(inputs=base_model.input, outputs=base_model_outputs)

down_stack.trainable = False
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/mobilenet_v2/mobilenet_v2_weights_tf_dim_ordering_tf_kernels_1.0_128_no_top.h5
9412608/9406464 [==============================] - 0s 0us/step
9420800/9406464 [==============================] - 0s 0us/step

رمزگشا/نمونه‌برگر به سادگی مجموعه‌ای از بلوک‌های نمونه بالاست که در نمونه‌های TensorFlow پیاده‌سازی شده‌اند.

up_stack = [
    pix2pix.upsample(512, 3),  # 4x4 -> 8x8
    pix2pix.upsample(256, 3),  # 8x8 -> 16x16
    pix2pix.upsample(128, 3),  # 16x16 -> 32x32
    pix2pix.upsample(64, 3),   # 32x32 -> 64x64
]
def unet_model(output_channels:int):
  inputs = tf.keras.layers.Input(shape=[128, 128, 3])

  # Downsampling through the model
  skips = down_stack(inputs)
  x = skips[-1]
  skips = reversed(skips[:-1])

  # Upsampling and establishing the skip connections
  for up, skip in zip(up_stack, skips):
    x = up(x)
    concat = tf.keras.layers.Concatenate()
    x = concat([x, skip])

  # This is the last layer of the model
  last = tf.keras.layers.Conv2DTranspose(
      filters=output_channels, kernel_size=3, strides=2,
      padding='same')  #64x64 -> 128x128

  x = last(x)

  return tf.keras.Model(inputs=inputs, outputs=x)

توجه داشته باشید که تعداد فیلترهای لایه آخر به تعداد output_channels شده است. این یک کانال خروجی در هر کلاس خواهد بود.

مدل را آموزش دهید

اکنون تنها کاری که باید انجام دهید این است که مدل را کامپایل و آموزش دهید.

از آنجایی که این یک مشکل طبقه بندی چند کلاسه است، از تابع ضرر tf.keras.losses.CategoricalCrossentropy با آرگومان from_logits روی True استفاده کنید، زیرا برچسب ها به جای بردارهای امتیاز برای هر پیکسل از هر کلاس، اعداد صحیح اسکالر هستند.

هنگام اجرای استنتاج، برچسب اختصاص داده شده به پیکسل کانالی است که بالاترین مقدار را دارد. این کاری است که تابع create_mask انجام می دهد.

OUTPUT_CLASSES = 3

model = unet_model(output_channels=OUTPUT_CLASSES)
model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

نگاهی گذرا به معماری مدل حاصل کنید:

tf.keras.utils.plot_model(model, show_shapes=True)

png

مدل را امتحان کنید تا آنچه را که قبل از آموزش پیش بینی می کند بررسی کنید.

def create_mask(pred_mask):
  pred_mask = tf.argmax(pred_mask, axis=-1)
  pred_mask = pred_mask[..., tf.newaxis]
  return pred_mask[0]
def show_predictions(dataset=None, num=1):
  if dataset:
    for image, mask in dataset.take(num):
      pred_mask = model.predict(image)
      display([image[0], mask[0], create_mask(pred_mask)])
  else:
    display([sample_image, sample_mask,
             create_mask(model.predict(sample_image[tf.newaxis, ...]))])
show_predictions()

png

تماس برگشتی تعریف شده در زیر برای مشاهده چگونگی بهبود مدل در حین آموزش استفاده می شود.

class DisplayCallback(tf.keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs=None):
    clear_output(wait=True)
    show_predictions()
    print ('\nSample Prediction after epoch {}\n'.format(epoch+1))
EPOCHS = 20
VAL_SUBSPLITS = 5
VALIDATION_STEPS = info.splits['test'].num_examples//BATCH_SIZE//VAL_SUBSPLITS

model_history = model.fit(train_batches, epochs=EPOCHS,
                          steps_per_epoch=STEPS_PER_EPOCH,
                          validation_steps=VALIDATION_STEPS,
                          validation_data=test_batches,
                          callbacks=[DisplayCallback()])

png

Sample Prediction after epoch 20

57/57 [==============================] - 4s 62ms/step - loss: 0.1838 - accuracy: 0.9187 - val_loss: 0.2797 - val_accuracy: 0.8955
loss = model_history.history['loss']
val_loss = model_history.history['val_loss']

plt.figure()
plt.plot(model_history.epoch, loss, 'r', label='Training loss')
plt.plot(model_history.epoch, val_loss, 'bo', label='Validation loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss Value')
plt.ylim([0, 1])
plt.legend()
plt.show()

png

پیش بینی کنید

حالا چند پیش‌بینی کنید. به منظور صرفه جویی در زمان، تعداد دوره ها کم نگه داشته شد، اما برای دستیابی به نتایج دقیق تر، ممکن است این مقدار را بیشتر تنظیم کنید.

show_predictions(test_batches, 3)

png

png

png

اختیاری: کلاس های نامتعادل و وزن کلاس

مجموعه داده‌های تقسیم‌بندی معنایی می‌توانند بسیار نامتعادل باشند، به این معنی که پیکسل‌های کلاس خاص می‌توانند بیشتر در داخل تصاویر نسبت به کلاس‌های دیگر وجود داشته باشند. از آنجایی که مشکلات بخش‌بندی را می‌توان به‌عنوان مشکلات طبقه‌بندی در هر پیکسل در نظر گرفت، می‌توانید با مشکل عدم تعادل با وزن کردن تابع تلفات برای محاسبه آن مقابله کنید. این یک راه ساده و زیبا برای مقابله با این مشکل است. برای کسب اطلاعات بیشتر به آموزش طبقه بندی در مورد داده های نامتعادل مراجعه کنید.

برای جلوگیری از ابهام ، Model.fit از آرگومان class_weight برای ورودی‌های با ابعاد 3+ پشتیبانی نمی‌کند.

try:
  model_history = model.fit(train_batches, epochs=EPOCHS,
                            steps_per_epoch=STEPS_PER_EPOCH,
                            class_weight = {0:2.0, 1:2.0, 2:1.0})
  assert False
except Exception as e:
  print(f"Expected {type(e).__name__}: {e}")
Expected ValueError: `class_weight` not supported for 3+ dimensional targets.

بنابراین، در این مورد شما باید وزنه را خودتان اجرا کنید. این کار را با استفاده از وزن‌های نمونه انجام می‌دهید: علاوه بر جفت‌های (data, label) ، Model.fit (data, label, sample_weight) را نیز می‌پذیرد.

Model.fit sample_weight را به ضررها و معیارها منتشر می کند، که آرگومان sample_weight را نیز می پذیرند. وزن نمونه قبل از مرحله کاهش در مقدار نمونه ضرب می شود. مثلا:

label = [0,0]
prediction = [[-3., 0], [-3, 0]] 
sample_weight = [1, 10] 

loss = tf.losses.SparseCategoricalCrossentropy(from_logits=True,
                                               reduction=tf.losses.Reduction.NONE)
loss(label, prediction, sample_weight).numpy()
array([ 3.0485873, 30.485874 ], dtype=float32)

بنابراین برای ساختن وزن‌های نمونه برای این آموزش به تابعی نیاز دارید که یک جفت (data, label) را بگیرد و یک سه‌گانه (data, label, sample_weight) . جایی که sample_weight یک تصویر 1 کانالی است که وزن کلاس برای هر پیکسل را در بر می گیرد.

ساده ترین پیاده سازی ممکن استفاده از برچسب به عنوان یک فهرست در لیست class_weight است:

def add_sample_weights(image, label):
  # The weights for each class, with the constraint that:
  #     sum(class_weights) == 1.0
  class_weights = tf.constant([2.0, 2.0, 1.0])
  class_weights = class_weights/tf.reduce_sum(class_weights)

  # Create an image of `sample_weights` by using the label at each pixel as an 
  # index into the `class weights` .
  sample_weights = tf.gather(class_weights, indices=tf.cast(label, tf.int32))

  return image, label, sample_weights

عناصر داده حاصل هر کدام شامل 3 تصویر است:

train_batches.map(add_sample_weights).element_spec
(TensorSpec(shape=(None, 128, 128, 3), dtype=tf.float32, name=None),
 TensorSpec(shape=(None, 128, 128, 1), dtype=tf.float32, name=None),
 TensorSpec(shape=(None, 128, 128, 1), dtype=tf.float32, name=None))

اکنون می توانید یک مدل را بر روی این مجموعه داده وزنی آموزش دهید:

weighted_model = unet_model(OUTPUT_CLASSES)
weighted_model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'])
weighted_model.fit(
    train_batches.map(add_sample_weights),
    epochs=1,
    steps_per_epoch=10)
10/10 [==============================] - 3s 44ms/step - loss: 0.3099 - accuracy: 0.6063
<keras.callbacks.History at 0x7fa75d0f3e50>

مراحل بعدی

اکنون که متوجه شدید تقسیم‌بندی تصویر چیست و چگونه کار می‌کند، می‌توانید این آموزش را با خروجی‌های لایه میانی مختلف یا حتی مدل‌های مختلف از پیش آموزش دیده امتحان کنید. همچنین می‌توانید با آزمایش چالش پوشاندن تصویر Carvana که در Kaggle میزبانی شده است، خود را به چالش بکشید.

همچنین ممکن است بخواهید Tensorflow Object Detection API را برای مدل دیگری که می‌توانید در داده‌های خود دوباره آموزش دهید، ببینید. مدل های از پیش آموزش دیده در تنسورفلو هاب موجود هستند