分類: OpenCV

  • 完美關掉 Python OpenCV 圖片視窗的方法

     

    相信各位做影像的同行在驗證自己演算法的時候,
    總是像我一樣眼見為憑、需要把圖片秀出來對吧?

    在很多的 OpenCV 教學文裡面都教我們用這行程式碼關掉視窗:

    cv2.imshow('Image', img)
    cv2.waitKey(0)

    但是用這行程式碼的問題是如果你按了視窗右上角的 “X” 來關掉視窗,
    那麼你的程式就會卡住,因為 OpenCV 不知道視窗被關掉了,
    所以視窗的程序就繼續執行跟你演。

    我每次遇到這狀況就快要中風,
    為了避免各位同行也中風我在此提供解決方法。

    cv2.imshow('Image', img)
    while True:
        if (cv2.getWindowProperty('Image', cv2.WND_PROP_VISIBLE) <= 0 or cv2.waitKey(1) > 0):
            cv2.destroyWindow('Image')
            break
    

    原理是去檢查名稱叫做 Image 的視窗狀態,
    如果他被關掉了,那就把視窗的程序結束掉讓程式就繼續進行。

    至於為什麼要放 waitKey(1) 而不是 waitKey(0),
    那是因為 waitKey(0) 放在條件式裡面會像王寶釧苦守寒窯十八年,
    等你在視窗按下任意鍵。

    如果你又按 “X” 把視窗結束掉了,那就真的老死不相往來了。

    那你說 CPP 裡面怎麼辦呢?有 CPP 的版本嗎?

    我也覺得很奇怪,Python 版的 OpenCV 理論上是 bind CPP版 的 OpenCV ,
    兩邊實現應該會一樣?

    但是 CPP 中 waitKey(0) 可以偵測視窗關掉(也就是按右上角”X”也能關掉視窗程序)。

    以上,謝謝指教。

  • 想知道網戀對象有沒有修圖嗎?試試看這款修圖偵測機器人!

    想知道網戀對象有沒有修圖嗎?試試看這款修圖偵測機器人!

     

    前陣子在咱們一群影像愛好者的群組開始流傳一套程式,
    一套號稱能檢測愛情動作片封面詐欺的程式!

    什麼?天底下有這等好事?

    於是我找到了開發這套程式的仁兄要到了原始論文,
    認為他實現得不夠完美,間接促使我完成這項服務。

    這篇文章可以幫你得出一個修圖參考值
    (但某些情況不適用,文後會補充說明。)

    接著會介紹論文以及背後數學原理,
    對於檢測服務比較有興趣可以直接跳到後面。

    原理

    本文是 Analyzing Benford’s Law’s Powerful Applications in Image Forensics 這篇論文的延伸應用:

    要講解這篇論文就要先解釋什麼是 Benford’s Law?

    Benford’s Law 的概念就是人類世界中隨機數其實並不隨機,
    其中數據的首位數字是遵循某種規律,這個規律就是 Benford’s Law。

    Benford’s Law 公式:

    $F_a = log_{10}{(frac{a+1}{a})}$, for all a = 1,2,…,9

    這樣算起來會呈現由首位數字出現比率是 1 往 9 遞減的一個分佈:

    舉個 Benford’s Law 的例子,

    如果你跑去韓總的宇宙造勢場合抓一萬人來訪問,

    問他們每個人存款有多少?

    如果他們沒有說謊的話,這一萬人的存款首位數字應該會符合 Benford’s Law。

    這篇論文主要結論是圖片經過 離散餘弦轉換(Discrete Cosine Transform)以下簡稱 DCT 後,
    轉換後的圖片會服從 Benford’s Law。

    而論文本意是拿這個結論做二次壓縮來估測 JPEG 的壓縮率,
    有興趣的大家可以自己閱讀一下論文

    實作

    我一開始是用土法煉鋼套 二維 DCT 公式:
    $D(i, j) = frac{2}{N} C(i)C(j) sum_{x=0}^{N-1} sum_{y=0}^{N-1} f(x, y) cosleft[frac{(2x+1)ipi}{2N}right] cosleft[frac{(2y+1)jpi}{2N}right]$

    $C(u) =
    begin{cases}
    frac{1}{sqrt{2}}, & text{if } i = 0 \
    1, & text{otherwise}
    end{cases}$

    $C(v) =
    begin{cases}
    frac{1}{sqrt{2}}, & text{if } j = 0 \
    1, & text{otherwise}
    end{cases}$

    慢得要死,時間大約是 OpenCV 內建二維 DCT 轉換的 4.5 倍。

    身為一個影像從業者,
    一定要做到比內建函式庫快!

    加速

    俗話說得好: 

    要看一個人會不會做立委,就要看他怎麼做立委。

    我是說要加速就要從數學看起,所以讓我們來看一下公式:

     
    其中 $D(i, j)$ 是轉換後的值,$i, j$ 是位置參數;
    $f(x, y)$ 是原始圖片的亮度值,$x, y$ 也是位置參數。

    發現亮度值可以提出來,其中位置參數可以自己組一個矩陣相乘,
    我們暫且把包含所有 $i, j$ 組合的稱為母矩陣。
    這個母矩陣可以重複用,就不用每個區塊都要算。
    (原始圖片會被 N*N 的小區塊分割,N 通常是 8。)

    因為一組 $i, j$ 負責一組子矩陣,
    所以如果輸出入尺寸相同,母矩陣大小為: $N * N * N * N$

    於是我用這方法寫了一個 Mask 法,
    的確速度與 OpenCV 內置函數比肩了,但是還沒有超越(#。

    再加速

    於是我又對平行運算生起了一絲邪念,
    如果我宣告一組共享記憶體紀錄首位數字,
    並對計算每個區塊的計算採取平行運算呢?

    起初我是用 Lock  方法,後來發現這樣設計寫入的時候會有 Race Condition 問題,
    後來便採取 Lock free 實作,缺點是佔用的記憶體較多。

    效率展示


    圖中數據為計算一張 1280*1280 的彩色 jpg 圖片:
    oldSingleTransform 為單執行緒的土法煉鋼法(公式法),
    SingleTransform 及 MultiTransform 則分別為 Mask 的 單執行緒、多執行緒方法。

    數據判讀


    本人拿自己的相片去做測試,發現若是原圖進去做計算,
    則出來的估計值大於 1 的話很有可能是修圖。
    若追求計算效率做 DownSample 到 512*512 的話,
    估計值約莫大於 0.4 就有可能是修圖。

    目前線上提供服務的機器人都是使用 DownSample 方法做計算。

    特別需要說明的是手機原生相機就會做白平衡、明暗部校正,
    尤其是 iPhone 做得非常優秀。

    (逆光下拍照還能看清人臉,這是一種明暗部校正的技術。)
    經過白平衡及明暗部校正後的圖片在本方法看來也是一種修圖,
    需要特別注意。
    惟整個圖片加權同一個值不是,
    所以如果整張調亮/暗的偵測不出來。

    開源


    本方法開源在 Github 
    https://github.com/wuyiulin/GraphAppBot

    也同時提供線上機器人服務,只要傳圖片就能得到修圖估計值:
    點我去 Telegram 機器人

    如果有錯誤歡迎聯絡我:wuyiulin@gmail.com

    Murmur


    想修改源碼的話,裡面有個 Ampfactor 是因為我使用 float32 存 Mask 值(DCT係數),
    係數有正負,為了避免計算途中正負相減過小(float32 小數點後有效七位),
    而把值歸零所以加入的。

    原始論文使用 Uncompressed Colour Image Database 資料庫,
    探討使用原始無損圖片如何估計 JPEG 壓縮率,
    所以本文的物理意義是估算原始無損圖片與輸入圖片的差異度。

    本文估計值僅供參考,
    窮盡科技之力後不如鼓起勇氣約網戀對象出來走走!

    謝謝大家

  • 解決 OpenCV 編譯後不定時崩潰、失效等問題

    解決 OpenCV 編譯後不定時崩潰、失效等問題

     

    這邊提供 OpenCV 編譯後崩潰的可能解決方法:

    我的環境是 OpenCV 4.5.4、Ubuntu 22.04,

    並使用 g++ 11.4.0 編譯我的專案。

    我遇到的情境問題是,我得到一包 C/C++ 的專案,

    裡面用 Makefile 來整合編譯專案,裡面包含我自己寫的一段高斯濾波程式碼。

    這貨整包編譯時沒有出錯;

    獨立把高斯濾波程式碼放到另一個編譯、運行也都沒有出錯。

    但當我在源碼裡面運行這段高斯濾波程式碼時”有機率”會出錯:

        int ksize = 3;
        cv::Size size = image.size();
        int width = size.width;
        int height = size.height;
        cv::Mat blurred_image(height, width, CV_8UC1, cv::Scalar::all(0));
        cv::GaussianBlur(grey, blurred_image, cv::Size(ksize, ksize), 0, 0);
    

    然而我給定的高斯核大小為 3×3,

    非常奇怪,程式會跳說高斯核定義不是奇數,所以不合法:

    error: (-215:Assertion failed) ksize.width > 0 && ksize.width % 2 == 1 && 
    ksize.height > 0 && ksize.height % 2 == 1 in function 'createGaussianKernels'
    

    除此之外,當我想使用旋轉圖片、在圖片上畫點的功能也全部失效,但是編譯又沒有出錯,

    這件事情真的是非常奇怪。

    因為是運行時錯誤,於是我先用 GDB 檢查了函式是否重複定義:

    然而並沒有,這就奇了怪了。

    後來我開始埋 log 想辦法抓鬼,也完全抓不到。

    解決方法:

    把專案的 Makefile 打開,有關於 OpenCV 的部份改寫,

    讓 Makefile 繞過 pkg-config ,手動給定 OpenCV.hpp 還有 libopencv_XXX.so 的路徑。


    如果我的 OpenCV.hpp 放在 /usr/include/opencv4/opencv2 底下,

    那就把 CFLAGS+= 裡面加入 -I/usr/include/opencv4;

    libopencv_XXX.so 放在 /usr/lib/x86_64-linux-gnu 下面,

    那就把 LDFLAGS += 裡面加入 -L/usr/lib/x86_64-linux-gnu。


    原版:

    ifeq ($(OPENCV), 1) 
    COMMON+= -DOPENCV
    CFLAGS+= -DOPENCV
    LDFLAGS+= `pkg-config --libs opencv4`
    COMMON+= `pkg-config --cflags opencv4`
    endif
    



    改寫成:

    ifeq ($(OPENCV), 1) 
    COMMON+= -DOPENCV
    CFLAGS+= -DOPENCV -I/usr/include/opencv4
    LDFLAGS+= `pkg-config --libs opencv4` 
    LDFLAGS+= -L/usr/lib/x86_64-linux-gnu -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_imgcodecs
    COMMON+= `pkg-config --cflags opencv4`
    endif
    


    完美解決

    雖然我另外一份獨立的 CPP 檔案能夠用 pkg 找到 OpenCV 也能編譯,

    pkg PATH 裡面也有 OpenCV,暫時不知道哪裡耦合到了,

    先留紀錄改天遇到再來深究。

    若有錯誤請聯絡我 – wuyiulin@gmail.com

  • Python 寫自動白平衡 – 完美反射核心

     

    最近上班遇到一些圖片顏色偏離原色,

    身為影像工程從業者就會想自己寫 3A算法來校正。

    而自動白平衡便是 3A 中的其中 1A – 南湖大山!

    而自動白平衡便是 3A 中的其中 1A – Auto White Balance

    自動白平衡的核心就是解決圖片色偏的方法,

    大都是找到圖片中的參考值,

    再用參考值及不同方法去對整張圖片做校正。

    像是灰色世界算法就是拿全圖片的加總的三通道均值當作參考值,

    再拿這個參考值除以三通道分別均值做成三通道的增益,

    得到三通道增益後分別對通道相乘。

    今天提到的完美反射核心,它的概念是相信圖片中有一群接近白色的區塊,

    因為 RGB or BRG 的世界裡面 (255, 255, 255) 就是白色嘛,

    所以有點像找到這塊邊長為 255 的正方形中那群最遠離原點 (0, 0, 0) 的那群點。

    找到那群點後,再求得這群的均值作為參考值,

    參考值除以各通道的均值就能得到各通道的增益,

    最後再拉回去與原圖相乘 -> 結案。

      
    def AutoWhiteBalance_PRA(imgPath, ratio=0.2): # 自動白平衡演算法_完美反射核心, 0 < ratio <1.
    
        BGRimg = cv2.imread(imgPath)
        BGRimg = BGRimg.astype(np.float32)    # 將資料格式轉為 float32,避免 OpenCV 讀圖進來為 np.uint8 而造成後續截斷問題。
        pixelSum = BGRimg[:,:,0] + BGRimg[:,:,1] + BGRimg[:,:,2]     
        pexelMax = np.max(BGRimg[:,:,:])    # 求三通道加總最大值作為像素值拉伸指標
        
        row , col, chan = BGRimg.shape[0], BGRimg.shape[1], BGRimg.shape[2]
        
        pixelSum = pixelSum.flatten()
        imgFlatten = BGRimg.reshape(row*col, chan)
        
        thresholdNum =  int(ratio * row * col)
        thresholdList = np.argpartition(pixelSum, kth=-thresholdNum, axis=None)    # 由右至左設定閾值位置,並得到粗略排序後的索引表。
        thresholdList = thresholdList[-thresholdNum:]    # 取得索引表中比閾值大的索引號
        
    
        blueMean, greenMean, redMean, i = 0, 0, 0, 0
        
        for i in thresholdList:
            blueMean  += imgFlatten[i][0]
            greenMean += imgFlatten[i][1]
            redMean   += imgFlatten[i][2]
        
        blueMean  /= thresholdNum
        greenMean /= thresholdNum
        redMean   /= thresholdNum
    
        blueGain  = pexelMax / blueMean
        greenGain = pexelMax / greenMean
        redGain   = pexelMax / redMean
        
        BGRimg[:,:,0] = BGRimg[:,:,0] * blueGain
        BGRimg[:,:,1] = BGRimg[:,:,1] * greenGain
        BGRimg[:,:,2] = BGRimg[:,:,2] * redGain
    
        BGRimg = np.clip(BGRimg, 0, 255)
        BGRimg = BGRimg.astype(np.uint8)
    
        return BGRimg
    

    np.argarptition
    中間那個 np.argarptition 比較迷惑,可以說一下。
    np.argarptition 的功能就是把你指定的值設定好,
    然後把 小於/大於 設定值的數的索引排序在設定值的 左邊/右邊。
    舉個例子,有個數列是:
    list = [3, 2, 5, 6, 1, 4]
    我想要找第二大的值,那我的參考值就設定成 -2。
    (從右邊數過來第二個,換句話說就是第二大的 5。)
    Index = np.argarptition(list, kth=-2, axis=None)
    那麼 Index 就會等於:
    Index = [0, 1, 4, 5, 2, 3] 
    分別是 [3, 2, 5, 6, 1, 4] 的索引中小於/大於 5 的索引。
    如果把 Index 換成值,像是 list[Index] 就會等於:
    list[Index] = [3, 2, 1, 4, 5, 6]
    但在指定值(5) 左右的順序不保證,得看演算法怎麼實現,
    會選這套方法是因為複雜度會比全排序還要快。
    破圖問題
    理論上所有通道的 Gain 都會是大於 1 的正數,
    再加上最後有做 clip(0, 255)
    換言之所有經轉換的像素值上限是 255, 下限是自己的原值。
    但在我計算這套算法的時候,
    看到某個藍色通道值是 80,計算完之後變成 17 我真的是傻了。
    後來排查才知道哪裡爆開:
    1. cv2.imread(imgPath) 讀進來的資料型態是 numpy.uint8
    那個 8 我之前都認為是 8 Byte,
    所以認為存值可以存到 $2^{8*8}$ 是沒問題的。
    沒想到它是 8 bit
    取值範圍直接變成 [0, 255]
    所以當我的 Gain 等於 3.421021082738187 時,
    理論上乘以增益的像素值(80)應該要是 273.多。
    但是為什麼是 17 呢?
    登登登登
    因為二進位下 273 是 100010001,而最終要符合 uin8 轉型則會變成 00010001,
    那就是 十進位 17 。
    所以這個問題才會演變成像素值驟降 -> 破圖。
    以下是我到酒吧喝酒,以被色偏的百富12年做標準的圖片:
    (左邊是原圖;中間是灰色世界校正法;右邊是完美反射法,ratio 開 0.05)
    百富的酒標應該要是純白色的,以這個基準來說我覺得完美反射校正得比較好,
    它連後面的燈光都一併校正,看起來比較舒服,
    而灰色世界感覺上了一層透明深藍色濾鏡。
    如果本文有任何書寫錯誤麻煩聯絡我:
    wuyiulin@gmail.com
    Ref.
  • 不均勻光源下的優化 SSIM 演算法

    不均勻光源下的優化 SSIM 演算法

     

    這篇是 SSIM 系列第三篇,接續前篇 使用 PyTorch 實做 2D 影像捲積

    要來談一下 SSIM 如何在不均勻光源下優化 SSIM。

    如果你是剛開始接觸 SSIM 的人,可能會跟我一樣看過一篇幫愛因斯坦去噪的實現文章

    沒錯,當初我也是看到愛因斯坦的臉逐漸浮現,才覺得 SSIM 可以拿來解工程問題!

    只是愛因斯坦那篇的調節亮度方法對於工程來說太過理想化,

    問題在於他是把整張圖片的像素值乘以 0.9 調亮度!

    這樣的話 SSIM 算法裡面的變異數項是不會變的,只有考慮到平均數項,

    所以他做出來的 SSIM 值不會差太多。

    但是現實世界是很絢爛多彩且難以預測的,
    就像我前兩天載室友出門,停等紅燈時他突然發生奇怪的聲音,

    轉頭問他在幹麻?

    :我在跟你的機車排氣管對頻。

    實務上,我們可能會面對不均勻光源的問題,就像是街邊的燈光:

    這燈光很糟糕,比忘記簽聯絡簿的國小學童還糟糕,

    因為它的光源只會影響照片的一部分,而且還會隨著距離遞減。

    如果使用原版的 SSIM 方法對比這種有無開燈的照片,

    則 SSIM值會不可避免的大幅下降,

    但這兩張照片的前景內容物(e.g. 人、車、直升機、脫光衣服在路上奔跑的人)一樣對吧?

    所以在基於前景的情況下,有些時候我們會希望有無開燈的照片應該要一樣,

    數學上也就是得到理想值為 1 的 SSIM 值。

    (實在是繞口令,讓我想到事件攝影機。)

    要解這個問題就要先了解 SSIM 的本質,

    SSIM 是由 sliding window 來解圖片中各自區域均值、變異數,還有兩張圖片的共變數所構成。

    而二維的 window 又是由一維的 kernel (通常是 Gaussian)矩陣相乘得到,

    基本上 OpenCV 裡面提供的方法是這樣:

    nkernel = cv2.getGaussianKernel(11, 1.5)
    nwindow = np.outer(nkernel, nkernel.transpose())
    

    往 cv2.getGaussianKernel 裡面看,我們可以知道這條函式來自 getGaussianKernelBitExact:

    再往 getGaussianKernelBitExact 裡面看:

    重點是 158, 181 行運算的兩個迴圈,他們在幫忙算高斯核並歸一化。

    我們的重點是歸一化,以前的簡易版 OpenCV Source Code 大概長這樣:

    def getGaussianKernel(M, std):
        n = np.arange(0, M) - (M - 1.0) / 2.0
        sig2 = 2 * std * std
        w = np.exp(-n ** 2 / sig2)
        muli = 1/sum(w[:])
        wn = w * muli
    
        return wn
    

    這種歸一化的一維高斯核長得像是:

    扁扁有點可愛,像是軟趴趴的史萊姆。

    歸一化的好處是能夠保證像素值永遠恆等(因為也只是重新分配)。

    但是這樣可能會造成一些問題,

    想像一下你是該像素當事人站在 X=5 的位置,
    現在要過年了,大家手上都有一筆要發出去的紅包錢,俗稱紅包預算。
    假設你的紅包預算是一萬塊:

    X=5 是你自己,辛苦了一整年,你決定給自己紅包預算 * 0.26 倍,恭喜獲得 2600 元。
    X=4, X=6 這個漢明距離為一的,是你的父母,得到他們的紅包預算 *0.21 倍;
    X=3, X=7 這個漢明距離為二的,是你的手足,得到他們的紅包預算 *0.10 倍;
    一路發下去,你會發現不管身邊的人多有錢,你最多只會得到原來的一萬xD
    因為你早就被歸一化了!
    這種 SSIM 分配框架下,就會產生很多問題,例如:
    你跟小明是同事,都有一萬塊的紅包預算。
    你身邊的人都很有錢,分完紅包你還有一萬塊;
    但是小明就不一樣了,他身邊的人都超窮,分完紅包後他只剩兩千六。

    小明知道這件事情後,會不會沒事就想從背後捅你這個好野死囝仔?
    這就會造成很多社會問題,因為貧富差距(變異數)過大。
    要怎麼解決這個問題呢?

    讓我們看看非歸一化的原始高斯核:

    這個版本就好多了,
    無論小明身邊的人再怎麼窮,他始終可以拿回自己的紅包預算一萬塊,
    剩下的都是多的。
    於是小明跟你的貧富差距就減少了(某種程度上減少變異數),
    所以這個問題就得到了緩解。
    結論:

    如果要讓兩張前景相同,
    但是其中一張有受燈光(或是部份遮罩影響)的圖片做 SSIM 計算,
    使用非歸一化的高斯核可以有效提昇 SSIM 值。

    以下附上我的實驗源碼:

    def ourGaussianKernel(M, std):
        n = np.arange(0, M) - (M - 1.0) / 2.0
        sig2 = 2 * std * std
        w = np.exp(-n ** 2 / sig2)
        # muli = 1/sum(w[:])
        # wn = w * muli
    
        return w
    
    def ssim_Normalized(img1, img2):
        C1 = (0.01 * 255)**2
        C2 = (0.03 * 255)**2
    
        img1 = img1.astype(np.float64)
        img2 = img2.astype(np.float64)
        nkernel = cv2.getGaussianKernel(11, 1.5)
        nwindow = np.outer(nkernel, nkernel.transpose())
        nmu1 = cv2.filter2D(img1, -1, nwindow)[5:-5, 5:-5]
        nmu2 = cv2.filter2D(img2, -1, nwindow)[5:-5, 5:-5]
        nmu1_sq = nmu1**2
        nmu2_sq = nmu2**2
        nmu1_mu2 = nmu1 * nmu2
        nsigma1_sq = cv2.filter2D(img1**2, -1, nwindow)[5:-5, 5:-5] - nmu1_sq
        nsigma2_sq = cv2.filter2D(img2**2, -1, nwindow)[5:-5, 5:-5] - nmu2_sq
        nsigma12 = cv2.filter2D(img1 * img2, -1, nwindow)[5:-5, 5:-5] - nmu1_mu2
        nssim_map = ((2 * nmu1_mu2 + C1) *
                    (2 * nsigma12 + C2)) / ((nmu1_sq + nmu2_sq + C1) *
                                           (nsigma1_sq + nsigma2_sq + C2))
        return nssim_map.mean()
    
    def ssim_Unnormalized(img1, img2):
        C1 = (0.01 * 255)**2
        C2 = (0.03 * 255)**2
    
        img1 = img1.astype(np.float64)
        img2 = img2.astype(np.float64)
        kernel = ourGaussianKernel(11, 1.5)
        window = np.outer(kernel, kernel.transpose())
        mu1 = cv2.filter2D(img1, -1, window)[5:-5, 5:-5]
        mu2 = cv2.filter2D(img2, -1, window)[5:-5, 5:-5]
        mu1_sq = mu1**2
        mu2_sq = mu2**2
        mu1_mu2 = mu1 * mu2
        sigma1_sq = cv2.filter2D(img1**2, -1, window)[5:-5, 5:-5] - mu1_sq
        sigma2_sq = cv2.filter2D(img2**2, -1, window)[5:-5, 5:-5] - mu2_sq
        sigma12 = cv2.filter2D(img1 * img2, -1, window)[5:-5, 5:-5] - mu1_mu2
        ssim_map = ((2 * mu1_mu2 + C1) *
                    (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) *
                                           (sigma1_sq + sigma2_sq + C2))
        return ssim_map.mean()
    if __name__=='__main__':
        img1_path = 'img1.png'
        img2_path = 'img2.png'
        kernelSize = 21
        light = list(map(np.uint8, gkern(kernlen=kernelSize, std=1.5)*100))
        img1 = np.random.randint(0, 150, size=(100, 100)).astype(np.uint8)
        img2 = np.zeros_like(img1)
        img2[:] = img1[:]
    
        row = np.random.randint(int(kernelSize/2), img1.shape[0] - int(kernelSize/2))
        col = np.random.randint(int(kernelSize/2), img1.shape[1] - int(kernelSize/2))
        times = np.random.randint(1, 100)
        for i in range(0, times):
            img2[row:row + kernelSize, col:col + kernelSize] += light
        img2 = np.clip(img2, 0, 255)
        
        print("This is Normalized): " + str("{:.5f}".format(ssim_Normalized(img1, img2))))
        print("This is Unnormalized): " + str("{:.5f}".format(ssim_Unnormalized(img1, img2))))
    

    親測可用:

    有任何問題或筆誤歡迎聯繫我:wuyiulin@gmail.com

  • 使用 PyTorch 實做 2D 影像捲積

    使用 PyTorch 實做 2D 影像捲積

     承上篇:Structural Similarity(SSIM) 的 PyTorch 實現

    由於我 3D 影像處理做太多(X) 2D 影像處理還沒有恢復記憶(O)

    上次在刻高斯濾波的時候忘記 PyTorch Kit 的 Convolution 一個很重要的特性:

    PyTorch 會參考其他通道的資訊啊!

    其實這點也是無可厚非,畢竟 PyTorch 的初衷是給深度學習用的框架,

    韓信點兵多多益善嘛!

    所以 PyTorch 的 Convolution 流程大概是這樣:

    顯而易見地,沒經過額外處理的話 SSIM值會超出邊界(0,1),

    這違反了 SSIM 這項指標的意義。

    所以我們要來看一下隔壁棚 2D影像處理套件 – OpenCV 怎麼做 2D 影像的 Convolution?

    在 OpenCV 裡面,每個通道都是用同一顆 Kernel,但是會分離通道做 Convolution,

    如下圖:

    得到這個資訊後,我打算用分 Groups 來解這題。

    PyTorch 中的 2D Convolution 有兩種,

    一種是 torch.nn.Conv2d

    另一是 torch.nn.functional.conv2d

    前者只要設定好 Kernel 大小就能動,後者則可以提供使用自己 Kernel 的 API。

    筆者這次使用後者,就以後者的 Document 來解釋:

    首先我們要知道自己要分幾組 Group?

    我們希望每個通道都各自做 Convolution ,而 RGB 圖片有三個通道,

    所以我們的 Groups 應該設定三組。

    再來需要注意上圖中 torch.nn.functional.conv2d 中的 weight,

    這裡的 weight 代表的是 Kernel。

    既然改了 Groups ,Kernel 的相應尺寸也要記得改變。

    原本是 Kernel.shape = {3, 3, H, W} 要變成 {3, 3/3 = 1, H, W}。

    這樣就能開心地模擬 OpenCV 的 2D Convolution 啦!

    筆者在手刻 Kernel 的時候也遇到一些光怪陸離的問題,

    預計下一篇會來寫這部份。

  • OpenCV AI Kit 1 (OAK-1)從零開始的傻瓜式教學

     

    起因是因為主管塞了兩盒工程機包裝的東西給我,

    叫我趕快做點東西出來。

    :大勾請問這兩盒是什麼?

    "窩也不是很清楚,應該是攝影機吧?"

    "上禮拜才送來的,就交給你好好幹了!(瞇眼。"

    回到座位拆開包裝,奇妙的字眼映入眼簾 – OAK-1?橡樹一號?

    乖乖隆叮咚?

    我的業務範圍上從先進飛彈制導,下至林業技術輔導了嗎?

    看來下一任臺灣隊長就是我了對吧?

    現在應該抓緊時間去曬太陽之類的?

    Anyway 這的確是攝影機,不是什麼先進的橡樹、農業技術輔導。

    OAK-1 的全名是 OpneCV AI Kit

    會出現這東西,我猜是 OpenCV 覺得大家用的環境太混帳了,

    導致沒辦法發揮我們程式的實力,所以自己跳出來賣攝影機的概念。

    (我看過學弟拿沒 GPU 的文書機配 WEBCAM 在那邊掃口罩偵測,FPS 著實感人。)

    做影像辨識是這個樣子:

    影像辨識是需要算的,就算你模型都訓練好,

    還是要有一個地方可以判斷(計算),像是我學弟那個例子就是把影像丟回電腦用 CPU 算,

    結果就是出來的影像 FPS 感人。

    這麼感人的 FPS 肯定是不行的,畢竟未來要用在載具上,

    任務時間來到毫秒級,FPS 必須提高,下游任務的準確度才會提高。

    (才不會上演街頭人肉保齡球。)

    但是 OpenCV 左想右想,總不可能在每臺載具上面塞一張很貴的 GPU 對吧?

    為了教育我們這些只用 CPU 算的弱智兒童(X)

    為了讓貪財產業鏈侵入學生生活(O)

    (OpenCV 後面是 Intel,Intel 最近重新出了自己的 GPU 你知道的。)

    就出了 OAK 系列,裡面幫你塞了一張 VPU。

    VPU 是一種介於 CPU 與 GPU 中的東西,

    可以想像成沒那麼強的 GPU ,但是可以負責簡單的判斷。

    總之有這張 VPU 塞進去以後捏,好處有兩個:

    1.後端(伺服器端)可以不用塞 GPU,這種 Camera 又比 GPU 便宜很多,省錢!

    2.判斷直接在 Camera 直接做完,理論上比較省傳輸的 latency。

    好,前面廢話那麼多,來裝環境。

    首先這篇環境是 for Windows 10 ,因為我在公司被分配到 Windows 10 的機子,

    順便做個紀錄,聽起來以後要交報告的,多塞點廢話(X)。

    首先用管理員權限打開你的 Windows Shell (藍底白字的那個)

    Windows Shell

    Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
    

    如果你沒有喜歡的編輯器 加上 還沒有安裝 Python 

    就下這行,幫你裝 PyCharm + Python-least:

    choco install cmake git python pycharm-community -y
    

    然後你可能會發現阿幹怎麼 GIT 裝失敗?

    下這行:

    winget install --id Git.Git -e --source winget

    然後就裝起來了,一整個正確!

    去載這包:

    https://github.com/luxonis/depthai-python

    啟動 example/Yolo/tiny_yolo.py

    偵測結果

    好欸!

    你也可以說自己會偵測物件了(ㄍ。

    之後應該會做其他東西吧?(望天。

    Ref. 

    官網教學:https://docs.luxonis.com/projects/api/en/latest/install/#installation

    輔助影片教學:https://www.youtube.com/watch?v=ekopKJfcWiE&ab_channel=BrandonGilles