2024年12月31日

利用AI進行評語描述的轉換



最近看到了一張網路梗圖,是把自己的真實習性寫在履歷上。於是我就想用幾個AI服務玩看看這些描述轉換的過程,各種大型語言模型是如何處理的?

ChatGPT


我:請參考以下對應關係,將我接下來提供的原始描述進行轉換
原始描述 轉換後描述
沒有優點 可塑性強,具有全方位培養潛力
沒基礎,什麼都不會 學習能力強,有非常大的進步空間
喜歡摸魚,愛聊八卦,愛擺爛 善於溝通,膽大心細,善若止水
脾氣不好,經常罵人 為人坦率,個性直爽

GPT:好的,請提供原始描述,我會依照對應關係進行轉換!


我:上課不專心,喜歡跟同學聊天
GPT:善於與人交流,具有良好的社交潛力。

我:不舉手發言,經常打斷老師同學說話
GPT:思維活躍,表達欲強,具備良好的主動參與意識。

我:不寫作業,考試都用猜的,上課都在睡覺
GPT:具備放鬆心態,應對壓力能力強,潛在學習潛力巨大。

我:經常跟同學吵架,甚至打架,會和一群同學在網路上言語霸凌其他同學
GPT:性格鮮明,影響力強,具備領導潛力與人際協調能力的發展空間。

我:經常跟同學吵架,甚至打架,會和一群同學在網路上言語霸凌其他同學
GPT:性格鮮明,影響力強,具備領導潛力與人際協調能力的發展空間。

我:口齒不清 語焉不詳 無法表達自己意見 經常與人爭吵
GPT:具備改善表達能力的潛力,情感豐富,對人際互動有探索與成長的空間。


Gemini 1.5 FLASH

同樣的起始問句,Gemini這麼回應:

好的,我了解您的需求。

我將根據您提供的對應關係,將您提供的原始描述進行轉換。

請您將原始描述逐一提供給我,我將逐一為您轉換。

例如:

您提供:沒有優點,沒基礎,什麼都不會

我轉換為:可塑性強,具有全方位培養潛力,學習能力強,有非常大的進步空間。

請注意:

  • 轉換後的描述可能會因原始描述的具體內容而有所不同。
  • 有些原始描述可能無法直接對應到現有的轉換關係,這時我會盡量根據您的意圖進行轉換。
  • 如果您對轉換結果不滿意,歡迎您提出修改意見。

請隨時提供您的原始描述,我將盡力為您服務。

此外,為了更精確地進行轉換,您可以提供以下額外的資訊:

  • 您希望轉換後的描述呈現出什麼樣的風格? (例如:積極正面、幽默風趣、專業嚴謹)
  • 您希望轉換後的描述針對什麼樣的場合或對象? (例如:求職面試、自我介紹、社交場合)

這些資訊將有助於我更準確地把握您的需求。


我:上課不專心,喜歡跟同學聊天
Gemini:
好的,根據您提供的「上課不專心,喜歡跟同學聊天」這個原始描述,我可以為您轉換成以下幾種不同的表達方式:

強調積極面,凸顯潛力:

  • 社交能力強,善於溝通協調,具有團隊合作精神。 (強調與人互動的能力)
  • 活潑好動,具備豐富的想像力,對人對事充滿好奇心。 (強調學習熱情與創造力)
  • 思維敏捷,反應迅速,善於表達自己的想法。 (強調思維能力與表達能力)
強調成長空間,鼓勵進步:

  • 需要加強專注力訓練,培養良好的學習習慣。 (直接指出需要改進的地方)
  • 透過有效的時間管理和學習策略,能更有效率地完成學習任務。 (提供改善建議)
  • 在老師的指導下,能逐步提升學習效率和成績。 (表達對未來進步的期待)
針對不同場合或對象,調整表達方式:

**** 求職面試:
「我個性活潑,喜歡與人交流,在團隊合作中能發揮積極作用。雖然過去在學習上有些許不足,但我正在積極調整,努力提升專注力,相信能很快適應新的學習環境。」
**** 自我介紹:
「我是一個喜歡交朋友的人,在課堂上常常會和同學分享想法。雖然有時會因為太投入討論而分心,但我正在學習如何平衡社交和學習。」
**** 家長溝通:
「孩子最近在學校表現得比較活潑,喜歡和同學聊天。我們會鼓勵他多參與課堂討論,同時也會加強對他的專注力訓練,幫助他更好地掌握課堂內容。」
您可以根據具體的場合和想要傳達的訊息,選擇最適合的表達方式。

此外,您還可以提供以下資訊,幫助我更精準地進行轉換:

您希望強調的是孩子的哪一方面? (例如:社交能力、學習潛力、成長空間)
您希望轉換後的描述給人什麼樣的印象? (例如:積極向上、謙虛謹慎、自信樂觀)
您希望針對哪些人傳達這個訊息? (例如:老師、家長、同學)
請您告訴我,您希望我進行哪一種轉換?

例如:

我希望強調孩子的社交能力,並表達他對學習的熱情。
我希望轉換後的描述能給老師留下積極向上的印象,同時也能讓孩子明白需要改進的地方。
只要您提供更詳細的資訊,我就能為您提供更貼合需求的轉換結果。


我:不舉手發言,經常打斷老師同學說話

Gemini:

好的,我們來看看如何將「不舉手發言,經常打斷老師同學說話」這個描述轉換成更正向、積極的表達。


分析原描述:


負面關鍵字: 不舉手、打斷、經常

行為特徵: 缺乏耐心、不懂得尊重他人、可能對課程內容不感興趣

轉換方向:

我們可以從以下幾個角度來進行轉換:

  • 強調潛力: 這個孩子可能具有活潑、好動、思維敏捷的特質,只是需要引導到正確的方向。
  • 轉換行為描述: 將「打斷」轉換成「積極參與」,將「不舉手」轉換成「有自己的想法」。
  • 強調成長空間: 每個人都有成長的空間,透過適當的引導和鼓勵,孩子一定能改進。

可能的轉換方式:


強調積極參與:

「對課程內容充滿好奇心,喜歡與他人分享自己的想法。」

「具有強烈的表達欲,渴望參與討論。」

強調個性特點:

「個性活潑,思維敏捷,善於表達。」

「對周遭事物充滿好奇心,喜歡探索新事物。」

強調成長潛力:

「透過引導,能培養良好的傾聽習慣,學會尊重他人。」

「在老師的鼓勵下,能更積極地參與課堂討論。」

強調獨特觀點:

「常常有獨特的見解,只是需要找到合適的表達方式。」

「對問題有自己獨特的思考角度,需要更多的機會來分享。」

針對不同場合的轉換:


與老師溝通:

「XX同學對課程內容非常感興趣,常常會提出一些很有意思的問題。只是有時會太過急於表達,打斷了其他同學的發言。希望老師能多給他一些機會,讓他能夠在課堂上分享自己的想法。」

與家長溝通:

「XX在家裡也是一個活潑好動的孩子,喜歡表達自己的意見。我們會鼓勵他多參與課堂討論,同時也教導他如何尊重他人,耐心傾聽。」

孩子自我介紹:

「我是一個喜歡思考、喜歡分享的人。雖然有時會忍不住想插嘴,但我正在學習如何耐心傾聽,尊重別人的意見。」

注意事項:


具體情況具體分析: 不同的孩子有不同的情況,轉換時要結合具體情況來進行。

保持積極態度: 即使孩子存在一些問題,也要看到他身上的優點,給予肯定和鼓勵。

與孩子一起溝通: 和孩子一起討論,聽聽他自己的想法,共同找到解決問題的方法。

總結:


通過正向的語言描述,我們可以幫助孩子建立自信,激發學習興趣,同時也能讓老師和家長更好地理解孩子,給予孩子更多的支持和幫助。


您想嘗試哪一種轉換方式呢?還是您有其他想法?


請提供更多關於這個孩子的資訊,例如:


年齡:

科目:

以往的表現:

您對他的期望:

這樣我才能給您更具體的建議。


Claude的表現如何?

 接下來看看Claude怎麼回應

Claude:

好的,我明白你的需求。這是一個將負面描述轉換成正面描述的對照表:

"沒有優點" → "可塑性強,具有全方位培養潛力"

"沒基礎,什麼都不會" → "學習能力強,有非常大的進步空間"

"喜歡摸魚,愛聊八卦,愛擺爛" → "善於溝通,膽大心細,善若止水"

"脾氣不好,經常罵人" → "為人坦率,個性直爽"

請提供你要轉換的原始描述,我會依照這個對應關係來協助你進行轉換。


 我:上課不專心,喜歡跟同學聊天

Claude:根據剛才的轉換邏輯,我會這樣描述:

"人際關係良好,善於交際,具備優秀的社交能力"

這個轉換保持了原意中的社交元素,但以更正面和專業的角度來描述這些特質。



我:不舉手發言,經常打斷老師同學說話

Claude:根據轉換邏輯,可以這樣正面描述:

"積極參與課堂互動,表達欲望強,樂於分享想法"

這個描述保留了原意中主動表達的特質,但轉換成更正面和建設性的表述方式。

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

 我對於AI評語轉換這件事的評價是正面的,當我自己要給予學生評價時,其實常常遇到詞不達意,或是無法完整描述,而AI協作時,可以讓自己用更寬廣的角度去看待行為的表現,其實不全然是說好話而已,而是用積極面向去說明,畢竟我們並非是要用評語去打擊學生。

2024年11月6日

用AI協助閱讀虎克的【微物圖誌】



昨天看到朋友的文章 關於細胞的發現,教科書寫錯啦!

文中提到「德國的法蘭克福國家歷史博物館以及普渡大學的彼得斯博士(Winfried S. Peters)重新把虎克的那本書《Micrographia》給看了一遍,發現...大家都誤會了!」

這讓我想起了十幾年前,我也曾經去讀過虎克的那本書,寫下這篇文章

虎克看軟木,還有許多你不知道的事

自己親身閱讀過虎克的書籍之後,確實被虎克的種種觀察與發現感動了,也能看到許多教科書上可能誤會的盲點。


當我回頭再去看那篇我寫的文章時,看到文章末了我寫道

「我很期待,哪天有人可以把Micrographia翻譯出中文版,那樣就可以有更多人被感動了,三百多前的書,沒版權問題了吧。(虎克這本Micrographia,我看了很久,其中一個原因就是太多單字都沒看過)」

這時我突然一驚,現在不就可以自己來做這件事嗎-用AI來協助翻譯出中文版,我使用古騰堡計劃線上的電子檔文字,切出區塊之後,再使用openAI的API進行翻譯輸出。

這就是成果了,二十幾萬個中文字的內容。

微物圖誌中文版

然後,我又想起了另一個神級應用NotebookLM,這個部份我已經大量使用在書籍和文獻閱讀上,也可以用他來快速洞察這本書的內容。用NotebookLM來讀書的好處是,它的設計相對於GPT會比較保守,基本上是有所本的,相對不太會產生幻覺,所以適合用在閱讀的輔助上,可以幫助快速理解書籍或文章。

但是如果想要深切了解文章,還是要自己閱讀才好。

目前NotebookLM的來源有字數限制,所以我就將Micrographia的內容切分成兩部分上傳,以下就是利用它進行的分析。



這本書是他使用顯微鏡觀察微小物體的紀錄。文中包含了虎克對針尖、線、絲綢、沙粒、玻璃、鑽石、火焰、水、鹽和植物等物體的觀察,並提出了他對物體的構造、顏色和光的性質的見解。他認為所有物體都是由微小的粒子組成,並提出了一個宇宙存在一個由微小粒子組成的流體以太的理論。虎克的觀察和思考,對後來的科學發展影響深遠,並被譽為是近代科學的先驅之一。


1. 物質的微觀結構:




虎克利用顯微鏡觀察了各種物質,包括針尖、剃刀刃、沙子、金屬和礦物質。他強調了宏觀平滑的物體在微觀尺度下呈現出的複雜性和不規則性。例如,他描述了針尖並不是一個完美的點,而是一個具有各種形狀的表面。

"現在,儘管這個點通常被認為是最鋒利的(因此,當我們想要用最高級的方式表達一個點的鋒利程度時,我們會說:像針一樣鋒利),但顯微鏡可以為我們提供數以百計的例子,證明在自然界中發現的各種其他點要比它鋒利得多。" (摘自對針尖的觀察)



虎克對物質微觀結構的觀察為理解物質的宏觀性質奠定了基礎。

2. 光與色彩:

虎克研究了薄膜和棱鏡中的光線折射和反射,並發展了一套關於色彩產生的理論。他認為,不同顏色是由光波的不同“脈衝”或頻率引起的。

"第三,在某些液體中,我發現了某些物質,它們非常不透明,會產生非常鮮豔的紅色;但通過將其與某些液體混合,可以將其稀釋成任何程度的藍色;因此,根據我的觀察,將紅色轉化為藍色似乎並不難。" (摘自對幾種物質顏色的觀察)


他還觀察了干涉現象,例如肥皂泡和雲母中的牛頓環,並將這些現象與光的波動性聯繫起來。

3. 自然的規律性和模式:

虎克對自然界中存在的規律性和模式感到驚嘆。他觀察到,即使是最小的物體,例如沙粒和礦物晶體,也表現出驚人的幾何形狀和結構。

"我經常觀察到尿液中的沙子或礫石,它似乎是一種酒石質物質,由鹽分和土質物質以類似牙垢的形式結晶而成,有時會粘在尿壺的側面,但大部分會沉到底部,並以粗糙的普通沙子的形式存在;通過顯微鏡觀察,這些物質似乎是一群小的物體,部分透明,部分不透明,有些是白色,有些是黃色,有些是紅色,有些是更棕色和昏暗的顏色。" (摘自對尿液中沙子或礫石的觀察)

他認為,這些模式揭示了潛在的自然法則,這些法則決定了物質的形成和組織。

4. 科學探究的重要性:

整個《微物圖誌》中,虎克強調了仔細觀察、實驗和基於證據的推理的重要性。他鼓勵讀者不要盲目接受他的觀察結果,而是要自己進行實驗並得出自己的結論。

"至於我自己,如果我這些微不足道的勞動成果被認為可以佔據自然史這個大寶庫中的一席之地,我的目的就達到了;如果我未能完成我提出的那些更大更崇高的機械設計,請原諒我,因為例子如此罕見,我無法做出更多這樣的題外話。" (摘自結束語)


虎克的工作表現了科學革命的精神,強調了經驗主義和理性探究的力量。

5. 跨學科思考的價值:

虎克的觀察涵蓋了廣泛的學科,包括物理學、化學、生物學和地質學。他展示了這些領域之間的相互聯繫,以及跨學科思考對於理解自然世界的重要性。


2024年10月9日

arcade製作迷宮,感受試誤學習與頓悟


https://makecode.com/_i5VH6XEsMPyg

最近突然想來做個可以用在動物行為課程的線上迷宮,能夠學習與記憶的動物,在面臨迷宮挑戰時,可以越來越快完成,這個迷宮就是用來體驗這件事。

進入這個迷宮之後,主角會出現在最左邊一欄,而終點會在最右邊一欄,當你費盡心力到達終點後,畫面會顯示你的完成時間,然後就會把你傳送回原來的地點,你必須再度找到終點。



每次完成後,都可以看到你的歷次完成時間。如果你是記性還不錯的動物,你應該可以表現出越來越快完成的趨勢(吧?)

像這類型的活動,以往我是印成紙本發給學生用紙筆完成,但是計時或統計等等都有點麻煩,現在可以用線上完成,真是方便不少。

接下來是程式的部分,這個迷宮是用深度優先搜尋演算法(Depth-First-Search,DFS)來完成的。

首先建立一個填滿0的二維陣列,0代表牆,1代表可移動的空間。未來留下的牆一定在奇數行列的位置,所以接下來任選一個偶數行列的格子開始,選擇任意一個可以打通的方向,把牆打破,能打破代表牆的另一邊還沒有走過,而且不可以把最外圍四周的牆打破。

最後會走到一個格子是完全沒有可以打通的方向,此時就後退到有可以打通方向的格子。

用這個方式就可以建立一個完全歷遍的迷宮,但是這種DFS會產生一條很長一段沒有岔路的通道。所以在建立DFS迷宮之後,再隨機把幾面牆打掉。

最後得到一個處理過的二維陣列,再利用Arcade的內建功能,依據這個陣列建立畫面中的牆或是走道。

迷宮生成還有其他不同的演算法,我也試了用Prim演算法做最小生成樹的,生成一個不同形式的迷宮

https://makecode.com/_J0LMkxHb0Ma1

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

function initializeMaze(rows: number, cols: number): number[][] {
    let maze: number[][] = [];
    for (let i = 0; i < rows; i++) {
        let row: number[] = [];
        for (let j = 0; j < cols; j++) {
            row.push(0); // Fill each cell with 0 (representing a wall)
        }
        maze.push(row);
    }
    return maze;
}

function createVisitedArray(rows: number, cols: number): number[][] {
    let visited: number[][] = [];
    for (let i = 0; i < rows; i++) {
        let row: number[] = [];
        for (let j = 0; j < cols; j++) {
            row.push(0); // Fill each cell with 0 (representing unvisited)
        }
        visited.push(row);
    }
    return visited;
}



// 判斷當前格子是否有路可走
function ifHasRoute(x: number, y: number, maze: number[][], visited: number[][], nr: number, nc: number) {
    let directions = [];
    if (x - 2 > 0  && !visited[y][x - 2]) directions.push([-1,  0]);
    if (x + 2 < nc && !visited[y][x + 2]) directions.push([ 1,  0]);
    if (y - 2 > 0  && !visited[y - 2][x]) directions.push([ 0, -1]);
    if (y + 2 < nr && !visited[y + 2][x]) directions.push([ 0,  1]);

    if (directions.length > 0) {
        // 隨機return一個可走的方向
        return directions[Math.floor(Math.random() * directions.length)];
    } else {
        return [00]; // 沒有可走的路
    }
}


// DFS 生成迷宮的 function
function dfsMaze(maze: number[][], visited: number[][], nr:number, nc:number) {
    let route: number[][] = [];

    // 隨機選擇起始位置
    let x = Math.floor(Math.random() * ((nc - 1) / 2)) * 2 + 1;
    let y = Math.floor(Math.random() * ((nr - 1) / 2)) * 2 + 1;

    route.push([x, y]);
    visited[y][x] = 1;
    maze[y][x] = 1// 設置起始點為路徑

    let way = ifHasRoute(x, y, maze, visited, nr, nc);

    while (route.length > 0) {
        //console.log("" + x + "_" +y)
        if (way[0] === 0 && way[1] === 0) {
            // 無路可走則回退
            route.pop();
            if (route.length > 0) {
                x = route[route.length - 1][0];
                y = route[route.length - 1][1];
                way = ifHasRoute(x, y, maze, visited, nr, nc);
            }
        } else {
            // 拆除牆並移動
            maze[y + way[1]][x + way[0]] = 1// 打開牆
            x = x + 2 * way[0];
            y = y + 2 * way[1];

            maze[y][x] = 1// 新的格子成為路徑
            visited[y][x] = 1;
            route.push([x, y]);
            way = ifHasRoute(x, y, maze, visited, nr, nc); // 繼續新路徑
        }
    }
}


// 隨機打開牆,形成額外的路徑
function openRandomWall(maze: number[][], nr: number, nc: number) {
    let x = Math.floor(Math.random() * ((nc - 1) / 2)) * 2 + 1;
    let y = Math.floor(Math.random() * ((nr - 1) / 2)) * 2 + 1;

    let possibleDirections = [];
    if (x - 2 > 0  && maze[y][x - 1] === 0) possibleDirections.push([-10]); // 左邊
    if (x + 2 < nc && maze[y][x + 1] === 0) possibleDirections.push([10]); // 右邊
    if (y - 2 > 0  && maze[y - 1][x] === 0) possibleDirections.push([0, -1]); // 上邊
    if (y + 2 < nr && maze[y + 1][x] === 0) possibleDirections.push([01]); // 下邊

    if (possibleDirections.length > 0) {
        let direction = possibleDirections[Math.floor(Math.random() * possibleDirections.length)];
        maze[y + direction[1]][x + direction[0]] = 1// 打開牆
    }
}

function generateMaze(nr: number, nc: number): number[][] {
    let maze: number[][] = initializeMaze(nr, nc);  // Initialize maze (0 means wall, 1 means path)
    let visited: number[][] = createVisitedArray(nr, nc);  // Record whether each cell has been visited
    
    // Generate maze using DFS
    dfsMaze(maze, visited, nr, nc);

    // Open additional walls to create multiple paths
    let additionalPaths = Math.floor((nr/10) * (nc/10));  // Open some additional walls
    for (let i = 0; i < additionalPaths; i++) {
        openRandomWall(maze, nr, nc);
    } 
    return maze;
}


function renderMaze(maze: number[][]){
    for (let y = 0; y < maze.length; y++) {
        for (let x = 0; x < maze[y].length; x++) {
            if(maze[y][x] === 0){
                tiles.setTileAt(tiles.getTileLocation(x, y), sprites.dungeon.floorLight0)
                tiles.setWallAt(tiles.getTileLocation(x, y), true)
            }
            else{
                tiles.setTileAt(tiles.getTileLocation(x, y), assets.tile`transparency16`)
            }
        }
    }
}

function print(array:number[][]){
    for (let i = 0; i < array.length; i++) {
        console.log(array[i])
    }
    console.log("---------------------------")
}
function main(){

    game.showLongText("終點在最右一欄,請快速找到它。\n每次到達之後會被送回原點,再走一次\n試試看你能不能越走越快"DialogLayout.Bottom)
    tiles.setCurrentTilemap(tilemap`map`)
    const nr = 39;
    const nc = 39;
    let maze = generateMaze(nr, nc); 

    let timeArray:number[] = [];
    
    
    renderMaze(maze);
    // 設定主角
    let mySprite = sprites.create(img`
        b b b b b b b b
        1 1 1 5 5 1 1 1
        1 f 1 5 5 1 f 1
        1 1 1 5 5 1 1 1
        5 5 5 5 5 5 5 5
        5 5 5 5 5 5 5 5
        b b 5 5 5 5 b b
        . b b b b b b .
    `SpriteKind.Player);
    let spritePosY = Math.floor(Math.random() * ((nr - 1) / 2)) * 2 + 1;
    tiles.placeOnTile(mySprite, tiles.getTileLocation(1, spritePosY));
    scene.cameraFollowSprite(mySprite);
    controller.moveSprite(mySprite, 200200);


    let startTime = game.runtime();
    let endTime;

    let food = sprites.create(img`
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
        2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
    `SpriteKind.Food);
    let foodPosY = Math.floor(Math.random() * ((nr - 1) / 2)) * 2 + 1;
    tiles.placeOnTile(food, tiles.getTileLocation(nc - 2, foodPosY));

    //主角到達終點
    sprites.onOverlap(SpriteKind.PlayerSpriteKind.Foodfunction(sprite: Sprite, otherSprite: Sprite) {
        endTime = game.runtime();
        timeArray.push(endTime - startTime);
        tiles.placeOnTile(mySprite, tiles.getTileLocation(1, spritePosY));
        let timeText = "";
        for(let i = 0; i< timeArray.length; i++){
            if (i == 0) { timeText = (1) + "-> " + (timeArray[0]/1000)}
            else{
            timeText +=  "\n" + (i +1) +"-> "  + (timeArray[i]/1000); 
            }
        }
        //game.splash(timeText);
        game.showLongText(timeText, DialogLayout.Bottom)
        startTime = game.runtime();        

    })

    
    
}

main();


2024年9月25日

電腦以藍牙直接控制micro:bit的LED

延續上一篇,繼續用電腦藍牙直接控制micro:bit,這次直接用軟體來控制,就不自己寫程式。

先安裝 Bluetooth LE Explorer

https://apps.microsoft.com/detail/9n0ztkf1qd98?hl=zh-tw&gl=US


在micro:bit上,只開啟[藍牙LED服務]


開啟 Bluetooth LE Explorer,掃描周圍的BLE裝置,我的micro:bit就在電腦旁邊,所以前幾個就能找到

點下去就連線了,看到一堆代碼應該不知從何下手

這時候又需要看 micro:bit的 Profile了

Bluetooth Developer Studio Level 3 Profile Report


LED服務的UUID是 E95DD91D251D470AA062FA1922DFA9A8


這個SERVICE裡有三個CHARACTERISTICS
  • E95D7B77251D470AA062FA1922DFA9A8 LED Matrix State
  • E95D93EE251D470AA062FA1922DFA9A8 LED Text
  • E95D0D2D251D470AA062FA1922DFA9A8 Scrolling Delay


在Bluetooth LE Explorer裡應該會在最底下看到這三個東西,先以第一個做例子,這就是讀取或寫入LED的狀態。

row 1 是第1個byte
row 2 是第2個byte
row 3 是第3個byte
row 4 是第4個byte
row 5 是第5個byte

0和1代表LED的關和開,以row 1為例
全關00000,用十六進位表示為00。
只開最右邊那個燈是00001,十六進位是01
只開最左邊那個燈是10000,十六進位是10
全開是11111,十六進位為1F

如果你了解上面的規則後,就把每一row的LED狀態寫入就能控制LED了

舉例來說愛心的圖案長這樣
如果是二進位就是輸入 01010-11111-11111-01110-00100
換成十六進位就是0A-1F-1F-0E-04




接下來是控制LED Text,記得改成用UTF8。只要輸入文字就可以用藍牙控制micro:bit顯示文字了。




最後一個Scrolling Delay則是控制捲動文字的時間,預設120ms,因為是Little-Endian,所以十六進位是7800,如果要更改成100,就用HEX輸入6400,這個就是100的十六進位,如果要捲超快,1ms的話,就是輸入0100。


使用python 讓電腦直接與micro:bit藍牙連線傳送加速度資料

如果需要利用micro:bit傳輸資料,例如感測加速度數值,然後需要傳到電腦上。做法上都是用第一台感測資料後,用廣播的方式傳送資料,第二台micro:bit一邊接收廣播,然後用序列寫入的方式傳進電腦,然後電腦再用這些資料做處理,這種算是很簡單的作法。

只用一台micro:bit可不可以直接把資料傳進電腦呢?是可以的,就是用藍牙,只是喔,過程有點麻煩。

首先micro:bit的晶片上,廣播和藍牙不可以同時使用,用了藍牙就不能用廣播。先在擴展上找bluetooth,點選第一個擴展積木。


隨後會出現一個對話視窗,告訴你它會移除廣播radio積木


接下來的程式就只有在當啟動時,開啟[藍牙加速度計服務]
為了方便連接,再到右上角齒輪點一下

為了連接方便,可以設定連接方式為第一個[不須配對]或是第二個[自動配對]都可以




接下來是電腦程式的撰寫,我的目標是希望直接拿到micro:bit的加速度資料,然後傳到電腦上自動繪圖。
其中有寫到micro:bit的加速度計資料的UUID 是 E95DCA4B251D470AA062FA1922DFA9A8


所以基本流程就是去找到先掃描找到micro:bit的,然後去讀藍牙資料裡的acc資料

這支程式就是做這件事情,先掃描周遭的藍牙裝置,選擇之後就可以連線取得加速度資料。

第一支程式成功後。我記錄micro:bit的address,就可以跳過掃描的過程,直接連線取得加速度資料來繪圖。
用的是這支程式 




試過這個專案後,再看到micro:bit還可以直接用藍牙傳送以下資料,我覺得日後自己要做一些裝置應該是蠻有發展性的,不然現在很多micro:bit的藍牙服務都還要用別人寫好的手機app才能串接。




下一篇再來寫一個另外的方式用電腦軟體與micro:bit直接藍牙對接,用的是這套軟體:

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>