2019年10月18日 星期五

小米電視定時開關


早前公司為了前台的小米電視買了一台 Raspberry Pi 4,用它來收集不同的數據,如:最新天氣、即時新聞、巴士到站資料、新同事宣佈、歡迎訪客參觀、手工藝工作坊資料⋯⋯等。我們叫它電子報告板。它是在星期一至五的上班時間才會開著,需要不同的同事每天開啟及關閉,遇上忘記關機又在星期五的話,電視長開兩天實在是一種浪費。要解決這個問題,得找出讓小米電視開機關機的方法。

小米電視遙控是以藍牙方式溝通,找過這邊的方法,能成功連接,可是溝通的指令卻無從得知。由於它是 Android 系統,所以只要它有連到內聯網或互聯網,就能透過 http 地址控制:
http://{ip-address}:6095/controller?action={action}

它不是所有指令都有,但我們可以模擬遙控按鍵來達到想要的效果。以下是一些指令:
指令動作
controller?action=keyevent&keycode=power按下電源鍵
controller?action=keyevent&keycode=menu按下選單鍵
controller?action=keyevent&keycode=enter按下確定鍵
controller?action=keyevent&keycode=left按下左鍵
controller?action=keyevent&keycode=right按下右鍵
controller?action=keyevent&keycode=volumeup調大聲
controller?action=keyevent&keycode=bolumedown調細聲
controller?action=changesource&source=hdmi1轉換成 HDMI 1 頻道
controller?action=changesource&source=hdmi2轉換成 HDMI 2 頻道

2019年10月16日 星期三

換臉程式



一直以來對 DeepFake 都感到興趣,如果技術成熟,分分鐘可以讓 Sita 在數碼世界重生。這幾天的起心肝埋首研究,總算有一點成績。我的換臉程式先從相片中尋找出 68 個座標點的面孔,然後把座標點以 Delaunay triangulation 方式連成一起,把兩張相片的相同位置三角形抽出來變形,再組合成新的面孔;這裡很容易導致臉孔出現白邊三角形,只要用 cv2.max() 取代 cv2.add() 就能解決。之後把輸出相片的原有面孔刪去變成黑色。最後以 OpenCV 的 Seamless Clone 放入新面孔。

2019年10月14日 星期一

在 iPhone XR 上讀取八達通餘額


最近有個 App 能透過在 iPhone 上的 NFC 模組讀取八達通餘額。感到很神奇,也想動手做做看。

事緣是 iOS 13 鬆綁了對 NFC 的限制,開發人員能讀取更多不同款式的 NFC 卡。按照蘋果的範例去做,只需要幾個步驟便能完成程式。可是,當在 iPhone XR 上執行程式,放上八達通,一點反應也沒有。以為是數據格式問題,加入 Breakpoint 同樣也沒有反應。是連檢測也沒有發生。

經過多翻研究及嘗試,知道八達通是使用 Felica 系統 Type-F 制式,需要用上 NFCTagReaderSession 而非 NFCNDEFReaderSession。同時在 Info.plist 內要加入:
<key>com.apple.developer.nfc.readersession.felica.systemcodes</key>
<array>
 <string>0003</string>
 <string>8008</string>
</array>
在 NFCTagReader.entitlements 中也要加入:
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
 <string>NDEF</string>
 <string>TAG</string>
</array>
八達通的系統代號為 8008。服務代碼為 0x0117。有了這些準確的設定才能成功讀取數據。數據頭 4 字元是沒有加密的八達通餘額。由於港幣餘額會有毫子出現,為了簡單快捷起見,八達通餘額會乘大 10 倍,即是數值 789 其實是 HK$78.9 餘額。還有是這個餘額包含了按金,所以實際餘額會是 HK$78.9 - HK$50 = $23.9。

2019年10月13日 星期日

大眾臉


在網上看到有人把不同臉孔圖片加疊在一起,造出一張大眾臉;這張臉不存在於世界上,至少在樣本收集的範圍內是沒有,感覺很神奇。教學道出了方法及流程,可是代碼卻沒有完整地公開,得自行處理遺漏缺失的地方。花了一段時間,我終於完整地編寫出一個「大眾臉」的 Python 程式。分別把公司 70 位男同事及 156 位女同事的樣貌合拼起來,就得出這張大眾臉。這兩張臉沒有在公司內出現過,是虛擬而像真的臉。很多同事問我:「當中有那一塊像我?」。說實在是有的,不過也溝淡了吧。說起來,這兩張臉挺有夫妻相 ^_^。

2019年10月12日 星期六

Instagram 貼文抓取程式


最近有一個念頭;希望抓取 Instagram 內的貼文數據,除了可以利用相片去訓練關於圖像的模型外,還可以訓練關於文字的模型。PIP 提供了一個很有用的「instagram-scraper」應用程式,可以抓取貼文相片,非常方便;只要稍為修改一下,便可以把貼文的文字數據也一併保存下來。安裝方法如下:
pip install instagram-scraper
安裝後,打開 /usr/local/lib/python3.7/site-packages/instagram-scraper/app.py 檔案;如上圖所示,加修第 975-978 行,儲存後就可以把貼文數據保存在 JSON 檔內。

2019年10月10日 星期四

製作 WiFi 二維碼


朋友到家作客,有可能會要求使用 WiFi 網絡,每每要自己幫忙輸入連線設定,確實有點麻煩;又或者你是小店東主,想讓客人使用店內 WiFi,把密碼標明出來好像不太安全,要是客人不懂設定,又會麻煩到店員。原來我們可以把 WiFi 設定變成一個 QR Code,顧客只需掃瞄一下便能連線上網,十分方便快捷。

要準備這樣的一個 QR Code,我們可以使用 https://qifi.org 這個工具。它會按你輸入的設定變成 QR Code 圖案。在 SSID 欄輸入你的 WiFi 名稱;然後選擇加密的方法,通常都是「WPA / WPA2」;再在密碼欄輸入你的 WiFi 密碼。按「Generate」便能產生 QR Code。它是一個單機的程式,載入後斷網仍然能夠使用。要是你仍然擔心它會在背後上傳登入資訊的話,只要利用其他的 QR Code 生成器,輸入以下格式:
WIFI:S:<SSID>;T:<WPA|WEP|>;P:<password>;;
也能生產出相同的二維碼。把二維碼裝飾一下再打印出來,貼在當眼處;朋友或顧客便能輕易地加入你的 WiFi 連線設定。

2019年9月29日 星期日

說說 Dark Mode


Dark Mode 暗黑模式,是在 macOS Mojave 引入,一年後的 iOS 13 也都加入。有不少人認為這個設計很革新,但其實它是一個必然發生的結果。為何會這樣說?大家都知道蘋果擅長把軟件和硬件深度配合,造出出色的產品。她們也著重環保及能源管理。暗黑模式不是憑空想像出來。我們得從硬件方面入手。

蘋果在 2019 年之前推出的 MacBook Air 或 MacBook Pro 是 LCD 制式;iPhone 8 或以前的手機也是 LCD,到 iPhone X 才開始 OLED。LCD 的顯像原理,是將液晶置於兩片導電玻璃之間,靠兩個電極間電場的驅動,引起液晶分子扭曲向列的電場效應,以控制光源透射或遮蔽功能,在電源開關之間產生明暗而將影像顯示出來,若加上彩色濾光片,則可顯示彩色影像。由於液晶本身不發光,所以 LCD 背後需要一大片白光來支持,肉眼才能看到顏色。在不通電的情況下,光能不受影響地直接輸出,亦即是白色;相反,要顯示黑色時,液晶需要分別在紅、綠、藍三色都通電,阻擋背光直出。因此,在 LCD 上顯示明亮色彩比暗淡色彩來得省電。如果見到 MacBook 上發光的蘋果標誌,就是因為 LED 使用了背光板。蘋果很巧妙地把硬件的特性應用到設計內。

至於 LED 就跟平常看到的 LED 燈一樣,它本身是發光物,每一顆像素都是一顆細小的燈。因此它不用背光板。為了追求纖薄,蘋果標誌也只好變回金屬圖案。所以,在 LED 上顯示明亮色彩比暗淡色彩來得耗電。不論是 MacBook 還是 iPhone,都是流動裝置,節能是重要的一環;在現今 LED 屏幕時代,推出 Dark Mode 一來可以成為行銷噱頭,也能節省能源;只是蘋果再一次巧妙地把硬件特性應用到設計中意料之內的事而已。

2019年9月8日 星期日

為中文字體檔瘦身

在開發網頁的過程中,尤其是希望用到特別的中文字體時,由於中文字體檔案動輒有數十 MB 大小,下載需要一點時間。因此通常都會以圖片代替文字。用圖片代替文字的短處是無法把文字拷貝、只有單一尺寸、修改起來麻煩...等。究竟有沒有其他方法?

如果能把用不著的文字從字體檔中刪除,檔案體積將會縮小,變成適合在網頁中加載。在 Node.js 中有一個名為「fontmin」的模組可以滿足到這個需求。在 Terminal 輸入:
npm install fontmin gulp-rename fs
它會安裝程式所需要的模組。

以下是我的瘦身程式:
//----------------------------------------------------------------------------------------
//  Font File Minifier
//----------------------------------------------------------------------------------------
//  Written by : Pacess HO
//  Copyright Pacess Studio, 2019.  All rights reserved.
//----------------------------------------------------------------------------------------

//  Require modules
var _GULPRENAME = require("gulp-rename");
var _FONTMIN = require("fontmin");
var _FS = require("fs");

//----------------------------------------------------------------------------------------
//  Global variables
var _now = new Date();
var year = _now.getFullYear().toString();
var month = _now.getMonth().toString();
var date = _now.getDate().toString();
var hours = _now.getHours().toString();
var minutes = _now.getMinutes().toString();
var seconds = _now.getSeconds().toString();

month = (month.length === 2) ? month : "0"+month;
date = (date.length === 2) ? date : "0"+date;
hours = (hours.length === 2) ? hours : "0"+hours;
minutes = (minutes.length === 2) ? minutes : "0"+minutes;
seconds = (seconds.length === 2) ? seconds : "0"+seconds;

//========================================================================================
//  Program start
console.log("\n##----------------------------------------------------------");
console.log("##  Font File Minifier");
console.log("##  Written by Pacess HO");
console.log("##  Copyright Pacess Studio, 2019.  All rights reserved.");
console.log("##----------------------------------------------------------\n");

//----------------------------------------------------------------------------------------
//  Reading parameters
var argu = process.argv.slice(2);
var inputFontFilename = "NotoSansTC-Medium.ttf";
var arguGlyphs = "";

for (var i=0; i<argu.length; i++)  {
   switch (argu[i])  {

      case "-f":
      case "--font":  {
         inputFontFilename = argu[i+1];
      }  break;

      case "-g":
      case "--glyphs":  {
         arguGlyphs = argu[i+1];
      }  break;
   }
}

if (arguGlyphs == "")  {

 //  No glyphs provided, show instruction
   console.log("Usage:");
   console.log("   node fontminifier.js -g <characters> -f <fontfile>\n");
   console.log("Options:");
   console.log("   --glyphs");
   console.log("   -g      = Characters that you want to keep in font file.\n");
   console.log("   --font");
   console.log("   -f      = Font source filename in TTF format with extension\n");
   console.log("Example:");
   console.log("   node fontminifier.js -g 0123456789 -f Arial.ttf\n");
   process.exit(1);
}

//----------------------------------------------------------------------------------------
//  Main process
console.log("Input font file: "+inputFontFilename);
console.log("Glyphs to be keep: "+arguGlyphs);

//  Font file name
var fontFilename = inputFontFilename.split(".");
var outputFilename = fontFilename[0]+"_"+year+month+date+hours+minutes+seconds+".ttf";

//  Set the files to be optimized
var fontPath = "./"+inputFontFilename;

//  Set the destination folder to where your files will be written
var outputPath = "./";

//  Check if font file is exists.
if (_FS.existsSync(fontPath) == false)  {

   //  If do not exists then will stop the process
   console.error("###  Font file not found...\n");
   process.exit(1);
}

//  Set up by fontmin
var fontmin = new _FONTMIN()
 .use(_GULPRENAME(outputFilename))
 .src(fontPath)
 .dest(outputPath);

if (arguGlyphs != "")  {
 console.log("\nProcessing...");
 fontmin.use(_FONTMIN.glyph({
  text: arguGlyphs,
  hinting: false
 }));
}

//  Start minifying font with the given settings.
fontmin.run(function(err, files)  {
 if (err)  {
  console.error("###  Something went wrong...\n");
  throw err;
 }

 console.log("Minify done: "+outputPath+outputFilename+"\n");
 process.exit(0);
});

讓我們來了解程式的運作。
//  Require modules
var _GULPRENAME = require("gulp-rename");
var _FONTMIN = require("fontmin");
var _FS = require("fs");
程式最開端是載入三個需要的模組。「gulp-rename」是用於更改檔案名稱;「fontmin」是字體瘦身的核心;「fs」主 要是檔案相關的處理。  
//  Global variables
var _now = new Date();
var year = _now.getFullYear().toString();
var month = _now.getMonth().toString();
var date = _now.getDate().toString();
var hours = _now.getHours().toString();
var minutes = _now.getMinutes().toString();
var seconds = _now.getSeconds().toString();

month = (month.length === 2) ? month : "0"+month;
date = (date.length === 2) ? date : "0"+date;
hours = (hours.length === 2) ? hours : "0"+hours;
minutes = (minutes.length === 2) ? minutes : "0"+minutes;
seconds = (seconds.length === 2) ? seconds : "0"+seconds;
向系統取得當刻的時間,並把數值分別設定在對應的變量中。  
var argu = process.argv.slice(2);
var inputFontFilename = "NotoSansTC-Medium.ttf";
var arguGlyphs = "";

for (var i=0; i<argu.length; i++)  {
   switch (argu[i])  {

      case "-f":
      case "--font":  {
         inputFontFilename = argu[i+1];
      }  break;

      case "-g":
      case "--glyphs":  {
         arguGlyphs = argu[i+1];
      }  break;
   }
}

if (arguGlyphs == "")  {

 //  No glyphs provided, show instruction
   console.log("Usage:");
   console.log("   node fontminifier.js -g <characters> -f <fontfile>\n");
   console.log("Options:");
   console.log("   --glyphs");
   console.log("   -g      = Characters that you want to keep in font file.\n");
   console.log("   --font");
   console.log("   -f      = Font source filename in TTF format with extension\n");
   console.log("Example:");
   console.log("   node fontminifier.js -g 0123456789 -f Arial.ttf\n");
   process.exit(1);
}
這段是讀取參數,如果是「-f」或「--font」就把數值儲存在「inputFontFilename」;如果是「-g」或「--glyphs」就把數值儲存在「arguGlyphs」。當沒有指定字體檔名時,會使用預設的「NotoSansTC-Medium.ttf」字體檔案。 由於這是一個瘦身程式,如果沒有指定保留哪些字元時,運作便會變得沒有意義,因此程式會停止執行。也有可能是用戶不清楚使用方法,所以同時會顯示使用說明。  
//  Main process
console.log("Input font file: "+inputFontFilename);
console.log("Glyphs to be keep: "+arguGlyphs);

//  Font file name
var fontFilename = inputFontFilename.split(".");
var outputFilename = fontFilename[0]+"_"+year+month+date+hours+minutes+seconds+".ttf";

//  Set the files to be optimized
var fontPath = "./"+inputFontFilename;

//  Set the destination folder to where your files will be written
var outputPath = "./";

//  Check if font file is exists.
if (_FS.existsSync(fontPath) == false)  {

   //  If do not exists then will stop the process
   console.error("###  Font file not found...\n");
   process.exit(1);
}
接下來是依照參數數值去準備好輸入路徑、輸出路徑、路徑檔名。同時也會檢查指定的字體檔案是否存在。沒有字體檔案會甚麼也做不來,這時就需要顯示錯誤訊息。  
//  Set up by fontmin
var fontmin = new _FONTMIN()
 .use(_GULPRENAME(outputFilename))
 .src(fontPath)
 .dest(outputPath);

if (arguGlyphs != "")  {
 console.log("\nProcessing...");
 fontmin.use(_FONTMIN.glyph({
  text: arguGlyphs,
  hinting: false
 }));
}
一切就緒,現在要建立核心模組,並把準備好的數值通知模組。  
//  Start minifying font with the given settings.
fontmin.run(function(err, files)  {
 if (err)  {
  console.error("###  Something went wrong...\n");
  throw err;
 }

 console.log("Minify done: "+outputPath+outputFilename+"\n");
 process.exit(0);
});
最後就是執行瘦身動作。如果當中出現錯誤時,就顯示錯誤訊息。成功的話則顯示輸出檔名。要執行程式,在「Terminal」輸入:  
node fontminifier.js -g 0123456789 -f phosphate.ttf
「-g」參數後面的是要保留的文字。「-f」參數後面的是輸入的字體檔案名稱;程式會在檔案名稱後加入當刻時間,作為 輸出的字體檔案名稱。留意:程式只支援 TTF 格式,TTC, OTF,...等是不支持。這段指令的意思是讀取 「phosphate.ttf」字體檔,只保留「0123456789」字符,其他的都刪除掉。完成後會有一個類似 「phosphate_YYYYMMDDHHIISS.ttf」的檔案出現;它就是瘦身後的字體檔案。

2019年8月28日 星期三

解決 Ubuntu 18 的 VNC 連線問題


最近在公司架設了一台 Ubuntu 伺服器,希望它可以跑 Web Server、PHP、在 Jupyter 執行 Python 及 PHP、Proxy 代理服務器、訓練機器學習模型,還有 VNC。然而,在 Ubuntu 18 下打開了屏幕分享,可是在 Mac 環境下連接卻出現版本錯誤及加密方法不被支援。問題是 Ubuntu 18 端的介面卻沒有加密選項。最後找到一個名為「dconf-editor」的工具,能顯示系統沒有公開的設定,只要把「require-encryption」關掉後,問題便能解決。

2019年3月2日 星期六

連接 PSVR 到 MacBook Pro


一兩個星期前,在 PS4 Pro 上安裝了 Littlstar 軟件去播放儲存在 USB 的立體影片。豈料今天在自動更新過後,這個免費的軟件現在只能播放兩分鐘,若要播放完整影片,則需要以訂購方式每月付費,或一次過給 US$39.9。對於 Littlstar 這個吸金方法,讀取 USB 影片要收費、官方內容又太少的情況下,很多人亦因此離開,尋找其他方法。其中一個方法是把 PSVR 連接到 macOS 上看。

方法是利用 https://github.com/emoRaivis/MacMorpheus 這個開源程式。先開著 PS4 Pro 及 PSVR,然後如上圖把線重新連接,這樣就能把 MacBook Pro 的畫面投射到 PSVR 上。