模拟相机拍照——对文档进行数据增强

一. 背景

假如我们有一个标准文件,我们对其进行文字识别、版面分析或者其他下游任务就比较容易。然而,当图片是手机拍照获取的,图片中往往有阴影、摩尔纹、弯曲。

那么,如何通过标准的文档,获得类似相机拍照的图片呢?

这里介绍的就是文档数据增强,用标准文档模拟相机拍照场景。该方法不仅能用于文档各场景的数据增强,用于OCR检测识别等任务;还能合成各种图片训练对,用于文档去阴影、文档去摩尔纹、文档弯曲矫正等各项任务。

二. 效果实现

首先给大家展示的是一个PDF截图和对应的标注(红色为标注框)

下面给标准图片分别添加阴影、摩尔纹、弯曲,效果如下:

摩尔纹+弯曲,并且把标注点映射到弯曲图片上,如下图所示:

阴影+弯曲,并且把标注点映射到弯曲图片上,如下图所示:

三. 算法原理与代码实现

原理:利用渲染工具(推荐blender),渲染出各种弯曲、阴影、摩尔纹,然后再pdf图片上进行合成。

最后,一定要代码实现(只给初级版本,完整版本比较复杂):

python 复制代码
import os
import cv2
import json
import random
import numpy as np
from scipy.interpolate import LinearNDInterpolator as linterp
from scipy.interpolate import NearestNDInterpolator as nearest


class LinearNDInterpolatorExt(object):
    def __init__(self, points, values):
        self.funcinterp = linterp(points, values)
        self.funcnearest = nearest(points, values)

    def __call__(self, *args):
        z = self.funcinterp(*args)
        chk = np.isnan(z)
        if chk.any():
            return np.where(chk, self.funcnearest(*args), z)
        else:
            return z


def crop_flow_from_nan(flow):
    mask = ~np.any(np.isnan(flow), -1)
    x, y, w, h = cv2.boundingRect(mask.astype(np.uint8))
    flow = flow[y: y + h, x: x + w]
    mask = mask[y: y + h, x: x + w]
    max_nonzero_ratio = 0.9
    max_crop_size = 20
    mask_h, mask_w = mask.shape[0], mask.shape[1]
    y0 = max_crop_size
    for i in range(0, max_crop_size):
        if np.count_nonzero(mask[i]) / mask_w > max_nonzero_ratio:
            y0 = i
            break

    y1 = mask_h - 1 - max_crop_size
    for i in range(mask_h - 1, y1, -1):
        if np.count_nonzero(mask[i]) / mask_w > max_nonzero_ratio:
            y1 = i
            break

    crop_mask = mask[y0:y1]
    mask_h, mask_w = crop_mask.shape[0], crop_mask.shape[1]
    x0 = max_crop_size
    for i in range(0, x0):
        if np.count_nonzero(mask[:, i]) / mask_h > max_nonzero_ratio:
            x0 = i
            break

    x1 = mask_w - 1 - max_crop_size
    for i in range(mask_w - 1, x1, -1):
        if np.count_nonzero(mask[:, i]) / mask_h > max_nonzero_ratio:
            x1 = i
            break
    flow = flow[y0:y1, x0:x1]
    return flow


def flow_2_points(flow, pts):
    """
    根据flow映射场反向计算点的对应点
    :param flow: 前向、或后向映射场, range (-1,  1)
    :param pts: 目标图、或原图的坐标点, 点经过归一化 range (0, 1),  shape: (n, 2)
    :return: 原图、或目标图的坐标点, 经过归一化 range (0, 1), shape: (n, 2)
    """
    mask = ~np.any(np.isnan(flow), -1)
    flow_masked = flow[mask]
    flow_w, flow_h = flow.shape[1], flow.shape[0]

    flow_xrange = np.arange(flow_w, dtype=np.float32)
    flow_yrange = np.arange(flow_h, dtype=np.float32)
    flow_xgrid, flow_ygrid = np.meshgrid(flow_xrange, flow_yrange)
    flow_xgrid_masked = flow_xgrid[mask]
    flow_ygrid_masked = flow_ygrid[mask]

    src_pts = (pts - 0.5) * 2  # (0-1) to (-1, 1)
    interpX = LinearNDInterpolatorExt(np.reshape(flow_masked, [-1, 2]), flow_xgrid_masked.reshape(-1))
    interpY = LinearNDInterpolatorExt(np.reshape(flow_masked, [-1, 2]), flow_ygrid_masked.reshape(-1))
    fm_x = interpX(src_pts)
    fm_y = interpY(src_pts)
    # fm_x, fm_y range is (0, flow_w-1)  and (0, flow_h-1), need convert to (0-1)
    fm_x = fm_x / (flow_w - 1)
    fm_y = fm_y / (flow_h - 1)
    return np.stack((fm_x, fm_y), axis=-1)


def warp_img(img, flow, points_list):
    h, w, _ = img.shape
    flow = crop_flow_from_nan(flow)
    flow = flow.astype(np.float32)
    flow = cv2.resize(flow, (256, 256))
    points_list_warp = []
    for points in points_list:
        points = points.astype(np.float64)
        points[:, 0] /= w*1.0
        points[:, 1] /= h*1.0
        points_warp = flow_2_points(flow, points)
        points_warp[:, 0] *= w
        points_warp[:, 1] *= h
        points_list_warp.append(points_warp)

    bm_flow = flow / 2 + 0.5
    bm_flow[..., 0] = bm_flow[..., 0] * w
    bm_flow[..., 1] = bm_flow[..., 1] * h
    bm_flow = np.nan_to_num(bm_flow, nan=-1)
    if bm_flow.shape[0] != h or bm_flow.shape[1] != w:
        bm_flow = cv2.resize(bm_flow, (w, h))

    warp_img = cv2.remap(img, bm_flow.astype(np.float32), None, cv2.INTER_LINEAR, borderValue=(255, 255, 255))
    return warp_img, points_list_warp


def json_2_points(json_path):
    with open(json_path, "r") as f:
        data = json.load(f)
    obj_list = []
    for obj in data[0]['annotations']:
        obj = obj['coordinates']
        cx, cy, w, h = obj['x'], obj['y'], obj['width'], obj['height']
        x1 = cx - 0.5 * w
        x2 = cx + 0.5 * w
        y1 = cy - 0.5 * h
        y2 = cy + 0.5 * h
        points = np.array([[x1,y1], [x2,y1], [x2,y2], [x1,y2]], np.int32)
        obj_list.append(points)
    return obj_list


def add_background(img, img_background):
    height, width, _ = img.shape
    background = cv2.resize(img_background, (width, height))
    img_res = img * 0.5 + background * 0.5
    img_res = np.clip(img_res, 0, 255)
    return img_res


if __name__ == "__main__":
    img = cv2.imread("test.png")
    shadow = cv2.imread("./background/shadow.jpg")
    img = add_background(img, shadow)
    obj_list = json_2_points("test.json")
    flow = np.load("test.npy")
    warp_img, points_list_warp = warp_img(img, flow, obj_list)
    cv2.imwrite("warp_shadow.jpg", warp_img)
    for points in points_list_warp:
        cv2.polylines(warp_img, [points.astype(np.int32)], isClosed=True, color=(0, 0, 255), thickness=1)
    cv2.imwrite("warp_shadow_draw.jpg", warp_img)

致谢,在写代码过程中受到了鑫哥的启发,再次表示感谢!

欢迎小伙伴们技术交流~

相关推荐
OCR_wintone42114 小时前
易泊车牌识别相机,助力智慧工地建设
人工智能·数码相机·ocr
lrlianmengba1 天前
推荐一款可视化和检查原始数据的工具:RawDigger
人工智能·数码相机·计算机视觉
CV-X.WANG1 天前
【详细 工程向】基于Smart3D的五镜头相机三维重建
数码相机·3d
小负不负2 天前
使用kalibr_calibration标定相机(realsense)和imu(h7min)
数码相机·opencv·计算机视觉
xm一点不soso2 天前
树莓派基本设置--10.使用MIPI摄像头
人工智能·数码相机·计算机视觉
古月居GYH3 天前
【图像与点云融合教程(五)】海康相机 ROS2 多机分布式实时通信功能包
分布式·数码相机
顾北川_野3 天前
Android 解决MTK相机前摄镜像问题
数码相机
创小董7 天前
履带式排爆演习训练机器人技术详解
数码相机·机器人
shuxianshrng7 天前
违停拍照和闯红灯拍照有什么区别吗
人工智能·数码相机·计算机视觉·视觉检测
图灵追慕者7 天前
机器视觉中光源镜头和相机的关系
数码相机·相机·机器视觉·光源·镜头