背景
我面臨着需要盡可能準确識别手寫金額的挑戰。難點在于保持誤判率低于0.01%。由于資料集中樣本數量固定,是以資料增強是合乎邏輯的選擇。快速搜尋未發現針對光學字元識别(OCR)的現成方法。是以,我挽起袖子,親自建立了一個資料增強例程。它在訓練過程中被使用,并幫助我的模型實作了目标。繼續閱讀以了解詳情。
通過每次訓練圖像時引入小的變化,模型不太可能過拟合,更容易泛化。我将其與TROCR一起使用,但任何其他模型也應該受益。
測試設定
由于無法分享來自專有資料集的圖像,我原本想使用IAM手寫資料庫的樣本,但我未收到使用權限的回複。是以,我為示範建立了一些自己的示例。
我将使用OpenCV和albumentations庫進行三種類型的修改:形态學、噪聲和變換。
OpenCV是一個衆所周知的計算機視覺庫。Albumentations是一個相對較新的Python庫,用于進行簡單但功能強大的圖像增強。
還有一個不錯的示範網站,你可以在其中嘗試albumentations的功能。但由于無法使用自己的圖像進行測試,是以我建立了一個Jupyter筆記本,用于在本文中渲染所有增強的圖像。請随時在Colab中打開并進行實驗。
我們将首先展示單獨的修改,附有一些解釋,然後讨論将它們結合在一起的技術。我将假設所有圖像都是灰階的,并已經進行了對比度增強(例如,CLAHE)。
第一種增強技術:形态學修改
這些與結構的形狀有關。簡單來說:它們可用于使文本行看起來像用細或粗筆寫的。它們被稱為腐蝕和膨脹。不幸的是,這些目前還沒有包含在albumentations庫中,是以我必須借助opencv來實作。
為了産生像是使用寬線寬度筆的效果,我們可以膨脹原始圖像:
另一方面,腐蝕(順便說一下)模拟了使用細筆寫的效果:
在這裡要小心,最後一個參數——即疊代次數——不要設定得太高(這裡設定為3),否則手寫完全被去除。
cv2.dilate(img, kernel,iterations=random.randint(1, 3))
對于我們的資料集,我們隻能将其設定為1,是以這确實取決于你的資料。
第二種增強技術:引入噪聲
我們可以從圖像中删除黑色像素,也可以添加白色像素。有幾種方法可以實作這一點。我嘗試了許多方法,但這是我的簡短清單:
黑色下降顔色的RandomRain非常有害。即使對我來說,仍然很難閱讀文本。這就是為什麼我選擇将其發生的機會設定得很低的原因:
RandomShadow會用不同強度的線模糊文本:
PixelDropout輕松将随機像素變為黑色:
與黑色滴落相比,具有白色滴落顔色的RandomRain使文字解體,進而增強了訓練的難度。就像當拍攝了影印一份傳真的照片時看到的低品質一樣。可以将此變換發生的機率設定得更高。
在較小程度上,PixelDropout到白色也會産生相同的效果。但它更多地導緻圖像普遍褪色:
第三種增強技術:變換
ShiftScaleRotate:在這裡要小心參數。盡量避免一些文本被切斷并超出原始尺寸。同時進行縮放和旋轉。確定不要過度使用太大的參數。否則,第一個樣本的機會更大。你可以看到它實際上将文本移出圖像。通過選擇較大的邊界框,可以防止這種情況——有效地在文本周圍添加更多的空白。
模糊。老(但金貴)可靠的技術。将以不同的強度執行。
大結局:将它們全部組合在一起:
這就是力量所在。我們可以随機組合這些效果,建立每個訓練時期都包含的獨特圖像。需要謹慎考慮,不要做太多相同類型的方法。我們可以使用albumentation庫中的OneOf函數來實作這一點。OneOf包含一系列可能的變換,正如其名稱所暗示的那樣,将僅以機率P執行其中的一個。是以,将做更多或更少相似的變換分組是有意義的,以避免過度使用。以下是該函數:
import random
import cv2
import numpy as np
import albumentations as A
#gets PIL image and returns augmented PIL image
def augment_img(img):
#only augment 3/4th the images
if random.randint(1, 4) > 3:
return img
img = np.asarray(img) #convert to numpy for opencv
# morphological alterations
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
if random.randint(1, 5) == 1:
# dilation because the image is not inverted
img = cv2.erode(img, kernel, iterations=random.randint(1, 2))
if random.randint(1, 6) == 1:
# erosion because the image is not inverted
img = cv2.dilate(img, kernel,iterations=random.randint(1, 1))
transform = A.Compose([
A.OneOf([
#add black pixels noise
A.OneOf([
A.RandomRain(brightness_coefficient=1.0, drop_length=2, drop_width=2, drop_color = (0, 0, 0), blur_value=1, rain_type = 'drizzle', p=0.05),
A.RandomShadow(p=1),
A.PixelDropout(p=1),
], p=0.9),
#add white pixels noise
A.OneOf([
A.PixelDropout(dropout_prob=0.5,drop_value=255,p=1),
A.RandomRain(brightness_coefficient=1.0, drop_length=2, drop_width=2, drop_color = (255, 255, 255), blur_value=1, rain_type = None, p=1),
], p=0.9),
], p=1),
#transformations
A.OneOf([
A.ShiftScaleRotate(shift_limit=0, scale_limit=0.25, rotate_limit=2, border_mode=cv2.BORDER_CONSTANT, value=(255,255,255),p=1),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0, rotate_limit=8, border_mode=cv2.BORDER_CONSTANT, value=(255,255,255),p=1),
A.ShiftScaleRotate(shift_limit=0.02, scale_limit=0.15, rotate_limit=11, border_mode=cv2.BORDER_CONSTANT, value=(255,255,255),p=1),
A.Affine(shear=random.randint(-5, 5),mode=cv2.BORDER_CONSTANT, cval=(255,255,255), p=1)
], p=0.5),
A.Blur(blur_limit=5,p=0.25),
])
img = transform(image=img)['image']
image = Image.fromarray(img)
return image
P代表事件發生的機會。它是一個介于0和1之間的值,其中1表示它總是發生,0表示永遠不會發生。
是以,讓我們看看它在實際操作中的效果:
看起來相當不錯,對吧?
另一種方法:
在EASTER 2.0論文中,他們提出了TACo技術。它代表平鋪和破壞。(哈哈)它能夠實作這樣的效果:
我還沒有嘗試過這個方法,因為我的直覺告訴我原始圖像被破壞得太厲害。在我看來,如果我看不懂它,計算機也一樣。然而,當考慮到作為人類,如果看到'TA█O',你可能會猜到它是'TACO'。我們會看周圍的字母,而taco是一個常見的詞。但是一個有字典支援的計算機可能會将其解讀為'TAMO',這恰好是英語中表示“日本灰”的一個詞。
結論
我們讨論了許多圖像操作以及它們對OCR任務的好處。我希望這對你有所幫助,或者至少給了你一些嘗試的靈感。你可以将我的方法作為基準,但可能需要微調一些參數,使其完美适應你的資料集。請告訴我你的模型準确性提高了多少!
我在這個Jupyter筆記本中公開了這個技術。
https://github.com/Toon-nooT/notebooks/blob/main/OCR_data_augmentations.ipynb
參考引用
https://opencv.org/
https://albumentations.ai/
https://fki.tic.heia-fr.ch/databases/iam-handwriting-database
https://arxiv.org/abs/2205.14879
https://github.com/Toon-nooT/notebooks