1. 程式人生 > 其它 >Ubuntu下OpenCV顯示文字資訊

Ubuntu下OpenCV顯示文字資訊

一、漢字點陣字型檔原理

1.1 什麼是點陣?

我們先分析兩個不同的點陣圖: A字母的點陣是這樣的:8×16
漢字“你”的點陣是這樣的:16×16 以上的兩個文字的字模資訊,應該讓我們很清楚的明白了文字的顯示原理。但是又是如何獲取這些字模資訊的呢? 我們知道英文字母數量比較少,我們只要用一個位元組(8位)就足以表達。但是漢字非常多。要怎麼表達呢? 前人採用的一個方法就是把ASCII碼的高128位作為漢字的內碼,低128位仍然作為英文字母的內碼,然後用兩個位元組來表示一個漢字。通過這個內碼,我們可以獲取漢字的字模資訊。然後再根據這些字模的資訊,把相應的漢字顯示出來。

1.2 什麼是漢字字型檔?如何定址?


點陣字型檔其實就是按照漢字內碼的順序,把漢字的字模資訊存起來。16×16的點陣字型檔有94區,每個區有94個漢字的字模。這樣總的有94×94個漢字。 我們之前說了,一個漢字由兩個ASCII擴充套件碼構成。第一個ASCII擴充套件碼用來存放漢字的區碼,第二個ASCII擴充套件碼用來存放漢字的位碼。具體是這樣的: 第一個擴充套件ASCII碼 = 128 + 漢字的區碼 第二個擴充套件ASCII碼 = 128 + 漢字的位碼 這樣,如果我們用char HZ[2]來表示一個漢字。則: 區碼 = HZ[0] - 128 位碼 = HZ[1] - 128 這樣,算出區位碼之後,我們就可以用它在漢字型檔裡面定址找字模了。具體的方式是:該漢字的偏移地址 = (區碼-1)×94×一個字佔用的位元組數 + 位碼×一個字佔用的位元組數

1.3 什麼是區位碼

在國標GB2312—80中規定,所有的國標漢字及符號分配在一個 94 行、94 列的方陣中,方陣的每一行稱為一個“區”,編號為 01 區到 94 區,每一列稱為一個“位”,編號為01 位到 94 位,方陣中的每一個漢字和符號所在的區號和位號組合在一起形成的四個阿拉伯數字就是它們的“區位碼”。區位碼的前兩位是它的區號,後兩位是它的位號。用區位碼就可以唯一地確定一個漢字或符號,反過來說,任何一個漢字或符號也都對應著一個唯一的區位碼。漢字“母”字的區位碼是 3624,表明它在方陣的 36 區 24 位,問號“?”的區位碼為0331,則它在 03 區 31 位。

1.4 什麼是機內碼

漢字的機內碼是指在計算機中表示一個漢字的編碼。機內碼與區位碼稍有區別。如上所述,漢字區位碼的區碼和位碼的取值均在 1~94 之間,如直接用區位碼作為機內碼,就會與基本 ASCII 碼混淆。為了避免機內碼與基本 ASCII 碼的衝突,需要避開基本 ASCII 碼中的控制碼(00H~1FH),還需與基本 ASCII 碼中的字元相區別。為了實現這兩點,可以先在區碼和位碼分別加上 20H,在此基礎上再加 80H(此處“H”表示前兩位數字為十六進位制數)。經過這些處理,用機內碼錶示一個漢字需要佔兩個位元組,分別稱為高位位元組和低位位元組,這兩位位元組的機內碼按如下規則表示:高位位元組 = 區 碼 + 2 0 H + 8 0 H ( 或 區 碼 + A 0 H ) 低 位 字 節 = 位 碼 + 2 0 H + 8 0 H ( 或 位 碼 + A 0 H ) 高位位元組 = 區碼 + 2 \ 0H + 8 \ 0H(或區碼 + A \ 0H) \\ 低位位元組 = 位碼 + 2 \ 0H + 8 \ 0H(或位碼 + A \ 0H)

由於漢字的區碼與位碼的取值範圍的十六進位制數均為01H~5EH(即十進位制的 01~94),所以漢字的高位位元組與低位位元組的取值範圍則為 A1H~FEH(即十進位制的 61~254)。 例如,漢字“啊”的區位碼為 1601,區碼和位碼分別用十六進位制表示即為 1001H,它的機內碼的高位位元組為 B0H,低位位元組為 A1H,機內碼就是 B0A1H。
正如我們在1.3中提到的

/*
        區碼 = HZ[0] - 128
        位碼 = HZ[1] - 128
*/

二、點陣字型檔的顯示原理

2.1顯示的規律

所有的漢字或者英文都是下面的原理,

由左至右,每8個點佔用一個位元組,最後不足8個位元組的佔用一個位元組,而且從最高位向最低位排列。生成的字型檔說明:(以12×12例子)一個漢字佔用位元組數:12÷8=1····4也就是佔用了2×12=24個位元組。編碼排序A0A0→A0FE A1A0→A2FE依次排列。以12×12字型檔的“我”為例:“我”的編碼為CED2,所以在漢字排在CEH-AOH=2EH區的D2H-A0H=32H個。所以在12×12字型檔的起始位置就是[{FE-A0}*2EH+32H]*24=104976開始的24個位元組就是我的點陣模。其他的類推即可。英文點陣也是如此推理。

2.2字型檔的結構

1、點陣字型檔儲存

  在漢字的點陣字型檔中,每個位元組的每個位都代表一個漢字的一個點,每個漢字都是由一個矩形的點陣組成,0代表沒有,1代表有點,將0和1分別用不同顏色畫出,就形成了一個漢字,常用的點陣矩陣有12*12, 14*14, 16*16三種字型檔。

  字型檔根據位元組所表示點的不同有分為橫向矩陣和縱向矩陣,目前多數的字型檔都是橫向矩陣的儲存方式(用得最多的應該是早期UCDOS字型檔),縱向矩陣一般是因為有某些液晶是採用縱向掃描顯示法,為了提高顯示速度,於是便把字型檔矩陣做成縱向,省得在顯示時還要做矩陣轉換。我們接下去所描述的都是指橫向矩陣字型檔。

2、16*16點陣字型檔

  對於16*16的矩陣來說,它所需要的位數共是16*16=256個位,每個位元組為8位,因此,每個漢字都需要用256/8=32個位元組來表示。

  即每兩個位元組代表一行的16個點,共需要16行,顯示漢字時,只需一次性讀取32個位元組,並將每兩個位元組為一行打印出來,即可形成一個漢字。

3、14*14與12*12點陣字型檔

  對於14*14和12*12的字型檔,理論上計算,它們所需要的點陣分別為(14*14/8)=25, (12*12/8)=18個位元組,但是,如果按這種方式來儲存,那麼取點陣和顯示時,由於它們每一行都不是8的整位數,因此,就會涉到點陣的計算處理問題,會增加程式的複雜度,降低程式的效率。

  為了解決這個問題,有些點陣字型檔會將14*14和12*12的字型檔按16*14和16*12來儲存,即,每行還是按兩個位元組來儲存,但是14*14的字型檔,每兩個位元組的最後兩位是沒有使用,12*12的位元組,每兩位元組的最後4位是沒有使用,這個根據不同的字型檔會有不同的處理方式,所以在使用字型檔時要注意這個問題,特別是14*14的字型檔。

三、漢字點陣的獲取

1、利用區位碼獲取漢字

  •   漢字點陣字型檔是根據區位碼的順序進行儲存的,因此,我們可以根據區位來獲取一個字型檔的點陣,它的計算公式如下:
  •   點陣起始位置 = ((區碼- 1)*94 + (位碼 – 1)) * 漢字點陣位元組數
  •   獲取點陣起始位置後,我們就可以從這個位置開始,讀取出一個漢字的點陣。

2、利用漢字機內碼獲取漢字

  •   前面我們己經講過,漢字的區位碼和機內碼的關係如下:
  •   機內碼高位位元組 = 區碼 + 20H + 80H(或區碼 + A0H)
  •   機內碼低位位元組 = 位碼 + 20H + 80H(或位碼 + AOH)
  •   反過來說,我們也可以根據機內碼來獲得區位碼:
  •   區碼 = 機內碼高位位元組 - A0H
  •   位碼 = 機內碼低位位元組 - AOH
  •   將這個公式與獲取漢字點陣的公式進行合併計就可以得到漢字的點陣位置。

四、點陣字型檔和向量字型檔的差別

我們都只知道,各種字元在電腦螢幕上都是以一些點來表示的,因此也叫點陣。最早的字型檔就是直接把這些點儲存起來,就是點陣字型檔。常見的漢字點陣字型檔有 16x16, 24x24 等。點陣字型檔也有很多種,主要區別在於其中儲存編碼的方式不同。點陣字型檔的最大缺點就是它是固定解析度的,也就是每種字型檔都有固定的大小尺寸,在原始尺寸下使用,效果很好,但如果將其放大或縮小使用,效果就很糟糕了,就會出現我們通常說的鋸齒現象。因為需要的字型大小組合有無數種,我們也不可能為每種大小都定義一個點陣字型檔。於是就出現了向量字型檔。

  向量字型檔

  向量字型檔是把每個字元的筆劃分解成各種直線和曲線,然後記下這些直線和曲線的引數,在顯示的時候,再根據具體的尺寸大小,畫出這些線條,就還原了原來的字元。它的好處就是可以隨意放大縮小而不失真。而且所需儲存量和字元大小無關。向量字型檔有很多種,區別在於他們採用的不同數學模型來描述組成字元的線條。常見的向量字型檔有 Type1字型檔和Truetype字型檔。

  點陣字型檔

  在點陣字型檔中,每個字元由一個位圖表示,並把它用一個稱為字元掩膜的矩陣來表示,其中的每個元素都是一位二進位制數,如果該位為1表示字元的筆畫經過此位,該畫素置為字元顏色;如果該位為0,表示字元的筆畫不經過此位,該畫素置為背景顏色。點陣字元的顯示分為兩步:首先從字型檔中將它的點陣圖檢索出來,然後將檢索到的點陣圖寫到幀緩衝器中。

  實際應用

  在實際應用中,同一個字元有多種字型(如宋體、楷體等),每種字型又有多種大小型號,因此字型檔的儲存空間十分龐大。為了減少儲存空間,一般採用壓縮技術。

  向量字元記錄字元的筆畫資訊而不是整個點陣圖,具有儲存空間小,美觀、變換方便等優點。例如:在AutoCAD中使用圖形實體-形(Shape)-來定義向量字元,其中,採用了直線和圓弧作為基本的筆畫來對向量字元進行描述。 對於字元的旋轉、放大、縮小等幾何變換,點陣字元需要對其點陣圖中的每個象素進行變換,而向量字元則只需要對其幾何圖素進行變換就可以了,例如:對直線筆畫的兩個端點進行變換,對圓弧的起點、終點、半徑和圓心進行變換等等。

  向量字元的顯示也分為兩步。首先從字型檔中將它的字元資訊。然後取出端點座標,對其進行適當的幾何變換,再根據各端點的標誌顯示出字元。

  輪廓字形法是當今國際上最流行的一種字元表示方法,其壓縮比大,且能保證字元質量。輪廓字形法採用直線、B樣條/Bezier曲線的集合來描述一個字元的輪廓線。輪廓線構成一個或若干個封閉的平面區域。輪廓線定義加上一些指示橫寬、豎寬、基點、基線等等控制資訊就構成了字元的壓縮資料。
  

五、實際的呼叫操作

已經提前建立好了一個名為cv9.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);
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 image_path="tearsgirl.png";
    char* logo_path="logo1.txt";
    put_text_to_image(20,300,image_path,logo_path);
    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 length=18;//要列印的字元長度
    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<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;
        }

    }

    cv::imshow("image", image);
    cv::waitKey();
}

完成程式碼後,我們需要完成一些額外的工作!!

在編譯檔案同子目錄下,放入一些支援的漢字顯示庫,用於讓我們的C++呼叫,可以顯示中文名等資訊。這點很重要,不然程式碼會一直報錯!

結果展示如下: 我們在lena圖上加入了自己的姓名等資訊

關於程式碼的一些解析:

int main(){
    String image_path="Picture_Name.Format ";
    char* logo_path="filename.txt";
    put_text_to_image(20,300,image_path,logo_path);//圖片內容資訊
    return 0;
}

可以將上述程式碼和前面的程式碼對照看 可以對應著進行修改

六、心得體會

Opencv的呼叫和修改的程式碼過程還是十分的複雜,尤其是漢字這樣資訊熵十分大而且顯示起來並不簡單的語言,使用的時候更加的困難。在改程式碼以及呼叫的過程中遇到了許多問題,但是都得以解決,且寫在了文章裡,希望通過接下來的學習能夠更加的瞭解和理解Ubuntu和Opencv,一段時間沒有使用讓我對Ubuntu和CV都有了些許的陌生,這次是一次很好的複習。

七、參考連結

https://blog.csdn.net/weixin_47554309/article/details/121165507

https://blog.csdn.net/junseven164/article/details/121130735?spm=1001.2014.3001.5501