1. 核心概念與目標
Mosaic 是一種在計算機視覺(尤其是目標檢測任務)中非常流行且強大的數據增強技術。它最早由 Ultralytics 的 Alexey Bochkovskiy 在?YOLOv4?中提出并推廣,后來被廣泛應用于 YOLOv5, YOLOv7, YOLOv8 等模型以及其他目標檢測框架中。
核心思想:?將?4 張不同的訓練圖像?按照隨機的位置、比例和排列方式(例如 2x2 網格)拼接組合成?1 張新的合成圖像。
主要目標:
豐富上下文信息:?模型在一張圖中同時看到來自 4 個不同場景的對象和背景,學習更魯棒的特征和上下文關系。
提升小目標檢測:?原始圖像中的小目標在被縮小后放入合成圖像中,可能變得極其微小且密集,迫使模型學習檢測更具挑戰性的小目標。
增加目標密度:?合成圖像通常包含比單張原始圖像更多的目標(最多 4 倍),讓模型在單次前向傳播中看到更多樣本,提高訓練效率。
模擬遮擋與裁剪:?拼接過程天然地裁剪了原始圖像的部分區域,模擬了目標被遮擋或只出現部分的情況。
減少對大批量 (Large Batch Size) 的依賴:?單張合成圖像包含的信息量相當于一個小批次 (mini-batch) 的數據,即使物理批大小較小,模型在每次迭代中也能處理更豐富的信息,降低了訓練對超大物理批大小的硬件要求。
增強模型魯棒性:?通過引入大幅度的幾何變換(縮放、平移、裁剪)和內容變化,提升模型對各種尺度、位置、背景變化的泛化能力。
2. 具體實現步驟
以下是 Mosaic 增強的典型實現流程:
(1)隨機選擇 4 張圖像:?從訓練數據集中隨機抽取 4 張原始圖像 (img1
,?img2
,?img3
,?img4
) 及其對應的標注(邊界框?bboxes
?+ 類別?labels
)。
(2)定義合成圖像尺寸:?確定最終合成圖像的目標尺寸 (通常是模型的輸入尺寸,如?640x640
)。
(3)確定拼接中心點 (隨機):
- 在目標尺寸的寬度 (
W
) 和高度 (H
) 范圍內,隨機生成一個中心點坐標?(xc, yc)
。這個點將作為 4 張圖像分割線的交點。 xc
?通常在?[0.25*W, 0.75*W]
?之間隨機,yc
?通常在?[0.25*H, 0.75*H]
?之間隨機。這確保了拼接點不會太靠近邊緣,從而讓每張原始圖像都有足夠部分被包含進來。
(4)劃分 4 個區域:?中心點?(xc, yc)
?將目標畫布劃分為 4 個矩形區域(左上、右上、左下、右下)。
(5)放置并縮放 4 張圖像:
- 左上區域:?放置?
img1
。根據左上區域的大小 (xc * yc
),對?img1
?進行縮放(可能放大或縮小),使其填充該區域。將縮放后的?img1
?粘貼到畫布的?(0, 0)
?到?(xc, yc)
?區域。 - 右上區域:?放置?
img2
。根據右上區域的大小 ((W - xc) * yc
),對?img2
?進行縮放,使其填充該區域。粘貼到?(xc, 0)
?到?(W, yc)
。 - 左下區域:?放置?
img3
。根據左下區域大小 (xc * (H - yc)
),縮放?img3
,填充該區域。粘貼到?(0, yc)
?到?(xc, H)
。 - 右下區域:?放置?
img4
。根據右下區域大小 ((W - xc) * (H - yc)
),縮放?img4
,填充該區域。粘貼到?(xc, yc)
?到?(W, H)
。
(6)處理邊界框標注:
對每張原始圖像,應用與其圖像相同的縮放比例和偏移量來變換其對應的邊界框坐標。
- 縮放:?根據圖像被縮放的倍數 (相對于原始尺寸到目標區域尺寸),等比例縮放邊界框的?
(x, y, w, h)
?坐標。 - 偏移:?根據該圖像在合成畫布上的起始位置?
(x_offset, y_offset)
,平移邊界框的?(x, y)
?坐標。 - 裁剪:?在縮放和偏移后,邊界框可能有一部分落在其所在區域之外(被相鄰圖像覆蓋)。需要裁剪掉落在區域外的部分邊界框,只保留完全位于其所在區域內部或邊界上的部分。如果一個邊界框被完全裁剪掉(即沒有任何部分留在其所屬區域內),則丟棄該標注。
(7)組合標注:?將處理(縮放、偏移、裁剪)后的 4 張圖像的邊界框和類別標注列表合并,作為這張合成圖像的標注。
(8)應用額外增強 (可選但常見):?在 Mosaic 合成之后,通常還會對這張合成圖像應用其他標準的數據增強,如:
- 色彩空間變換 (HSV 色調、飽和度、明度抖動)
- 隨機水平翻轉
- 隨機旋轉 (角度通常較小)
- 模糊、噪聲等。
3. 視覺示例與代碼實現
想象一個?640x640
?的畫布。隨機中心點?(xc=400, yc=300)
。
左上角 (
0:300, 0:400
) 區域:縮放并放置一張包含狗的圖像。右上角 (
0:300, 400:640
) 區域:縮放并放置一張包含汽車和樹的圖像。左下角 (
300:640, 0:400
) 區域:縮放并放置一張包含人和自行車的圖像。右下角 (
300:640, 400:640
) 區域:縮放并放置一張包含貓和沙發的圖像。
最終得到的合成圖像看起來像一張被分成 4 塊、內容各異的“馬賽克”拼圖,每塊區域內的目標邊界框都根據其位置進行了調整。
import os
import cv2
import numpy as np
import random
from xml.etree import ElementTree as ET
from typing import List, Tuple, Anydef augment_hsv(img: np.ndarray, hgain: float = 0.015, sgain: float = 0.7, vgain: float = 0.4) -> np.ndarray:"""HSV顏色空間增強Args:img: 輸入圖像 (H, W, C)hgain: 色調增益系數sgain: 飽和度增益系數vgain: 明度增益系數Returns:增強后的圖像"""if hgain or sgain or vgain:# 隨機增益系數r = np.random.uniform(-1, 1, 3) * [hgain, sgain, vgain] + 1# 轉換到HSV空間hue, sat, val = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV))dtype = img.dtype# 應用隨機增益x = np.arange(0, 256, dtype=np.int16)lut_hue = ((x * r[0]) % 180).astype(dtype)lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)lut_val = np.clip(x * r[2], 0, 255).astype(dtype)# 應用查找表img_hsv = cv2.merge((cv2.LUT(hue, lut_hue), (cv2.LUT(sat, lut_sat)), (cv2.LUT(val, lut_val)))img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)return imgdef merge_bboxes(bboxes: List[np.ndarray], cutx: int, cuty: int, min_area: int = 10) -> np.ndarray:"""合并并修正邊界框Args:bboxes: 四張圖像的邊界框列表,每張圖像的邊界框形狀為(N, 4)cutx: 橫向分割點cuty: 縱向分割點min_area: 最小有效區域閾值Returns:合并后的邊界框 (M, 4)"""merged_boxes = []for i, boxes in enumerate(bboxes):for box in boxes:x1, y1, x2, y2 = boxvalid = True# 左上區域if i == 0:# 完全在區域外if x1 > cutx or y1 > cuty:valid = False# 跨越下邊界if y2 > cuty and y1 < cuty:y2 = cuty# 跨越右邊界if x2 > cutx and x1 < cutx:x2 = cutx# 右上區域elif i == 1:if x2 < cutx or y1 > cuty:valid = Falseif y2 > cuty and y1 < cuty:y2 = cutyif x1 < cutx and x2 > cutx:x1 = cutx# 左下區域elif i == 2:if x1 > cutx or y2 < cuty:valid = Falseif y1 < cuty and y2 > cuty:y1 = cutyif x2 > cutx and x1 < cutx:x2 = cutx# 右下區域elif i == 3:if x2 < cutx or y2 < cuty:valid = Falseif y1 < cuty and y2 > cuty:y1 = cutyif x1 < cutx and x2 > cutx:x1 = cutx# 檢查框是否有效if valid:# 確保坐標有效x1, y1 = max(0, x1), max(0, y1)x2, y2 = max(0, x2), max(0, y2)# 檢查面積是否足夠area = (x2 - x1) * (y2 - y1)if area > min_area:merged_boxes.append([x1, y1, x2, y2])return np.array(merged_boxes) if merged_boxes else np.zeros((0, 4))def mosaic_augmentation(image_paths: List[str], annotation_paths: List[str], input_size: Tuple[int, int] = (416, 416),min_offset: float = 0.2,show_result: bool = False) -> Tuple[np.ndarray, np.ndarray]:"""Mosaic數據增強Args:image_paths: 圖像路徑列表annotation_paths: 標注路徑列表input_size: 輸出圖像尺寸 (h, w)min_offset: 最小偏移比例show_result: 是否顯示結果Returns:mosaic_image: 增強后的圖像 (H, W, C)mosaic_boxes: 邊界框數組 (M, 4)"""assert len(image_paths) == 4 and len(annotation_paths) == 4, "需要4張圖像和4個標注文件"h, w = input_sizemin_offset_x, min_offset_y = min_offset, min_offset# 讀取所有圖像和標注images = []all_boxes = []for img_path, ann_path in zip(image_paths, annotation_paths):# 讀取圖像img = cv2.imread(img_path)if img is None:raise FileNotFoundError(f"圖像未找到: {img_path}")# 解析XML標注boxes = []tree = ET.parse(ann_path)root = tree.getroot()for obj in root.findall('object'):bndbox = obj.find('bndbox')x1 = int(bndbox.find('xmin').text)y1 = int(bndbox.find('ymin').text)x2 = int(bndbox.find('xmax').text)y2 = int(bndbox.find('ymax').text)boxes.append([x1, y1, x2, y2])images.append(img)all_boxes.append(np.array(boxes))# 隨機選擇縮放比例scale = random.uniform(0.5, 1.5)# 隨機生成分割點cutx = random.randint(int(w * min_offset_x), int(w * (1 - min_offset_x)))cuty = random.randint(int(h * min_offset_y), int(h * (1 - min_offset_y)))# 創建空白畫布mosaic_img = np.zeros((h, w, 3), dtype=np.uint8)mosaic_boxes = []# 處理每張圖像for i, (img, boxes) in enumerate(zip(images, all_boxes)):ih, iw = img.shape[:2]# 隨機縮放圖像new_ar = w / hnw = int(scale * iw)nh = int(nw / new_ar)# 調整縮放比例防止過大if nw > w or nh > h:ratio = min(w / nw, h / nh)nw = int(nw * ratio)nh = int(nh * ratio)# 縮放圖像img = cv2.resize(img, (nw, nh))# 調整邊界框坐標scale_x = nw / iwscale_y = nh / ihif boxes.size > 0:boxes[:, [0, 2]] = boxes[:, [0, 2]] * scale_xboxes[:, [1, 3]] = boxes[:, [1, 3]] * scale_y# 確定圖像位置if i == 0: # 左上x1a, y1a, x2a, y2a = 0, 0, min(nw, cutx), min(nh, cuty)img = img[:y2a, :x2a]if boxes.size > 0:boxes[:, [0, 2]] = boxes[:, [0, 2]]boxes[:, [1, 3]] = boxes[:, [1, 3]]elif i == 1: # 右上x1a, y1a, x2a, y2a = cutx, 0, w, min(nh, cuty)img = img[:y2a, (x1a - (w - nw)):x2a - (w - nw)]if boxes.size > 0:boxes[:, [0, 2]] = boxes[:, [0, 2]] + w - nwboxes[:, [1, 3]] = boxes[:, [1, 3]]elif i == 2: # 左下x1a, y1a, x2a, y2a = 0, cuty, min(nw, cutx), himg = img[(y1a - (h - nh)):y2a - (h - nh), :x2a]if boxes.size > 0:boxes[:, [0, 2]] = boxes[:, [0, 2]]boxes[:, [1, 3]] = boxes[:, [1, 3]] + h - nhelse: # 右下x1a, y1a, x2a, y2a = cutx, cuty, w, himg = img[(y1a - (h - nh)):y2a - (h - nh), (x1a - (w - nw)):x2a - (w - nw)]if boxes.size > 0:boxes[:, [0, 2]] = boxes[:, [0, 2]] + w - nwboxes[:, [1, 3]] = boxes[:, [1, 3]] + h - nh# 將圖像放置到mosaic畫布上mosaic_img[y1a:y2a, x1a:x2a] = imgmosaic_boxes.append(boxes)# 合并并修正邊界框final_boxes = merge_bboxes(mosaic_boxes, cutx, cuty)# 應用HSV增強mosaic_img = augment_hsv(mosaic_img)# 可選:顯示結果if show_result:display_img = mosaic_img.copy()for box in final_boxes:x1, y1, x2, y2 = map(int, box)cv2.rectangle(display_img, (x1, y1), (x2, y2), (0, 255, 0), 2)# 繪制分割線cv2.line(display_img, (cutx, 0), (cutx, h), (255, 0, 0), 2)cv2.line(display_img, (0, cuty), (w, cuty), (255, 0, 0), 2)cv2.imshow('Mosaic Augmentation', display_img)cv2.waitKey(0)cv2.destroyAllWindows()return mosaic_img, final_boxesif __name__ == '__main__':# 數據集路徑base_dir = 'VOCdevkit/VOC2007'image_dir = os.path.join(base_dir, 'JPEGImages')annotation_dir = os.path.join(base_dir, 'Annotations')# 獲取所有圖像和標注文件all_images = [f for f in os.listdir(image_dir) if f.endswith('.jpg')]all_annotations = [f for f in os.listdir(annotation_dir) if f.endswith('.xml')]# 確保圖像和標注匹配image_stems = [os.path.splitext(f)[0] for f in all_images]annotation_stems = [os.path.splitext(f)[0] for f in all_annotations]valid_stems = set(image_stems) & set(annotation_stems)# 創建完整路徑image_paths = [os.path.join(image_dir, f"{stem}.jpg") for stem in valid_stems]annotation_paths = [os.path.join(annotation_dir, f"{stem}.xml") for stem in valid_stems]# 隨機選擇4張圖像進行Mosaic增強if len(image_paths) >= 4:indices = random.sample(range(len(image_paths)), 4)selected_images = [image_paths[i] for i in indices]selected_annotations = [annotation_paths[i] for i in indices]# 執行Mosaic增強mosaic_img, mosaic_boxes = mosaic_augmentation(selected_images, selected_annotations,input_size=(640, 640), # 更大的輸入尺寸min_offset=0.3, # 更大的最小偏移show_result=True # 顯示結果)# 保存結果cv2.imwrite('mosaic_result.jpg', mosaic_img)print(f"Mosaic增強完成! 檢測框數量: {len(mosaic_boxes)}")else:print("需要至少4張帶標注的圖像進行Mosaic增強")
4. 關鍵優勢
提升小目標性能:?如前所述,這是其最顯著的優勢之一。
數據利用效率高:?一張合成圖包含 4 張圖的標注信息,相當于增大了有效批大小。
學習復雜上下文:?模型被迫理解不同場景片段拼合在一起的上下文。
增強幾何魯棒性:?大幅度的縮放和裁剪模擬了現實世界目標的尺度變化和部分遮擋。
降低訓練成本:?可以在較小的物理批大小下達到接近使用更大批大小的效果(尤其在顯存受限時)。
5. 潛在缺點與注意事項
不自然的圖像:?合成的圖像在視覺上可能非常不自然(如天空和地板相接),雖然這有助于魯棒性,但過于離奇的組合可能引入噪聲。通常實踐中利大于弊。
標注噪聲:?邊界框裁剪可能導致部分目標信息丟失(如只保留半個目標),或者裁剪邊緣目標時可能引入輕微的定位噪聲。需要仔細實現裁剪邏輯。
訓練后期可能不穩定:?一些研究發現,在訓練后期繼續使用高概率的 Mosaic 可能導致優化困難或性能震蕩。常見的策略是隨著訓練進行線性衰減 Mosaic 的應用概率(例如,從第 N 個 Epoch 開始,每個 Epoch 將 Mosaic 的概率乘以一個衰減因子,最終降為 0)。
計算開銷:?合成圖像和變換標注需要額外的計算,但通常這個開銷被其帶來的訓練效率提升所抵消。
與其他增強的協調:?Mosaic 通常作為增強流水線的第一步,其后應用其他像素級或輕量幾何增強。過強的后續增強可能破壞 Mosaic 帶來的好處。
6. 應用場景
目標檢測 (Object Detection):?這是 Mosaic 最主要的應用領域,尤其適用于 YOLO 系列、SSD 等單階段檢測器。
實例分割 (Instance Segmentation):?理論上也可用,但需要額外處理掩碼 (mask) 的縮放、偏移和裁剪,實現更復雜。不如在目標檢測中普及。
其他密集預測任務 (如語義分割):?較少使用,因為拼接可能導致語義邊界嚴重不連續,合成圖像過于離奇。
7. 總結
Mosaic 數據增強是一種通過拼接 4 張隨機圖像及其標注來創建新訓練樣本的強大技術。它通過顯著增加圖像中目標的密度和多樣性、強制模型學習不同尺度和上下文、以及提高小目標檢測能力,在目標檢測任務(特別是基于 YOLO 的模型)中取得了巨大成功。盡管會生成視覺上不自然的圖像并帶來一些實現復雜性,但其在提升模型性能、特別是對小目標的魯棒性方面的優勢使其成為現代目標檢測訓練流程中一個不可或缺的組件。合理使用(如訓練后期衰減其概率)可以最大化其收益。