tinydoc-5m / tinydoc /synth /augment.py
harryfrz's picture
tinydoc-5m weights (5,035,273 params)
4dce182 verified
Raw History Blame Contribute Delete
6.14 kB
"""Image degradations that mimic scans, phone photos, faxes and faded thermal receipts.
Geometric transforms return the homography so boxes can be moved with the pixels.
"""
import cv2
import numpy as np
from ..geometry import transform_quads
def _smooth_field(rng, h, w, cells=4):
f = rng.random((cells, cells)).astype(np.float32)
return cv2.resize(f, (w, h), interpolation=cv2.INTER_CUBIC)
def photometric(img, rng, strength=1.0, thermal=False):
"""img: uint8 HxW or HxWx3. Returns a degraded copy of the same shape."""
out = img.astype(np.float32)
h, w = out.shape[:2]
if thermal or rng.random() < 0.15 * strength: # faded print: lift ink toward paper, unevenly
fade = 0.25 + 0.6 * _smooth_field(rng, h, w, int(rng.integers(2, 6)))
if out.ndim == 3:
fade = fade[..., None]
paper = np.percentile(out, 90)
out = paper - (paper - out) * (1 - fade * rng.uniform(0.3, 0.9))
if rng.random() < 0.5 * strength: # contrast / brightness
out = (out - 128) * rng.uniform(0.6, 1.3) + 128 + rng.uniform(-40, 30)
if rng.random() < 0.3 * strength: # lighting gradient / shadow
g = 0.55 + 0.45 * _smooth_field(rng, h, w, int(rng.integers(2, 4)))
out *= g[..., None] if out.ndim == 3 else g
if out.ndim == 3 and rng.random() < 0.3 * strength: # colour cast
out *= rng.uniform(0.85, 1.1, 3)
out = np.clip(out, 0, 255).astype(np.uint8)
if rng.random() < 0.3 * strength: # resolution loss
f = rng.uniform(0.35, 0.8)
small = cv2.resize(out, (max(1, int(w * f)), max(1, int(h * f))), interpolation=cv2.INTER_AREA)
out = cv2.resize(small, (w, h), interpolation=rng.choice([cv2.INTER_LINEAR, cv2.INTER_NEAREST, cv2.INTER_CUBIC]))
if rng.random() < 0.4 * strength: # blur
if rng.random() < 0.8:
out = cv2.GaussianBlur(out, (0, 0), rng.uniform(0.3, 1.3))
else:
k = int(rng.integers(3, 7))
kern = np.zeros((k, k), np.float32)
kern[k // 2, :] = 1.0 / k
m = cv2.getRotationMatrix2D((k / 2 - 0.5, k / 2 - 0.5), rng.uniform(0, 180), 1)
out = cv2.filter2D(out, -1, cv2.warpAffine(kern, m, (k, k)))
if rng.random() < 0.5 * strength: # sensor / scan noise
noise = rng.normal(0, rng.uniform(2, 14), out.shape)
out = np.clip(out.astype(np.float32) + noise, 0, 255).astype(np.uint8)
if rng.random() < 0.08 * strength: # salt & pepper speckle
mask = rng.random(out.shape[:2])
out[mask < 0.002] = 0
out[mask > 0.998] = 255
if rng.random() < 0.05 * strength: # fax-like binarisation
g = out if out.ndim == 2 else cv2.cvtColor(out, cv2.COLOR_RGB2GRAY)
_, b = cv2.threshold(g, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
out = b if out.ndim == 2 else cv2.cvtColor(b, cv2.COLOR_GRAY2RGB)
if rng.random() < 0.6 * strength: # JPEG
ok, enc = cv2.imencode(".jpg", out, [cv2.IMWRITE_JPEG_QUALITY, int(rng.integers(25, 95))])
out = cv2.imdecode(enc, cv2.IMREAD_UNCHANGED)
return out
def _background(rng, h, w):
r = rng.random()
if r < 0.4:
base = rng.integers(20, 200, 3)
bg = np.ones((h, w, 3), np.float32) * base
bg += (rng.random((h // 8 + 1, w // 8 + 1, 3)) * 40 - 20).repeat(8, 0).repeat(8, 1)[:h, :w]
elif r < 0.7: # wood-ish stripes
base = rng.integers(60, 180, 3).astype(np.float32)
stripes = np.sin(np.linspace(0, rng.uniform(20, 80), w))[None, :, None] * rng.uniform(5, 25)
bg = np.ones((h, w, 3), np.float32) * base + stripes
else:
bg = np.ones((h, w, 3), np.float32) * rng.integers(0, 255, 3)
bg *= (0.6 + 0.4 * _smooth_field(rng, h, w, 3))[..., None]
return np.clip(bg, 0, 255).astype(np.uint8)
def geometric(img, rng, mode=None):
"""Returns (image, 3x3 homography from input to output coordinates).
modes: none | rotate (scanner skew) | photo (page on a background with perspective)
"""
h, w = img.shape[:2]
mode = mode or rng.choice(["none", "rotate", "photo"], p=[0.35, 0.4, 0.25])
if mode == "none":
return img, np.eye(3)
if mode == "rotate":
ang = rng.normal(0, 2.0) if rng.random() < 0.8 else rng.uniform(-8, 8)
m = cv2.getRotationMatrix2D((w / 2, h / 2), ang, 1.0)
cos, sin = abs(m[0, 0]), abs(m[0, 1])
nw, nh = int(h * sin + w * cos), int(h * cos + w * sin)
m[0, 2] += nw / 2 - w / 2
m[1, 2] += nh / 2 - h / 2
border = tuple(int(v) for v in np.median(img.reshape(-1, img.shape[-1]) if img.ndim == 3 else img.reshape(-1, 1), 0))
out = cv2.warpAffine(img, m, (nw, nh), flags=cv2.INTER_LINEAR, borderValue=border)
return out, np.vstack([m, [0, 0, 1]])
# photo: page occupies 55-95% of a larger frame, corners jittered
s = rng.uniform(1.05, 1.6)
W, H = int(w * s), int(h * s)
ox, oy = rng.uniform(0, W - w), rng.uniform(0, H - h)
j = 0.04 * min(w, h)
src = np.float32([[0, 0], [w, 0], [w, h], [0, h]])
dst = np.float32([[ox, oy], [ox + w, oy], [ox + w, oy + h], [ox, oy + h]]) + rng.uniform(-j, j, (4, 2)).astype(np.float32)
m = cv2.getPerspectiveTransform(src, dst)
bg = _background(rng, H, W) if img.ndim == 3 else cv2.cvtColor(_background(rng, H, W), cv2.COLOR_RGB2GRAY)
warped = cv2.warpPerspective(img, m, (W, H), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
mask = cv2.warpPerspective(np.full((h, w), 255, np.uint8), m, (W, H), flags=cv2.INTER_NEAREST)
out = np.where((mask > 0)[..., None] if img.ndim == 3 else mask > 0, warped, bg)
return out, m
def augment_page(img, quads, ignore_quads, rng, strength=1.0, thermal=False, geo_mode=None):
img, m = geometric(img, rng, geo_mode)
quads = transform_quads(quads, m)
ignore_quads = transform_quads(ignore_quads, m)
img = photometric(img, rng, strength, thermal)
return img, quads, ignore_quads