2017年6月21日 星期三

C / C++ Bitmap(BMP) 圖檔讀寫範例 與 檔頭詳細解析

C / C++ Bitmap(BMP) 圖檔讀寫範例 與 檔頭詳細解析

C/C++ 純軟體讀取修改 bitmap 檔案完整介紹如下,文末附上詳細的原始碼可以參考。
例外也有如何讀取灰階圖gray與彩圖RGB的問題、以及解析度不為4倍數的時候如何處理4byte位元對齊,並且寫成一個萬用的函式,代碼開源(source code)可在文末下載,可直接直接編譯並執行程式碼不需外掛函式庫。

介紹

BMP圖檔大致分為四大部分下面會依序解說
  • 檔案檔頭(14byte)
  • 圖片檔頭(40byte)
  • 調色盤(1024byte)
  • Raw檔
這裡的byte看成是一個字元(char),如果是字元與Raw檔讀取就順著讀而已,比較特別是數字讀取的時候要一起讀;比如說根據表頭某4byte是一組的他們是 00 FF 01 0A 那你要反著讀變成 0A 01 FF 00 而不是直接讀取。(注意看細微變化 01 不會變成 10)
有興趣的可以查查這個詞 Endianness:https://goo.gl/xu9uYB
建議除錯的使用二進位軟體查看,比如說 HxD 來看(阿榮載點)

檔案檔頭(14byte)

範例代碼:(占用位元可以從代碼上看到)
struct BmpFileHeader{
    uint16_t bfTybe = 0x424D;
    uint32_t bfSize;
    uint16_t bfReserved1 = 0;
    uint16_t bfReserved2 = 0;
    uint32_t bfOffBits = 54;
};
其中有寫數值等於多少的地方是 C++的類內初始化。C要刪除等號與數值才能編譯。
第一個 bfTybe 比較特別的是他是BM,你也可以寫成 unsigned char type[2]= {'B', 'M'};,他就是固定的識別代碼,標準規定。
再來 bfSize 是指檔案大小,就是包含上述全部資料大小,這也會是你直接對BMP右鍵內容看到的容量大小。
再來 bfReserved 兩項直接填0就好了,不影響一般操作,詳細規格可以參考維基百科。
再來最後面 bfOffBits 的54是固定的就是14+40,不過如果你是用灰階圖記得要加上1024(調色盤占用的)。
類內初始化:C++11標準才可以使用
使用 g++ 加上 -std=c++11 參數即可,如 g++ -std=c++11 source.cpp
Visual Studio 2017 以上就直接是 C++11 了,不需要設置。

圖片檔頭(40byte)

範例代碼:(占用位元可以從代碼上看到)
struct BmpInfoHeader{
    uint32_t biSize = 40;
    uint32_t biWidth;
    uint32_t biHeight;
    uint16_t biPlanes = 1; // 1=defeaul, 0=custom
    uint16_t biBitCount;
    uint32_t biCompression = 0;
    uint32_t biSizeImage = 0;
    uint32_t biXPelsPerMeter = 0; // 72dpi=2835, 96dpi=3780
    uint32_t biYPelsPerMeter = 0; // 120dpi=4724, 300dpi=11811
    uint32_t biClrUsed = 0;
    uint32_t biClrImportant = 0;
};
  • biSize 檔頭的大小
  • biWidth 圖片的寬
  • biHeight 圖片的長
  • biBitCount 位元數,彩圖要設RGB 8*3=24,灰階圖設8
  • biSizeImage 指的是總共有幾個像素,如果是彩圖有RGB,除了長x寬還要再 *3
    實測有些軟體存出來的bmp的biSizeImage會是0,建議用長x寬自己算像素值。
  • biClrUsed 指的是調色盤的顏色數,彩圖設 0 即可,灰階圖要自訂調色盤要設 256
  • PelsPerMeter指的是密度,一般軟體好像也不看都是設0即可,windwos電腦預設是96dpi,大概就差別在你把這張圖拉上PPT上設越高會縮的越小。

調色盤(1024byte)

這個是可選的不一定會有,圖是RGB彩色不用留空(如00),直接就是沒有才是正確的。
如過圖片是灰階圖你就要自己補上調色盤,調色盤意思其實也很簡單就是 00 00 00 00~ FF FF FF 00 而已,這是因為灰階圖只需用一個byte表示,但是整張BMP需要用 RGB 表示,所以調色盤的功用就是把看到的RAW檔,就比如看到 00 變成 00 00 00 看到 FF 變成 FF FF FF
總的來說意思是自訂規則從 “單一通道” 解析出 “RGB三通道”。
你可以一次做好整份調色盤在寫入,也可以用for迴圈寫入,數量不多不會太損效能可以用for比較整潔些,範例如下寫法。
for(unsigned i = 0; i < 256; ++i)
    img << uch(i) << uch(i) << uch(i) << uch(0);

Raw檔

比較特別的是讀取的方式是從圖的左下角開始往右邊上面讀取,而且數據是BGR反過來的,並且他們的行會對齊4byte,如果圖片是40x42那麼你寫入的時候要寫成40x44不足的2位元補空白0。
bmp 4bit 對齊的公式代碼如下
size_t RealWidth = Width * bits/8;
size_t alig = (RealWidth*3)%4;
算出來是2的話就是說每行的結尾要塞 2 個 char(0) 就是 0x0000
算出來是4的話就是說每行的結尾要塞 3 個 char(0) 就是 0x000000
值得注意的是公式要針對不同的位元的圖片做不同的處理
可以想像成假設同一張3x3的圖片那麼,對黑白圖來說就是每行結尾要補1個空白,對於彩色的圖來說每個點個別有RGB,也就是實際寬度總共是9個點,9個點的話4位元對齊就需要補3個空白。
總結一下萬用公式就是 圖的實際位元寬度*3 % 4 這樣。

注意

  1. 灰階圖要多寫調色盤,有三個地方要更動
    FileHeader 的 size        +1024
    FileHeader 的 headSize    +1024
    InfoHeader 的 size        +1024
    InfoHeader 的 ncolours    =256
    
  2. 讀RAW檔的時候(也就是像素值),檔案指標要跳過調色盤(建議是直接抓 headSize 比較省事)。
  3. 讀寫RAW檔區域的時候,記得跳過4byte對齊,對齊算法是
    size_t alig = ((width*bits/8)*3) % 4;
    

範例 SourceCode

C++的版本多了不少功能,後來維護到一半荒廢了QQ 留下不少測試代碼~
還有許多未完成的功能~覺得好用的話可以在這或gihub上留言~
建議其他功能或是bug回報也好,有人留言可能哪天就有動力大改版了XDD

2017年6月15日 星期四

什麼時候必須要用到指標

什麼時候必須要用到指標

很多時候會遇到必須使用指針的情況,舉例來說以下兩個降低依存關係的技巧,才不用動一個一個地方整份專案重編。
  • Handle Class
  • Protocol Class
實際上應該還有很多場合不得不用,或使用會更好。
舉個簡單的例子,考慮一個互相依存的類別,雙方彼此都要有雙方
struct A{
    B b;
};
struct B{
    A a;
};
這時候你怎麼辦呢在類別A內找不到B的定義沒但法實作
舉兩個簡單手段避開依存關係
  • vector (或一個設計良好自訂的容器)
  • 指針
struct B; // 前置宣告
struct A{
    B* b;
};
struct B{
    A a;
};
struct B; // 前置宣告
struct A{
    vector<B> b;
};
struct B{
    A a;
};
其中的前置宣告,也可以使用詳細的型別說明符(Elaborated type specifier)取代
struct A{
    struct B* b;
};

2017年6月8日 星期四

為什麼 cin.get() getline() 沒辦法讀取第二個

為什麼 cin.get() getline() 沒辦法讀取第二個

主要是偵測到結束符號導致被略過,最簡單的方式直接使用
string str;
cin >> str;
就可以連續讀取了
如果真的要用標題的方式則要讓串流做完這一次、調整指針、清除才可以
提供以下幾個方法,是用在不同的情況,需要使用再去詳細爬文
cin.ignore();
cin.sync();
cin.seekg(1,ios::end);
fflush(stdin);

2017年6月5日 星期一

指針(point) 與 參考(ref) 有什麼不同之處

指針(point) 與 參考(ref) 有什麼不同之處

tags: C++ Concept2
本文所指的指針的指針指的是指標,個人覺得這樣比較好做區別
初學時時常讓人搞不清楚的兩個不同的東西,這邊提供幾個大原則做判斷
指針:可以儲存記憶體位置的型別
參考:就好像別名一樣可以完全等同於所指之物
比較特別的還有一種情況叫做(*\&)指針的參考,這又是什麼呢?

兩個符號的用法

分別是
  • *取址 reference
  • &取值 dereference
他們放在非宣告時的變數的前方,從英文可以看出他們具有相反的關係。
首先最讓人搞不清楚的是為什麼他們在宣告時有不同的關係,那根本就是不一樣意思了。
int* i;
int& a;
你把它當作別的東西來看直接把整串看做是一個型別。
我的意思是你不要這樣看int* i; 而直接是 int* i;
前者表示宣告一個指針容器,後者表示宣告一個別名,跟剛剛的*&完全是兩回事。
由此我也更推薦你使用 int* i; 而不是 int *i;這樣更容易表達。
除非你需要連續宣告,這就真的不得已了int *i, *j; 連續宣告時要個別指定。
回到剛剛的取址與取值簡單來說就是你現在有一個變數,它存在記憶體的某個地方,這個某個地方會用一串數值表示位置比如說 0x01
現在你手上拿到 p=0x01 你希望獲取他的數值就是使用取值 *p 獲取內容
你現在手上拿到 p=0; 你希望獲取他的地址就是使用取址 &p 獲取地址

地址的傳遞可以共同編輯

函式(包含main)內都有各自的空間,彼此是不同互相存取的除非你使用全域函式。
他就像一個公用櫃子一樣,只要你告訴別人我的東西放在幾號櫃子別人就可以來存取。
比如以下的代碼
int i=0;
int* p=&i;
現在p也可以修改i
*p = 1;
把它放到函式內就像這樣
void fun(int* p){
    *p = 1;
}
使用時就像這樣
fun(&i);

參考基本用法

底層裡面都是做指針的存取,使用方法怎麼區別可以這樣想像
參考就像一個不用輸入符號的指針
什麼意思呢,假設有一個指針為下
int i=0;
int* p=&i;
現在p也可以修改i
如果寫成參考就像這樣子
int i=0;
int& ali=i;
現在ali也可以修改i,現在操作ali就像在操作i
叫王小明的綽號 阿明 就像在叫王小明一樣。
放到函式可以省很多事情
void fun(int& p){
    p = 1;
}
使用時就像這樣
int i=0;
fun(i);
現在你在函式內的操作以及呼叫函式時的操作,全部都不用加上符號上了,是不是更好用呢?
或許你會在學到參考後一股腦地全部換參考,但其實他們之間大概有細微的差異

指針

  • 需要檢查有效性
  • 可以指向Null表示無效,也可以隨時更換指向的目標
  • 有指針的指針

參考

  • 不需檢查有效永遠有效
  • 不能換參考,且只能在宣告時給定目標
  • 沒有參考的參考
彙整出來的兩條守則:
  1. 不想付出檢查成本就用參考
  2. 需要更動指向的目標物或初始化為無效,只能用指針

活用參考與指針

很多時候他們是可以互相通用的,這可能不好記憶,透過練習題更容易熟悉,詳見以下的範例
有一個函式
  • 可以在函式內改他的值
  • 並使回傳的可以更改內容
看起來有些繞口,直接看代碼會比較好理解
要可以做到以下的樣板功能
int i;
fun(/*i*/);       //更改 i 的值
fun(/*i*/) += 10; //並加10
大致可以有以下四種做法,各字看一次,做一次應該10分鐘就可以入門參考與指針了
#include <iostream>
using namespace std;

int* fun1(int* i){
    *i = 1;
    return i;
}
int* fun2(int& i){
    i = 2;
    return &i;
}
int& fun3(int* i){
    *i = 3;
    return *i;
}
int& fun4(int& i){
    i = 4;
    return i;
}

int main() {
    int i=0;
    *fun1(&i) += 10;
    cout << "i=" << i << endl;
    *fun2(i) += 10;
    cout << "i=" << i << endl;
    fun3(&i) += 10;
    cout << "i=" << i << endl;
    fun4(i) += 10;
    cout << "i=" << i << endl;
    return 0;
}

2017年6月4日 星期日

批次檔 bat 如何輸出當前目錄檔案名稱到文件.txt 內

CMD 讀取指定

檔案名稱與打印

使用 dir
dir /b /on >list.txt
再來是讀取檔案使用for迴圈一行一行讀出來
@echo off
for /f "tokens=*" %%i in (list.txt) do ( 
    echo %%i 
)
其中 “tokens=*” 是關鍵如果讀取的文字檔中有空白,不加會讀不到
詳細可以參考:Re: [問題] bat檔判斷ini內容

日文處理

如果有日文會亂碼,加一些指令使用UTF-8存檔
cmd /u /c dir /b /on >list_UTF8.txt
這樣輸出是正常的,不過日文字體用內建的記事本開啟可能會有問題,需要其他支援的軟件來開啟。(如 SublimeText);
最悲劇的是…使用UTF-8存檔會導致bat無法讀取,貌似沒救了找不到什麼解。



powershell

後來又花了幾小時改用 powershell 這個真的有點神
附檔名是 ps1 預設打開會是記事本,在CMD輸入指令改由直接啟動
ftype Microsoft.Powershellscript.1="%SystemRoot%\system32\windowspowershell\v1.0\powershell.exe" "%1"
新版的win10初始設定會限制是不能執行腳本的,需要打開權限在 powshell 輸入
Set-ExecutionPolicy RemoteSigned
回答是就可以正常執行了

先創一個讀檔的

Function Read_File($ex_name){
    $file=Get-ChildItem -Recurse -ErrorAction SilentlyContinue -Filter *$ex_name | 
    Where-Object { $_.Extension -eq $ex_name }
    return $file
}
然後直接使用
$file=Read_File '.jpg'
印出就直接打就好了
$file
印出來會一團東西加個
$file.Name           # 保留檔名
$file.BaseName   # 去除附檔名
他會特麼很神的直接變成陣列
$file.Name          # 印出全部
$file.Name [0]     # 印出第一個
$file.Name [-1]   # 印出最後一個
怎麼這麼神奇,果斷放棄bat QuQ
接下來for迴圈可以上場了,一個一個處理
# 複製至資料夾內
ForEach($item in $file){
    mkdir $item.BaseName -ErrorAction SilentlyContinue
    Copy-Item $item.Name $item.BaseName
    # Remove-Item $item.Name
}
$item就是當前檔案名稱了,他會挨個輪過一次,這裡我是要把它放入資夾內。
比如說有個檔案是 123.jpg 我要放到 123/123.jpg 內。


其他參數

回頭來說一下剛剛一團函式
Get-ChildItem
印出當前目錄檔案
Recurse
這個貌似是讀子目路
-ErrorAction
略過警告比如說移動檔案時問你要不要覆蓋,SilentlyContinue是他的附帶參數(安靜的繼續)
-Filter *$ex_name
第一階段過濾出擴展名(副檔名),不過能力有限,假設過濾圖檔.jpg.jpg這樣會一起進來
Where-Object { $_.Extension -eq $ex_name }
補足上一個問題 $_ 指的是當前的緩存物件,這裡有指名(給名字)是$file


輸出到文本

兩行就可以解決了,如果要寫成一行中間加個分號隔開即可,後面選定Unicode才可以支持中文、日文、韓文等等會出狀況的文字。
$file=Get-ChildItem
set-content $file -path list.txt -Encoding Unicode
如果是要在CMD環境下使用可以使用(不過前提是系統支援)
powershell $file=Get-ChildItem; set-content $file -path list.txt -Encoding Unicode
如果想換個位置的話,比如說我目前在使用者目錄,但工作目錄不想cd切過去桌面(這裡輸入的目錄也可以改成絕位置)
$file=Get-ChildItem -path Desktop
set-content $file -path Desktop/list.txt -Encoding Unicode


查找函式

要查找函式可以透過這樣的方式知道有哪些方法可以用
Get-ChildItem $FilenNme | format-list *
另外獲取目錄後來發現一個比較簡單的用法
$file=Get-ChildItem -Recurse -include "*.jpg"