2024年9月8日

anamorph鏡筒變形畫的幾何轉換原理,使用python實作

 2013年時,我玩了一個叫做鏡筒玩玩變形畫anamorph的東西






當時就想要寫一支程式來作這種影像變換,不過當時還不知道從何下手,直到最近上影像處理的課,講到了仿射變換的作法,才讓我想通可以怎麼作。

以影像的放大來舉例

原始影像在x方向放大a倍,在y方向放大b倍,新影像可以表示為:
x' = ax
y' = by

就可以用原始影像算出新影像應該長什麼樣子,
舉例來說,原始影像在x方向和y方向都放大2倍,
那麼原始影像的(1,1)和(1,2)就會變成新影像的(2,2)和(2,4)兩個點的顏色,

但是新影像的(2,3)呢?原始影像並沒有(1,1.5)這個點啊,所以那個點就會沒有顏色。所以實作上就會用inverse mapping 來處理。

原本的方程式可以改寫成
x = x' /a
y = y' /b

再透過一些內插法(Interpolation)就可以計算新影像應該是什麼顏色。例如剛剛的問題,新影像的(2,3)的顏色就是原始影像的(1,1.5)的顏色。如果使用Near Neighbor Interpolation,就用原始影像的(1,2)來代表(1,1.5),如果是用Bilinear Interpolation,就用原始影像的(1,1)和(1,2)的顏色算平均得到(1, 1.5)的顏色,然後在指定給新影像的(2,3)。

接下來再來思考這種鏡筒變形畫怎麼製作吧。

鏡筒變形畫


原始影像長這樣


新影像要長這樣



也就是要先將原始影像的座標從笛卡爾座標變成極座標,然後在極座標上進行幾何轉換。

原始影像的紅點和藍點都在同一個x座標上,經過變換後,它們的極座標都會在同樣的θ 上,而它們的r由它們的y座標決定,y座標越小,則r越大,像是變換後的紅點會移動到最外圍。以藍點為例,原始的y座標是影像高度的4/7,變換後的r就是 r0 + r1(4/7)

原始影像的紅點和綠點在同一個y座標上,變換後它們的 r 都一樣,而θ則不相同,由原始影像的x座標決定。

用數學方程式表達這種變換如下:

定義參數:

  • f(x, y):原始影像的像素值。
  • r0:定義影像的初始半徑。
  • r1:定義影像的變換範圍半徑。
  • θ:角度座標,用來決定像素的旋轉。
  • r:半徑座標,用來決定像素的距離。
  • (x0, y0):中心點的坐標。
  • nx, ny:原始影像的寬度和高度。

變換步驟:

1. 極座標轉換:

r = √((x - x₀)² + (y - y₀)²)

θ = arctan2(y - y₀, x - x₀)

其中 (x₀, y₀) 是新影像的中心,xy 是新影像的坐標點。

2. 新座標映射:

新的橫坐標 map_x 和縱坐標 map_y 是透過角度 θ 和半徑 r 映射回原始影像的座標:

map_x = nx - 1 - (nx - 1) · (θ + π) / 2π

map_y = ny - 1 - (ny - 1) · (r - r₀) / r₁

這些公式將極座標 rθ 重新映射到原始影像的笛卡爾坐標 (map_x, map_y),然後將原影像進行扭曲或展開。

3. 有效區域檢測:

使用mask來確保映射後的像素點在有效範圍內:

valid_mask = (map_x ≥ 0) ∧ (map_x < nx) ∧ (map_y ≥ 0) ∧ (map_y < ny)

4. 生成新影像:

將原影像的有效像素根據映射後的座標填充到新影像中,無效區域使用白色填充。


根據這樣的原理所撰寫的程式在github

https://github.com/ChihHsiangChien/anamorph

其中的anamorph.py 就是產生變形畫的程式
其中有兩個參數start_radian 和 spread_radian ,指定的數值是弧度,為np.pi的倍數
start_radian = np.pi時,弧形的起點從np.pi的方向開始畫,開展的弧度是0.5π = 90°



開展弧度設定成1.95π,接近2π也就是擴展成360°


start_radian則決定從哪個弧度開始畫圖,若為0則是長這樣,你可以跟前面幾張圖比較看看






以JS處理moodle選擇題轉檔

 這兩天葉老師問我有關Moodle的GIFT題目格式的問題,如果要使用多選題,應該如何使用符號,後來我找了moodle官方的頁面答案給她

What two people are entombed in Grant's tomb? {
   ~%-50%No one
   ~%50%Grant
   ~%50%Grant's wife
   ~%-50%Grant's father
}


已經有題庫的人,可以用一些批次方法直接處理這些題目轉檔的問題,例如python,但如果是對程式陌生的人,讓他使用python的話,可能太難。

不過現在這個時代,有很多方法可以處理,例如你可以用自然語言讓AI寫出一個轉檔程式啊,像是用Claude,它的Artifacts功能甚至還可以直接在頁面旁產出程式執行的畫面。

回到本文主題,如何讓沒有程式基礎的人可以順利轉檔,那就寫一個轉檔程式來使用吧。

把以下的文字貼在記事本上,副檔名存成html,例如convert.html,用瀏覽器打開就是一個轉檔程式了。

使用方式就是打開excel,把題目複製貼到轉檔程式上方的框框,按下按鈕就生成轉檔完的文字了。

這個程式只有處理單選和多選題目而已,程式是根據標準答案來分辨單選或多選,標準答案使用123來編碼,而不是ABC



===================

<!DOCTYPE html>

<html lang="zh">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>轉換工具</title>

    <style>

        body {

            font-family: Arial, sans-serif;

            margin: 20px;

        }

        textarea {

            width: 80%;

            height: 300px;

            margin-bottom: 10px;

        }

        .button-container {

            text-align: left; /* 将按钮靠左对齐 */

            margin-bottom: 10px;

        }

        button {

            margin-bottom: 10px;

        }

    </style>

</head>

<body>

    <h1>題目轉換工具</h1>

    <textarea id="input" placeholder="貼上excel複製的文字"></textarea>

    <div class="button-container">

        <button onclick="convert()">轉換</button>

    </div>

    <textarea id="output" placeholder="輸出結果" readonly></textarea>


    <script>

function convert() {

const inputText = document.getElementById('input').value.trim();

const outputElement = document.getElementById('output');

if (!inputText) {

outputElement.value = '請輸入數據。';

return;

}


const lines = inputText.split('\n').map(line => line.trim()).filter(line => line);

let result = '';


for (const line of lines) {

                const [title, question, ...optionsAndAnswer] = line.split('\t');

const answerIndex = optionsAndAnswer.pop(); // 正確答案

const options = optionsAndAnswer;

const correctIndices = answerIndex.split('').map(Number);

const numOptions = options.length;

const numCorrect = correctIndices.length;


                result += `::${title}::${question} {\n`;


if (numCorrect === 1) {

// 單選題

options.forEach((option, index) => {

if (correctIndices.includes(index + 1)) {

result += `   =${option}\n`;

} else {

result += `   ~${option}\n`;

}

});

} else {

// 多選題

const percentage = numCorrect > 0 ? Math.floor(100 / numCorrect) : 0; // 計算正確答案的百分比

options.forEach((option, index) => {

if (correctIndices.includes(index + 1)) {

result += `   ~%${percentage}%${option}\n`;

} else {

result += `   ~%-100%${option}\n`;

}

});

}


result += '}\n\n';

}


outputElement.value = result.trim();

}

    </script>

</body>

</html> 


2024年9月3日

用python進行影像處理看看平均臉長怎樣

 幾年前玩影像處理的時候,用過python做過平均臉的專案

不過程式不是自己寫的,是從這個網站來的

https://learnopencv.com/average-face-opencv-c-python-tutorial/


製作平均臉的流程是先抓取68個臉部特徵,原作者用的是Dlib,但是在Dlib安裝實在太困難,我在linux編譯好一陣子才成功,而在windows上還沒試成功,所以後來我就改用OpenCV來做。


這68個特徵如下圖,在這個專案中,兩眼眼尾的兩點很重要,我們要將所有人的這兩點都對齊。



找到臉部的這些特徵點之後,因為下一步是要進行對齊,所以要把這些點連線成許多小三角,這樣才能進行仿射變形。而哪些點才能互相連接呢?這要用到Delaunay triangulation 的方法了。


圖片來源:wikipedia
這些三角形有一個特殊性質:任何一個三角形的其外接圓內不會有其他點。

用各實例來看看,我們用湯姆的臉來看看,這些藍點是偵測出的臉部特徵點


找出這些點的Delaunay triangulation



接下來就要對齊了,這篇文章的程式的做法是把大家的眼睛位置都放在新圖像的1/3高度,而橫坐標分別是新圖像寬度的0.3和0.7。

接下來就是用原圖的兩眼尾位置和新圖的眼尾位置,去計算出縮放、平移、旋轉的仿射矩陣長什麼樣子,然後就用這個矩陣把整張臉稍稍變形對齊到新的位置。

不好意思,湯姆的臉就稍稍變形過去了


我們把一群人的照片都用同樣方式對齊到新的位置



最後就是把大家的臉放在一起做平均啦,以下就是五位影星的平均臉,帥!



這是找了九位女星的照片,經過對齊的樣子



平均臉呢?






我想製作這平均臉專題的目的其實不是看漂亮的明星啊,我想看的是如果我用不同政黨的立委照片,是不是會看到不同的平均臉呢?

所以第一步就是上立法院網站抓圖片,我先確定好目標,我要把不同政黨的男性和女性分開來將圖檔存在不同資料夾中。




所以就要寫一隻爬蟲,把立委的個人資料和照片都爬下來再整理


最後就是令人期待的結果了,除了中間的政黨以外,左右兩個政黨幾乎無法分辨,怎麼會這樣呢?其實這就是採樣的問題了,中間的人數少,所以容易看出個別差異,而兩邊的人數多,統計之後就會讓個別差異減少。





如果你對程式有興趣,我放在github了

https://github.com/ChihHsiangChien/averageFace