opencv在图片上添加中文汉字(c++以及python)

opencv在图片上添加中文汉字(c++以及python)_c++ opencv绘制中文 知乎-CSDN博客

环境:

  • ubuntu18.04 desktop
  • opencv 3.4.15
  • opencv是不支持中文的。

    这里C++代码是采用替换原图的像素点来实现的,实现之前我们先了解一下汉字点阵字库。

    python代码是采用将opencv格式转为PIL格式实现的。

    不想了解汉字点阵字库的,可以直接看代码

    一. 汉字点阵字库实现原理

    1. 汉字编码

    1.1 区位码

    在国标 GD2312—80 中规定,所有的国标汉字及符号分配在一个 94 行、94 列的方阵中,方阵的每一行称为一个“区”,编号为 01 区到 94 区,每一列称为一个“位”,编号为 01 位到 94 位,方阵中的每一个汉字和符号所在的区号和位号组合在一起形成的四个阿拉伯数字就是它们的“区位码”。

    区位码的前两位是它的区号,后两位是它的位号。

    用区位码就可以唯一地确定一个汉字或符号,反过来说,任何一个汉字或符号也都对应着一个唯一的 区位码。

    汉字“母”字的区位码是 3624,表明它在方阵的 36 区 24 位,问号“?”的区位码为 0331,则它在 03 区 3l 位。

    1.2 机内码

    汉字的机内码是指在计算机中表示一个汉字的编码。

    机内码与区位码稍有区别

    如上所述,汉字区位码的区码和位码的取值均在 1-94 之间,如直接用区位码作为机内码,就会与基本 ASCII 码混淆。

    为了避免机内码与基本 ASCII 码的冲突,需要避开基本 ASCII 码中的控制码(00H~1FH),还需与基本 ASCII 码中的字符相区别。

    为了实现这两点,可以先在区码和位码分别加上 20H,在此基础上再加 80H(此处“H”表示前两位数字为十六进制数)。经过这些处理,用机内码表示一个汉字需要占两个字节,分别称为高位字节和低位字节,这两位字节的机内码按如下规则表示:

    高位字节 = 区码 + 20H + 80H(或区码 + A0H)

    低位字节 = 位码 + 20H + 80H(或位码 + AOH)

    由于汉字的区码与位码的取值范围的十六进制数均为 01H~5EH(即十进制的 01~94),所以汉字的高位字节与低位字节的取值范围则为 A1H~FEH(即十进制的 161~254)。 例如,汉字“啊”的区位码为 1601,区码和位码分别用十六进制表示即为 1001H,它 的机内码的高位字节为 B0H,低位字节为 A1H,机内码就是 B0A1H。

    2. 点阵字库结构

    2.1 点阵字库存储

    在汉字的点阵字库中,每个字节的每个位都代表一个汉字的一个点,每个汉字都是由一个矩形的点阵组成,0 代表没有,1 代表有点,将 0 和 1 分别用不同 颜色画出,就形成了一个汉字,常用的点阵矩阵有 12 * 12, 14 * 14, 16 * 16 三种字库。

    字库根据字节所表示点的不同有分为横向矩阵和纵向矩阵。

    目前多数的字库都是横向矩阵的存储方式(用得最多的应该是早期 UCDOS 字库),纵向矩阵一般是因为有某些液晶是采用纵向扫描显示法,为了提高显示速度,于是便把字库矩阵做成纵向,省得在显示时还要做矩阵转换。我们接下去所描述的都是指横向矩阵字库。

    2.2 16*16点阵字库

    对于 16*16 的矩阵来说,它所需要的位数共是 16*16=256 个位,每个字节为 8 位,因此,每个汉字都需要用 256/8=32 个字节来表示。 即每两个字节代表一行的 16 个点,共需要 16 行,显示汉字时,只需一次性读取 32 个字节,并将每两个字节为一行打印出来,即可形成一个汉字。 点阵结构如下图所示:

    3. 汉字点阵获取

    3.1 利用区位码获取汉字

    汉字点阵字库是根据区位码的顺序进行存储的,因此,我们可以根据区位来获取一个字库的点阵,它的计算公式如下:

    *点阵起始位置 = ((区码- 1)94 + (位码 – 1)) * 汉字点阵字节数

    获取点阵起始位置后,我们就可以从这个位置开始,读取出一个汉字的点阵。

    3.2 利用汉字机内码获取汉字

    前面我们己经讲过,汉字的区位码和机内码的关系如下:

    机内码高位字节 = 区码 + 20H + 80H(或区码 + A0H)

    机内码低位字节 = 位码 + 20H + 80H(或位码 + AOH)

    反过来说,我们也可以根据机内码来获得区位码:

    区码 = 机内码高位字节 – A0H

    位码 = 机内码低位字节 – AOH

    将这个公式与获取汉字点阵的公式进行合并计就可以得到汉字的点阵位置。

    二. 代码实现

    1. C++

    创建文件夹 DrawChinese 并进入此文件夹

    准备中文点阵字库,以及文本文件、图片。

    中文点阵字库及显示工具程序百度网盘

    链接:https://pan.baidu.com/s/1gCtiVbHrr2y6teo-bG8rEw
    提取码:2413

    windows上文本文件默认编码是ANSI。
    ANSI即扩展的ASCII编码,简体中文操作系统中ANSI编码代表GB2312编码。linux系统默认编码UTF-8,两种编码对中文的处理不一致,因此在linux中显示中文乱码。

    因为代码是采用ascii字库以及hz中文点阵字库,中文字库编码基本采用GB2312(扩展的ASCll编码),所以不能将ANSI改为UTF-8。

    在当前文件夹里创建文件 drawchinese.cpp,编写代码

    c++的代码不是我编的,因为c++的opencv过于复杂。
    参考我们老师给的资料。

     vim drawchinese.cpp
    
    #include<iostream>
    #include<opencv/cv.h>
    #include"opencv2/opencv.hpp"
    #include<opencv/cxcore.h>
    #include<opencv/highgui.h>
    #include<math.h>
    using namespace cv;
    using namespace std;
    
    //绘制汉字,采用中文字库
    void paint_chinese(Mat& image,int x_offset,int y_offset,unsigned long offset);
    //绘制字符,采用ASCll字库
    void paint_ascii(Mat& image,int x_offset,int y_offset,unsigned long offset);
    //在图片上添加文本
    void put_text_to_image(int x_offset,int y_offset,String image_path,char* logo_path);
    
    int main()
    {	
        string imagePath="liqin.jpg";
        char* txtPath=(char*)"logo.txt";
    	//参数分别为:文本添加的位置(x,y)、图片路径、所添加的文本路径
    	put_text_to_image(100,250,imagePath,txtPath);		
        return 0;
    }
    
    //绘制字符
    void paint_ascii(Mat& image,int x_offset,int y_offset,unsigned long offset){
    	Point p;
    	p.x = x_offset;
    	p.y = y_offset;
    	//存放ascii字膜
    	char buff[16];           
    	//打开ascii字库文件
    	FILE *ASCII;
    	if ((ASCII = fopen("Asci0816.zf", "rb")) == NULL){
            printf("Can't open ascii.zf,Please check the path!");
    		//getch();
    		exit(0);
    	}
    	fseek(ASCII, offset, SEEK_SET);
    	fread(buff, 16, 1, ASCII);
    	int i, j;
    	Point p1 = p;
    	for (i = 0; i<16; i++)                  //十六个char
    	{
    		p.x = x_offset;
    		for (j = 0; j < 8; j++)              //一个char八个bit
    		{
    			p1 = p;
    			if (buff[i] & (0x80 >> j))    /*测试当前位是否为1*/
    			{
    				/*
    					由于原本ascii字膜是8*16的,不够大,
    					所以原本的一个像素点用4个像素点替换,
    					替换后就有16*32个像素点
    					ps:感觉这样写代码多余了,但目前暂时只想到了这种方法
    				*/
    				circle(image, p1, 0, Scalar(0, 0, 255), -1);
    				p1.x++;
    				circle(image, p1, 0, Scalar(0, 0, 255), -1);
    				p1.y++;
    				circle(image, p1, 0, Scalar(0, 0, 255), -1);
    				p1.x--;
    				circle(image, p1, 0, Scalar(0, 0, 255), -1);
    			}						
    			p.x+=2;            //原来的一个像素点变为四个像素点,所以x和y都应该+2
    		}
    		p.y+=2;
    	}
    }
    //绘制汉字
    void paint_chinese(Mat& image,int x_offset,int y_offset,unsigned long offset){
        Point p;
        p.x=x_offset;
        p.y=y_offset;
        FILE *HZK;
        char buff[72];//72个字节,用来存放汉字的
       if((HZK=fopen("HZKf2424.hz","rb"))==NULL){
            printf("Can't open HZKf2424.hz,Please check the path!");
            exit(0);//退出
        }
        fseek(HZK, offset, SEEK_SET);/*将文件指针移动到偏移量的位置*/
        fread(buff, 72, 1, HZK);/*从偏移量的位置读取72个字节,每个汉字占72个字节*/
        bool mat[24][24];//定义一个新的矩阵存放转置后的文字字膜
        int i,j,k;
        for (i = 0; i<24; i++)                 /*24x24点阵汉字,一共有24行*/
    	{
    		for (j = 0; j<3; j++)                /*横向有3个字节,循环判断每个字节的*/
    			for (k = 0; k<8; k++)              /*每个字节有8位,循环判断每位是否为1*/
    				if (buff[i * 3 + j] & (0x80 >> k))    /*测试当前位是否为1*/
    				{
    					mat[j * 8 + k][i] = true;          /*为1的存入新的字膜中*/
    				}
    				else {
    					mat[j * 8 + k][i] = false;
    				}
    	}
        for (i = 0; i < 24; i++)
    	{
    		p.x = x_offset;
    		for (j = 0; j < 24; j++)
    		{		
    			if (mat[i][j])
    				circle(image, p, 1, Scalar(255, 0, 0), -1);		  //写(替换)像素点
    			p.x++;                                                //右移一个像素点
    		}
    		p.y++;                                                    //下移一个像素点
    	}
    }
    //在图片上添加文本
    void put_text_to_image(int x_offset,int y_offset,String image_path,char* logo_path)
    {
        //x和y就是第一个字在图片上的起始坐标
        //通过图片路径获取图片
        Mat image=imread(image_path);
        int text_length=17;         //文本的字节长度
        unsigned char qh,wh;      //定义区号,位号
        unsigned long offset;           //偏移量
        unsigned char hexcode[30];      //用于存放记事本读取的十六进制,记得要用无符号
        FILE* file_logo;
        if ((file_logo = fopen(logo_path, "rb")) == NULL){
    		printf("Can't open txtfile,Please check the path!");
    		//getch();
    		exit(0);
    	}
        fseek(file_logo, 0, SEEK_SET);
        fread(hexcode, length, 1, file_logo);
        int x =x_offset,y = y_offset;//x,y:在图片上绘制文字的起始坐标
        for(int m=0;m<text_length;){
            if(hexcode[m]==0x23){
                break;//读到#号时结束
            }
            else if(hexcode[m]>0xaf){
                qh=hexcode[m]-0xaf;//使用的字库里是以汉字啊开头,而不是以汉字符号开头
                wh=hexcode[m+1] - 0xa0;//计算位码
                offset=(94*(qh-1)+(wh-1))*72L;
                paint_chinese(image,x,y,offset);
                /*
                计算在汉字库中的偏移量
                对于每个汉字,使用24*24的点阵来表示的
                一行有三个字节,一共24行,所以需要72个字节来表示
                */
                m=m+2;//一个汉字的机内码占两个字节,
                x+=24;//一个汉字为24*24个像素点,由于是水平放置,所以是向右移动24个像素点
            }
            else{//当读取的字符为ASCII码时
            wh=hexcode[m];
            offset=wh*16l;//计算英文字符的偏移量
            paint_ascii(image,x,y,offset);
            m++;//英文字符在文件里表示只占一个字节,所以往后移一位就行了
            x+=16;
            }
        }
        imwrite("image_text.jpg",image);    //保存为新图片 image_text.jpg
    	imshow("image",image);          //展示图片
        waitKey(0); 
    }
    

    注意函数 void put_text_to_image() 中的
    int text_length=17; //文本的字节长度,
    改成自己的文本长度

    字节长度计算:汉字两个字节,空格以及字符一个字节
    如果按照这样计算,最后显示的文本还有多余,则你可能需要考虑文本内容是全角还是半角的问题。全角就是每个字符占两个字节。

    编译代码

    g++ drawchinese.cpp -o test `pkg-config --cflags --libs opencv`
    

    执行程序,输出结果

    ./test

    2. python

    这里还是选择在 DrawChinese 文件夹中

    准备字体,我采用simhei字体

    百度网盘链接:百度网盘 请输入提取码
    提取码:7qzy

    创建文件,编写代码

    vim drawchinese.py
    
    from PIL import Image, ImageDraw, ImageFont
    import cv2
    import numpy as np
     
    # cv2读取图片
    img = cv2.imread('liqin.jpg') # 名称不能有汉字
    cv2img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # cv2和PIL中颜色的hex码的储存顺序不同
    #将cv2格式转化为PIL格式
    pilimg = Image.fromarray(cv2img)
     
    # PIL图片上打印汉字
    draw = ImageDraw.Draw(pilimg) # 图片上打印
    # 参数1:字体文件路径,参数2:字体大小 ,参数3:编码格式
    font = ImageFont.truetype("simhei.ttf", 60, encoding="utf-8") 
    # 参数1:打印坐标,参数2:文本,参数3:字体颜色,参数4:字体
    draw.text((150, 250), "Hi,我是缔宇", (255, 0, 0), font=font) 
     
    # PIL图片转cv2 图片
    # cv2.cvtColor颜色空间转换函数,第一个参数是需要转换的图片,第二个参数是转换成何种格式,这里是将RGB换成BGR
    cv2charimg = cv2.cvtColor(np.array(pilimg), cv2.COLOR_RGB2BGR)
    
    cv2.imwrite("image_python.jpg",cv2charimg)   #保存图片
    cv2.imshow("photo", cv2charimg)   #展示图片
    cv2.waitKey (0) 
    cv2.destroyAllWindows()
    

    运行程序

    python3 drawchinese.py
    

    三. 总结

    运用opencv添加中文,在C++中虽然也有多种方法添加中文,但都很复杂,而python的方法就很简单了。

    作者:去往火星

    物联沃分享整理
    物联沃-IOTWORD物联网 » opencv在图片上添加中文汉字(c++以及python)

    发表回复