证件拍照扫描——基于C++与深度神经网络实现证件识别扫描并1比1还原证件到A4纸上

news2024/11/15 13:26:10

前言

数字化时代的到来,越来越多的证件需要进行电子化处理,例如身份证、驾驶证、护照等。在进行电子化处理时,最常见的需求就是将证件照片复制到A4纸上,以便于打印、存档或传输。同时,为了方便信息的录入和管理,还需要对证件进行格式化识别,将证件上的信息自动提取出来。

为了实现这些需求,我们可以使用深度学习与图像处理来实现用证件去复印到A4纸上的效果,以及证件的格式化识别。

处理步骤如下:

1. 拍照:使用手机或相机对证件进行拍照,确保照片清晰、无遮挡。

2. 图像处理:对照片进行目标识别、语义分割与边缘提取之后裁剪、旋转、调整亮度、对比度等,以确保照片符合要求,之后重新映射到A4纸上。

3. 格式化识别:对证件上的信息进行格式化识别,可以利用OCR技术,将证件上的文字自动提取出来,并进行分类和整理,以便于后续的信息录入和管理。

 图1:把拍照的证件1比1还原证件到A4纸上

 图2:证件OCR格式化识别

2.使用Yolo做目标识别,Enet做边缘检测,Paddle OCR做文字识别,OpenCV做处理图像,当前的开发环境为开发环境是win10,显卡RTX3080,cuda11.2,cudnn8.1,OpenCV4.5,ncnn,IDE 是Vs2019,界面是Qt 写的。

3.可以关注《可立AI科技》这个公众号,之后我会把实现的功能以网页端的形式放在这个公众号上。

一、目标识别

1.要识别的证卡有身份证正反面、银行卡正反面、社保卡正反面、港澳通行证正反面、护照、驾驶证、居住证等,这些数据都涉及到个人数据安全,所以很难找到可以使用的数据集,但训练模型又不能没有数据集,解决的办法是从网上获取一些公开的证件样本数据集,然后使用生成对抗(GAN)生成可以训练的数据集。

2.使用yolov5训练目标识别模型,关于yolov5的如果训练模型,可以看我之前的博客:《深度学习目标检测(YoloV5)项目——完整记录从数据处理开始到项目落地部署》https://blog.csdn.net/matt45m/article/details/118598706?spm=1001.2014.3001.5501

3.身份证的正面和社保卡、居住证特征很像,目标识别时往往会误检,为了更好的区分开来,在标注的时候,把头像,国徽这些统一的特征也标注出来,在目标识别之后,再做逻辑判断。比如当前识别到身份证的背面时,要去判断有没有同时识别到国徽,国徽是否在背面识别框内等。这样确保目标识别时的误检率。

static void mergeFrameRect(std::vector<cv::Rect> r1, std::vector<cv::Rect> r2, std::vector<cv::Rect>& r_m)
{
    for (int i = 0; i < r1.size(); i++)
    {
        for (int j = 0; j < r2.size(); j++)
        {
            if (computRectJoinUnion(r1.at(i), r2.at(j)))
            {
                r_m.push_back(r1.at(i));
            }
        }
    }
}

int filterTarget(std::vector<ObjectFlag>& objects, std::map<int, std::vector<cv::Rect>>& rect_frame)
{
    std::vector<cv::Rect> IDF_Frame, IDB_Frame, SSCB_Frame, SSCF_Frame, BCF_Frame, BCB_Frame, CNPF_Frame, RPF_Frame, DLF_Frame, OWPF_Frame, OWPB_Frame;

    std::vector<cv::Rect> AC, BD, CNPF, DLF, OWPB, OWPF, RPF, SSCB, SSCF, UPAY, BCF, BCB, IDF, IDB, CNM, PTT;
    //"AC标识", "书本", "护照", "驾驶证", "港澳通行证背面", "港澳通行证正面", "居住证", 
    //"社保卡背面", "社保卡正面", "银联标示", "银行卡正面", "银行卡背面", "身体证正面", "身体证背面", "国徽", "头像"
    ///目标识别返回标志位///
    //AC芯片			0		AC
    //书本				1		BD
    //护照正面			2		CNPF
    //驾驶证			3       DLF
    //港澳通行证背面    4       OWPB
    //港澳通行证正面	5	    OWPF
    //居住证			6		RPF
    //社保卡反面		7		SSCB
    //社保卡正面		8		SSCF
    //银联标志			9		UPAY
    //银行卡正面		10		BCF
    //银行卡反面		11		BCB
    //身份证正面		12		IDF
    //身份证反面		13		IDB
    //国徽				14		CNM
    //头像				15		PTT
    for (int i = 0; i < objects.size(); i++)
    {
        const ObjectFlag obj = objects[i];
        if (obj.prob >= 0.4)
        {
            switch (obj.label)
            {
            case 0:
                AC.push_back(obj.rect);
                break;
            case 1:
                BD.push_back(obj.rect);
                break;
            case 2:
                CNPF.push_back(obj.rect);
                break;
            case 3:
                DLF.push_back(obj.rect);
                break;
            case 4:
                OWPB.push_back(obj.rect);
                break;
            case 5:
                OWPF.push_back(obj.rect);
                break;
            case 6:
                RPF.push_back(obj.rect);
                break;
            case 7:
                SSCB.push_back(obj.rect);
                break;
            case 8:
                SSCF.push_back(obj.rect);
                break;
            case 9:
                UPAY.push_back(obj.rect);
                break;
            case 10:
                BCF.push_back(obj.rect);
                break;
            case 11:
                BCB.push_back(obj.rect);
                break;
            case 12:
                IDF.push_back(obj.rect);
                break;
            case 13:
                IDB.push_back(obj.rect);
                break;
            case 14:
                CNM.push_back(obj.rect);
                break;
            case 15:
                PTT.push_back(obj.rect);
                break;
            default:
                break;
            }
        }
    }
    if (BD.size() > 0)//书本
    {
        rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(10, BD));
    }
    if (IDF.size() > 0 && PTT.size() > 0)//身份证正面
    {
        mergeFrameRect(IDF, PTT, IDF_Frame);
        if (IDF_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(20, IDF_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (IDB.size() > 0 && CNM.size() > 0)//身份证反面
    {
        mergeFrameRect(IDB, CNM, IDB_Frame);
        if (IDB_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(21, IDB_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (SSCF.size() > 0 && AC.size() > 0 && PTT.size() > 0) //社保卡正面
    {
        std::vector<cv::Rect> RECT;
        mergeFrameRect(SSCF, AC, RECT);
        if (RECT.size() > 0)
        {
            mergeFrameRect(RECT, PTT, SSCF_Frame);
        }
        if (SSCF_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(30, SSCF_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (SSCB.size() > 0 && CNM.size() > 0)//社保卡反面
    {
        mergeFrameRect(SSCB, CNM, SSCB_Frame);
        if (SSCB_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(31, SSCB_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (CNPF.size() > 0 && PTT.size() > 0)//护照正面
    {
        mergeFrameRect(CNPF, PTT, CNPF_Frame);
        rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(40, CNPF_Frame));
    }
    if (BCF.size() > 0 && UPAY.size()>0)//银行卡正面
    {
        mergeFrameRect(BCF, UPAY, BCF_Frame);
        if (BCF_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(50, BCF_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (BCB.size() > 0 && UPAY.size()>0)//银行卡反面
    {
        mergeFrameRect(BCB, UPAY, BCB_Frame);
        if (BCB_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(51, BCB_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (RPF.size() > 0 && PTT.size()>0)//居住证
    {
        mergeFrameRect(RPF, PTT, RPF_Frame);
        if (RPF_Frame.size())
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(60, RPF_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (OWPF.size() > 0 && PTT.size()>0)//港澳通行证正面
    {
        mergeFrameRect(OWPF, PTT, OWPF_Frame);
        if (OWPF_Frame.size())
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(70, OWPF_Frame));
        }
        else
        {
            return -2;
        }
    }
    if (OWPB.size())//港澳通行证背面
    {
        rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(71, OWPB));
    }
    if (DLF.size() > 0 && PTT.size()>0)//驾驶证
    {
        mergeFrameRect(DLF, PTT, DLF_Frame);
        if (DLF_Frame.size() > 0)
        {
            rect_frame.insert(std::pair<int, std::vector<cv::Rect>>(80, DLF_Frame));
        }
        else
        {
            return -2;
        }
    }
    return 0;
}
int screeningTarget(cv::Rect& rect_out, std::map<int, std::vector<cv::Rect>>& rect_frame)
{
    std::vector<cv::Rect> IDF_Frame, IDB_Frame, SSCB_Frame, SSCF_Frame, BCF_Frame, BCB_Frame, CNPF_Frame, BD_Frame, RPF_Frame, DLF_Frame, OWPF_Frame, OWPB_Frame;
    std::vector<int> indes;
    for (auto i = rect_frame.begin(); i != rect_frame.end(); i++)
    {
        int index = i->first;

        //std::cout << index << std::endl;
        switch (index)
        {
        case 10:
            BD_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(BD_Frame.size())).c_str()));
            break;
        case 20:
            IDF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(IDF_Frame.size())).c_str()));
            break;
        case 21:
            IDB_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(IDB_Frame.size())).c_str()));
            break;
        case 30:
            SSCF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(SSCF_Frame.size())).c_str()));
            break;
        case 31:
            SSCB_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(SSCB_Frame.size())).c_str()));
            break;
        case 40:
            CNPF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(CNPF_Frame.size())).c_str()));
            break;
        case 50:
            BCF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(BCF_Frame.size())).c_str()));
            break;
        case 51:
            BCB_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(BCB_Frame.size())).c_str()));
            break;
        case 60:
            RPF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(RPF_Frame.size())).c_str()));
            break;
        case 70:
          
            OWPF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(OWPF_Frame.size())).c_str()));
            break;
        case 71:
            OWPB_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(OWPB_Frame.size())).c_str()));
            break;
        case 80:
            DLF_Frame = i->second;
            indes.push_back(atoi((std::to_string(i->first) + std::to_string(DLF_Frame.size())).c_str()));
            break;
        default:
            break;
        }
    }
    if (indes.size() > 0)
    {
        for (int i = 0; i < indes.size(); i++)
        {

            int index = std::stoi(std::to_string(indes.at(i)).substr(0, 2));
            switch (index)
            {
            case 10:
            {
                rect_out = BD_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 20:
            {
                rect_out = IDF_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 21:
            {
                rect_out = IDB_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 30:
            {
                rect_out = SSCF_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 31:
            {
                rect_out = SSCB_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 40:
            {
                rect_out = CNPF_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 50:
            {
                rect_out = BCF_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 51:
            {
                rect_out = BCB_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 60:
            {
                rect_out = RPF_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 70:
            {
                rect_out = OWPF_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 71:
            {
                rect_out = OWPB_Frame.at(0);
                return indes.at(i);
            }
            break;
            case 80:
            {
                rect_out = DLF_Frame.at(0);
                return indes.at(i);
            }
            break;
            default:
                break;
            }
        }
    }
    return 0;
}


识别效果如下,可以很好的识别到正反面,加上一些逻辑上的处理之后,基本精度能在99%左右。

  

二、边缘提取与校正

1.获取当前目标之后,对当前目标进行进行边缘提取,这里我使用了Enet做语义分割,这里先择了Enet做目标分割,证卡目标分割场景并不是很复杂,使用Enet可以不用考虑推理速度的问题

 关于Enet网络的训练步骤可以参考我之前的博客,处理步骤是一样的轻量化实时语义分割LiteSeg——从算法原理到模型训练与部署https://blog.csdn.net/matt45m/article/details/124539667?spm=1001.2014.3001.5502ENet的训练框架是Pytorch,可以使用git上的这个源码进行训练:GitHub - davidtvs/PyTorch-ENet: PyTorch implementation of ENetPyTorch implementation of ENet. Contribute to davidtvs/PyTorch-ENet development by creating an account on GitHub.https://github.com/davidtvs/PyTorch-ENet 

2.训练好模型之后,把模型量化成FP16以提升速度,用NCNN实现模型推理

/// <summary>
/// 语义分割
/// </summary>
/// <param name="ncnn_net">分割模型</param>
/// <param name="cv_src">输入图像</param>
/// <param name="cv_enet">输出分割后的图像</param>
/// <param name="threadsm">阈值</param>
/// <param name="image_size">推理尺寸大小</param>
/// <returns></returns>
static int enetSegmentation(cv::Mat& cv_src, cv::Mat& cv_enet, ncnn::Net& ncnn_net, int threadsm, int image_size)
{

    if (cv_src.empty())
    {
        return -20;
    }
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(cv_src.data, ncnn::Mat::PIXEL_BGR, cv_src.cols, cv_src.rows, image_size, image_size);

    const float norm_vals[3] = { 1 / 255.f, 1 / 255.f, 1 / 255.f };
    in.substract_mean_normalize(0, norm_vals);
    ncnn::Extractor ex = ncnn_net.create_extractor();
    ex.set_num_threads(threadsm);

    ncnn::Mat out;


    ex.input("input.1", in);
    ex.extract("887", out);

    cv::Mat cv_seg = cv::Mat::zeros(cv::Size(out.w, out.h), CV_8UC1);
    for (int i = 0; i < out.h; ++i)
    {
        for (int j = 0; j < out.w; ++j)
        {
            const float* bg = out.channel(0);
            const float* fg = out.channel(1);
            if (bg[i * out.w + j] < fg[i * out.w + j])
            {
                cv_seg.data[i * out.w + j] = 255;
            }
        }
    }

    cv::resize(cv_seg, cv_enet, cv::Size(cv_src.cols, cv_src.rows), cv::INTER_LINEAR);
    return 0;
}

分割效果:

 

 3.获取目标的分割位置之后要对目标的连边缘进行提取,要进行直线检测,直线可以用传统算法,OpenCV的霍夫曼直接检测,也可以用基于深度学习的M-LSD,如果用M-LSD也可以直接省掉语义分割的这一步,直线检测效果的对比可以看我之前的博客:直线检测——对比M-LSD直线检测(基于深度学习)与霍夫曼直线检测https://blog.csdn.net/matt45m/article/details/124362068?spm=1001.2014.3001.5502

直线检测与拟合边缘点:

static int getCorrectionPoint(cv::Mat cv_edge, cv::Mat& cv_enet, std::vector<cv::Point>& points_out,
    double theta = 50, int threshold = 30, double minLineLength = 10)
{
    std::vector<cv::Vec4f> lines;
    HoughLinesP(cv_edge, lines, 1, CV_PI * 1 / 180, theta, threshold, minLineLength);

    if (lines.size() <= 3)
    {
        int mask = enetLinesToPoint(cv_enet, points_out);

        return std::stoi(std::to_string(42) + std::to_string(mask));
    }

    std::vector<Line> horizontals, verticals;

    linesDichotomy(lines, horizontals, verticals, cv_edge);

    if (horizontals.size() < 2 || verticals.size() < 2)
    {
        int mask = enetLinesToPoint(cv_enet, points_out);

        return std::stoi(std::to_string(43) + std::to_string(mask));
    }
    std::vector<Line> lines_out;
    screenLines(horizontals, verticals, lines_out, 40);

    if (lines_out.size() < 4)
    {
        int mask = enetLinesToPoint(cv_enet, points_out);

        return std::stoi(std::to_string(44) + std::to_string(mask));
    }

    if (decideAngle(lines_out))
    {
        int mask = enetLinesToPoint(cv_enet, points_out);

        return std::stoi(std::to_string(45) + std::to_string(mask));
    }

    std::vector<cv::Point> points;
    points.push_back(computeIntersect(lines_out.at(0), lines_out.at(2)));
    points.push_back(computeIntersect(lines_out.at(0), lines_out.at(3)));
    points.push_back(computeIntersect(lines_out.at(2), lines_out.at(1)));
    points.push_back(computeIntersect(lines_out.at(1), lines_out.at(3)));

    if (decodeArea(cv_enet, points, 4))
    {
        int mask = enetLinesToPoint(cv_enet, points_out);

        return std::stoi(std::to_string(46) + std::to_string(mask));
    }
    if (((points.at(1).x - points.at(0).x) < 60) || ((points.at(3).x - points.at(2).x) < 60) || 
        ((points.at(2).y - points.at(0).y) < 60) || ((points.at(3).y - points.at(1).y) < 60))
    {
        int mask = enetLinesToPoint(cv_enet, points_out);

        return std::stoi(std::to_string(47) + std::to_string(mask));
    }

    points_out = points;

    return 400;
}

 4.获取边缘之后,要对边缘进行校正,校正就是把边缘的四个点重新映射到平面上,为了更智能化的处理,在这里加上了文字检测与文字角度检测,这样可以更智能化的处理用户拍照的证件方向与实际映射的方向,不管是什么方向,最终得到的校正后图像文字都是正过来的,避免用户过多的参与操作。最终是要区分出正摆的文字和颠倒的文字,步骤是先对图像做文字检测,之后对检测到的文字做角度检测,积分后,判断出当前文字的方向,旋转图像。

int reviseImage(cv::Mat& cv_src, cv::Mat& cv_dst, ncnn::Net& db_net, 
    ncnn::Net& angle_net, std::vector<cv::Point>& in_points)
{
    int val = verify();
    if (val != 0)
    {
        return val;
    }

    if (cv_src.empty())
    {
        return -20;
    }
    cv::Mat cv_warp = cv_src.clone();
    if (in_points.size() != 4)
    {
        return -444;
    }

    cv::Point point_f, point_b;

    point_f.x = (in_points.at(0).x < in_points.at(2).x) ? in_points.at(0).x : in_points.at(2).x;
    point_f.y = (in_points.at(0).y < in_points.at(1).y) ? in_points.at(0).y : in_points.at(1).y;
    point_b.x = (in_points.at(3).x > in_points.at(1).x) ? in_points.at(3).x : in_points.at(1).x;
    point_b.y = (in_points.at(3).y > in_points.at(2).y) ? in_points.at(3).y : in_points.at(2).y;

    //2020.8.24更新了比例不对的问题,加了点到点之间的距离运算,最终取水平与垂直线最长线
    float l_1 = getDistance(in_points.at(0), in_points.at(1));
    float l_2 = getDistance(in_points.at(2), in_points.at(3));
    float l_3 = getDistance(in_points.at(1), in_points.at(3));
    float l_4 = getDistance(in_points.at(0), in_points.at(2));

    int width = l_1 >= l_2 ? l_1 : l_2;
    int height = l_3 >= l_4 ? l_3 : l_4;

    //旧代码取目标的最小外接矩形,但倾斜45度时会出现比例变形的现象
    //cv::Rect rect(point_f, point_b);
    cv_dst = cv::Mat::zeros(height, width, CV_8UC3);

    std::vector<cv::Point2f> dst_pts;
    dst_pts.push_back(cv::Point2f(0, 0));
    dst_pts.push_back(cv::Point2f(width - 1, 0));
    dst_pts.push_back(cv::Point2f(0, height - 1));
    dst_pts.push_back(cv::Point2f(width - 1, height - 1));

    std::vector<cv::Point2f> tr_points;
    tr_points.push_back(in_points.at(0));
    tr_points.push_back(in_points.at(1));
    tr_points.push_back(in_points.at(2));
    tr_points.push_back(in_points.at(3));

    cv::Mat transmtx = getPerspectiveTransform(tr_points, dst_pts);

    cv::Mat cv_revise;
    cv::warpPerspective(cv_warp, cv_revise, transmtx, cv_dst.size());


    std::vector<cv::Mat> cv_dsts;
    int vh = cutTextLines(cv_revise, db_net, cv_dsts);

    int angle = directionTextLines(cv_dsts, angle_net);

    //std::cout << angle << std::endl;

    int rotate_angle = 0;

    if (vh == 7 && angle == 1)//横排文字竖放从上到下
    {
        rotate_angle = 90;
    }
    if (vh == 7 && angle == 0)//横排文字竖放从下到上
    {
        rotate_angle = 270;
    }
    if (vh == 4 && angle == 0)//横排文字颠倒
    {
        rotate_angle = 180;
    }
    if (vh == 7 && angle == 3)//竖排文字正常
    {
        rotate_angle = 0;
    }
    if (vh == 4 && angle == 2)//竖排文字横放从下到上
    {
        rotate_angle = 90;
    }
    if (vh == 4 && angle == 3)//竖排文字横放从上到下
    {
        rotate_angle = 270;
    }
    if (vh == 7 && angle == 2)//竖排文字颠倒
    {
        rotate_angle = 180;
    }
    if (vh == 4 && angle == 1)//横排文字正常
    {
        rotate_angle = 0;
    }
    if (vh == 7 && angle < 0)//横排文字检测不到多于3行的文字
    {
        rotate_angle = 0;
    }
    if (vh == 4 && angle < 0)//竖排文字检测不到多于3行的文字
    {
        rotate_angle = 90;
    }

    switch (rotate_angle)
    {
    case 0:
        cv_dst = cv_revise;
        break;
    case 90:
        cv_dst = rotateMat(cv_revise, 0);
        break;
    case 180:
        cv_dst = rotateMat(cv_revise, -1);
        //flip(cv_revise, cv_dst, -1);
        break;
    case 270:
        cv_dst = rotateMat(cv_revise, 1);
        break;
    default:
        break;
    }
   
    return std::stoi(std::to_string(vh) + std::to_string(angle));
}

不管拍照时图像是什么状态,最张剪切出来的证件都是文字摆正的状态:

 

三、正反面合并

1、按使用复印机扫描的正常逻辑,合并扫描时,正面的那一面都是要放在A4最上边,那么这里就要做出逻辑上的判断,如果合并的时同一类型的证件,比如只合并身份证,不混着合并身份证和银行卡,那么身份证的正面就就放在A4纸的最上边,身份证的背面放在A4纸的下边,这里要用到之前做目标识别时所获取的目标标识位来做处理。

void KL_SmartOffice::merge_imgae()
{
    ui.label_r->setMaximumWidth(0);
    ui.widget_d->setMaximumHeight(0);
    
    if (btnGroup->checkedId() == 1)
    {
        if (!cv_dis_1.empty() && !cv_dis_2.empty())
        {
            if (od_index_1 == 20 && od_index_2 == 21)
            {
                scan.merge_a4(cv_dis_1, cv_dis_2, cv_merge);
            }
            else if (od_index_1 == 21 && od_index_2 == 20)
            {
                scan.merge_a4(cv_dis_2, cv_dis_1, cv_merge);
            }
            else if (od_index_1 ==30 && od_index_2 == 31)
            {
                scan.merge_a4(cv_dis_1, cv_dis_2, cv_merge);
            }
            else if (od_index_1 == 31 && od_index_2 == 30)
            {
                scan.merge_a4(cv_dis_2, cv_dis_1, cv_merge);
            }
            else if (od_index_1 == 50 && od_index_2 == 51)
            {
                scan.merge_a4(cv_dis_1, cv_dis_2, cv_merge);
            }
            else if (od_index_1 == 51 && od_index_2 == 50)
            {
                scan.merge_a4(cv_dis_2, cv_dis_1, cv_merge);
            }
            else
            {
                scan.merge_a4(cv_dis_1, cv_dis_2, cv_merge);
            }
        }
       
        ui.pushButton_save->setEnabled(true);
        ui.pushButton_style->setEnabled(false);
        ui.pushButton_rotate->setEnabled(false);
        ui.pushButton_merge->setEnabled(false);
        ui.pushButton_ocr->setEnabled(false);
    }
}

2.合并到A4纸上,分辨率是300dpi,如果太低了,合并出来的照片就会很模糊。

void ScanJia::merge_a4(const cv::Mat& cv_src_1, const cv::Mat& cv_src_2, cv::Mat& cv_dst)
{
    cv::Mat cv_1, cv_2;
    cv::resize(cv_src_1, cv_1, cv::Size(1031, 658));
    cv::resize(cv_src_2, cv_2, cv::Size(1031, 658));
    cv_dst = cv::Mat(3508, 2479, CV_8UC3, cv::Scalar(255, 255, 255));
    cv::Mat cv_one = cv_dst(cv::Rect(724, 700,cv_1.cols, cv_1.rows));
    cv_1.copyTo(cv_one);
    cv::Mat cv_two = cv_dst(cv::Rect(724, 2058,cv_2.cols, cv_2.rows));
    cv_2.copyTo(cv_two);
}

合并的效果如下,港澳通行证正反面合并:

身份证正反面合并:

四、OCR文字识别

1.证件的文字识别,使用场景相对简单一些,为了之后可能要上到移动端考虑,选择了paddle OCR移动端模型,方便库的统一管理,Paddle模型要转ncnn模型,模型转换可以参考:

https://github.com/FeiGeChuanShu/ncnn_paddleocr ​​​​​​https://github.com/FeiGeChuanShu/ncnn_paddleocr

2.NCNN推理代码 

 OcrResult OcrLite::detect(const cv::Mat& mat, int padding, int maxSideLen,
        float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle)
    {
        cv::Mat originSrc = mat;
        int originMaxSide = (std::max)(originSrc.cols, originSrc.rows);
        int resize;
        if (maxSideLen <= 0 || maxSideLen > originMaxSide) {
            resize = originMaxSide;
        }
        else {
            resize = maxSideLen;
        }
        resize += 2 * padding;
        cv::Rect paddingRect(padding, padding, originSrc.cols, originSrc.rows);
        cv::Mat paddingSrc = makePadding(originSrc, padding);
        ScaleParam scale = getScaleParam(paddingSrc, resize);
        OcrResult result;
        result = detect(NULL, NULL, paddingSrc, paddingRect, scale,
            boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
        return result;
    }

    std::vector<cv::Mat> OcrLite::getPartImages(cv::Mat& src, std::vector<TextBox>& textBoxes)
    {
        std::vector<cv::Mat> partImages;
        for (int i = 0; i < textBoxes.size(); ++i)
        {
            cv::Mat partImg = getRotateCropImage(src, textBoxes[i].boxPoint);
            partImages.emplace_back(partImg);
        }
        return partImages;
    }

    OcrResult OcrLite::detect(const char*, const char*,
        cv::Mat& src, cv::Rect& originRect, ScaleParam& scale,
        float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle) {

        cv::Mat textBoxPaddingImg = src.clone();
        int thickness = getThickness(src);

        double startTime = getCurrentTime();
        std::vector<TextBox> textBoxes = dbNet.getTextBoxes(src, scale, boxScoreThresh, boxThresh, unClipRatio);
        double endDbNetTime = getCurrentTime();
        double dbNetTime = endDbNetTime - startTime;

        drawTextBoxes(textBoxPaddingImg, textBoxes, thickness);

        //---------- getPartImages ----------
        std::vector<cv::Mat> partImages = getPartImages(src, textBoxes);

        std::vector<Angle> angles;
        angles = angleNet.getAngles(partImages, doAngle, mostAngle);


        //Rotate partImgs
        for (int i = 0; i < partImages.size(); ++i) {
            if (angles[i].index == 1) {
                partImages.at(i) = matRotateClockWise180(partImages[i]);
            }
        }
        std::vector<TextLine> textLines = crnnNet.getTextLines(partImages);

        std::vector<TextBlock> textBlocks;
        for (int i = 0; i < textLines.size(); ++i) {
            std::vector<cv::Point> boxPoint = std::vector<cv::Point>(4);
            int padding = originRect.x;//padding conversion
            boxPoint[0] = cv::Point(textBoxes[i].boxPoint[0].x - padding, textBoxes[i].boxPoint[0].y - padding);
            boxPoint[1] = cv::Point(textBoxes[i].boxPoint[1].x - padding, textBoxes[i].boxPoint[1].y - padding);
            boxPoint[2] = cv::Point(textBoxes[i].boxPoint[2].x - padding, textBoxes[i].boxPoint[2].y - padding);
            boxPoint[3] = cv::Point(textBoxes[i].boxPoint[3].x - padding, textBoxes[i].boxPoint[3].y - padding);
            TextBlock textBlock{ boxPoint, textBoxes[i].score, angles[i].index, angles[i].score,
                                angles[i].time, textLines[i].text, textLines[i].charScores, textLines[i].time,
                                angles[i].time + textLines[i].time };
            textBlocks.emplace_back(textBlock);
        }

        double endTime = getCurrentTime();
        double fullTime = endTime - startTime;

        //cropped to original size
        cv::Mat textBoxImg;

        if (originRect.x > 0 && originRect.y > 0) {
            textBoxPaddingImg(originRect).copyTo(textBoxImg);
        }
        else {
            textBoxImg = textBoxPaddingImg;
        }

        std::string strRes;
        for (int i = 0; i < textBlocks.size(); ++i) {
            strRes.append(textBlocks[i].text);
            strRes.append("\n");
        }

        return OcrResult{ dbNetTime, textBlocks, textBoxImg, fullTime, strRes };
    }
}

 3.识别的效果

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/464798.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一条命令搭建HTTP服务器

文章目录 1.前言2.本地http服务器搭建2.1.Python的安装和设置2.2.Python服务器设置和测试 3.cpolar的安装和注册3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 转载自远程内网穿透的文章&#xff1a;【Python】快速简单搭建HTTP服务器并公网访问「cpolar内网穿透…

word自带公式编辑

快捷键&#xff1a; 公式编辑&#xff1a;alt“” 上标&#xff1a;x^i 空格 下标&#xff1a;x_i 空格 实数R&#xff1a;\doubleR 空格 偏微分算子&#xff1a;“\partial” 极限&#xff1a;“\limit”&#xff08;按空格后会显示一串很长的式子&#xff0c;再空格就变…

在Linux操作系统上部署wgcloud监控

1.wgcloud监控介绍 1.1 介绍 ​ 这是一款开源的主机监控系统&#xff0c;可以支持主机各种指标监测&#xff08;cpu使用率&#xff0c;cpu温度&#xff0c;内存使用率&#xff0c;磁盘容量空间&#xff0c;磁盘IO&#xff0c;硬盘SMART健康状态&#xff0c;系统负载&#xff…

starrocks基于prometheus实现监控告警

监控报警 本文介绍如何为 StarRocks 设置监控报警。 StarRocks 提供两种监控报警的方案。企业版用户可以使用内置的 StarRocksManager&#xff0c;其自带的 Agent 从各个 Host 采集监控信息&#xff0c;上报至 Center Service&#xff0c;然后做可视化展示。StarRocksManager …

[陇剑杯 2021]之Misc篇(NSSCTF)刷题记录⑤

NSSCTF-Misc篇-[陇剑杯 2021] 日志分析:[陇剑杯 2021]日志分析&#xff08;问1&#xff09;[陇剑杯 2021]日志分析&#xff08;问2&#xff09;[陇剑杯 2021]日志分析&#xff08;问3&#xff09; 简单日志分析&#xff1a;[陇剑杯 2021]简单日志分析&#xff08;问1&#xff0…

Python制作一个自动发送弹幕的工具,让你看直播不冷场

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 让我们先看看效果&#xff1a; 名字我就打码了&#xff0c;当然名字不是关键&#xff0c;我直接截图展示算了&#xff0c;GIF的话&#xff0c;太麻烦了。 环境使用: Python 3.8 / 编译器 Pycharm 2021.2版本 / 编辑器…

存在列排斥力的另一例证

( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有3个节点&#xff0c;AB训练集各由5张二值化的图片组成&#xff0c;让A中有5个1&#xff0c;B中全是0&#xff0c;排列组合A的所有可能&#xff0c;统计迭代次数的顺序。其中有12组数据 A-B 迭代次数 1 0 1 5*4*2*1…

Inception 深度卷积神经网络(CNN)架构

Inception是一种深度卷积神经网络&#xff08;CNN&#xff09;架构&#xff0c;由Google在2014年提出。它是一种基于多尺度卷积的网络结构&#xff0c;旨在解决传统CNN在处理不同大小的输入图像时存在的问题。 Inception的主要特点是使用了多个不同尺度的卷积核来提取不同尺度…

API测试| 了解API接口测试| API接口测试指南(一)

什么是API&#xff1f; API是一个缩写&#xff0c;它代表了一个 pplication P AGC软件覆盖整个房间。API是用于构建软件应用程序的一组例程&#xff0c;协议和工具。API指定一个软件程序应如何与其他软件程序进行交互。 例行程序&#xff1a;执行特定任务的程序。例程也称为过…

人脸检测和行人检测2:YOLOv5实现人脸检测和行人检测(含数据集和训练代码)

人脸检测和行人检测2&#xff1a;YOLOv5实现人脸检测和行人检测(含数据集和训练代码) 目录 人脸检测和行人检测2&#xff1a;YOLOv5实现人脸检测和行人检测(含数据集和训练代码) 1. 前言 2. 人脸检测和行人检测数据集说明 &#xff08;1&#xff09;人脸检测和行人检测数据…

初识C++之左值引用与右值引用

目录 一、左值引用与右值引用 1. 左值和右值的概念 1.1 左值 1.2 右值 1.3 左值与右值的区分 2. 左值引用与右值引用 2.1 左值引用与右值引用的使用方法 2.2 左值引用的可引用范围 2.3 右值引用的可引用范围 3. 右值引用的作用 3.1 减少传值返回的拷贝 3.2 插入时的…

2023北京新一代信息技术应用融合创新人才发展峰会暨鲲鹏开发者创享日·北京站成功举办

以技术创新促产业发展&#xff0c;以开放使能筑人才根基 4月25日&#xff0c;由北京市经济和信息化局、北京市朝阳区人民政府、国家工业信息安全发展研究中心与华为技术有限公司联合主办&#xff0c;北京鲲鹏联合创新中心、北京市中小企业公共服务平台、中国软件行业协会承办的…

字节超全学习流程图流出,100天涨薪10k,从功能测试到自动化测试

今年年初&#xff0c;由于经济压力让我下定决心进阶自动化测试&#xff0c;已经24的我做了3年功能测试&#xff0c;坐标广州薪资定格在8k&#xff0c;可能是生活过的太安逸&#xff0c;觉得8000的工资也够了。 但是生活总是多变的&#xff0c;女朋友的突然怀孕&#xff0c;让我…

软件测试面试一定要看的面试题和笔试题全套教程

1、什么是软件测试&#xff1f;2’ 【要点】 在规定条件下对程序进行操作&#xff0c;以发现错误&#xff0c;对软件质量进行评估&#xff0c;包括对软件形成过程的文档、数据以及程序进行测试。 【详解】 软件测试就是在软件投入运行前对软件需求分析、软件设计规格说明书…

ApplicationContextAware接口

一、ApplicationContextAware接口的基本介绍 public interface ApplicationContextAware extends Aware {void setApplicationContext(ApplicationContext applicationContext) throws BeansException;}在Spring/SpringMVC中&#xff0c;我们拿到IOC容器无非有三种方式&#x…

通达信结构紧凑形态选股公式编写思路

在威廉欧奈尔的《笑傲股市》、马克米勒维尼的《股票魔法师》等书籍中都有结构紧凑形态的相关描述&#xff0c;股票在形成基底时&#xff0c;价格波动幅度逐渐减小&#xff0c;量能逐步萎缩&#xff0c;同时价格相对强度较高。 结构紧凑的形态通过眼睛观察&#xff0c;一般可以…

JS类的学习

文章目录 一、JavaScript 类(class)二、JavaScript 类继承三、 JavaScript 静态方法总结 一、JavaScript 类(class) 类是用于创建对象的模板。 我们使用 class 关键字来创建一个类&#xff0c;类体在一对大括号 {} 中&#xff0c;我们可以在大括号 {} 中定义类成员的位置&…

【Shell编程之条件语句】

目录 一、条件测试操作1、test命令2、文件测试2.1、常用的测试操作符 3、整数值比较3.1、常用的测试操作符(重点&#xff09; 4、逻辑测试4.1、常用的测试操作符号 二、if语句的结构1、单分支结构2、双分支结构3.多分支结构 一、条件测试操作 1、test命令 测试表达式是否成立…

同城跑腿APP开发需具备哪些功能?

移动互联网的飞速发展加上人们生活水平的提高&#xff0c;生活工作闲暇之余&#xff0c;人们不愿意为买药、送文件、取东西、送花、排队等小事浪费时间或者是根本没有时间去处理类似的事情。这个时候就想如果能够花钱请人来替我做这些事就好了&#xff0c;于是同城跑腿就在这样…

C/C++中的数据结构对齐,#pragma pack() 和 __attribute__

C/C中的数据结构对齐 总览 数据结构对齐是指在计算机内存中排列和访问数据的方式。它包含三个独立但相关的问题&#xff1a;数据对齐&#xff08;data alignment&#xff09;&#xff0c;数据结构填充&#xff08; data structure padding&#xff09;和打包&#xff08;pack…