之前,為了教其他跟我一樣,沒受過正規資訊教育的人,如何使用 Azure,稍微做了一些功課,我把我曾經教過的內容整理一番,剛好拿來參加這次的鐵人賽。整份內容的主題都圍繞在如何利用 Azure 做一些小工具,以解決生活中的小問題。當初這樣設計,是因為我想告訴剛踏入程式世界的新手們,遇到問題的時候,可以盡量利用外部資源,讓自己快速得到成果。同時,也讓新手們明白,不單是工作上的問題可以用程式解決,生活中的疑難雜症都可以寫程式處理。而 Azure 正好給了一堆方便的工具,讓使用者可以針對自己的需求,快速解決。這聽起來雖然有點偷懶,但實際上,真的也沒有必要每個工具都由自己一一從零打造,Azure 也只是眾多解決方案的其中之一而已。
就像其他技藝課程一樣,我希望新手學完這些東西後,解開自己的人生成就任務,可以自己完成一個小小的作品,一個可以帶走的作品,所以我將這些 Azure 相關的內容,全部用一個簡單的 Line Chatbot 涵括在內了。
接下來的 30 天,我會帶領大家一步一步在 Azure 上完成這個 Chatbot,這個 Chatbot 會有四個基本功能:
基本起手式,能註冊的先註冊,能安裝的先安裝。
3. 驗證email及真人身份
4. 進入https://azure.microsoft.com/,按下免費試用Azure
5. 填妥個人資訊與信用卡資訊
申請免費試用時,我踩了幾次雷,因為我之前申請過免費試用,申請過就無法再次試用了。原本以為只要換一張信用卡就能再次申請,但失敗了。後來我換了電話號碼之後,又可以使用了,所以這個機制應該是以電話號碼作為 unique ID,同一組電話號碼只能申請一次免費試用。
sudo apt-get update
sudo apt-get install ca-certificates \
curl apt-transport-https lsb-release gnupg
curl -sL https://packages.microsoft.com/keys/microsoft.asc |
gpg --dearmor |
sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
AZ_REPO=$(lsb_release -cs)
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" |
sudo tee /etc/apt/sources.list.d/azure-cli.list
sudo apt-get update
sudo apt-get install azure-cli
brew update && brew install azure-cli
az login
可能會出現兩種情況,一是直接跳出瀏覽器視窗,讓你輸入帳號密碼登入,另一種情況是你是連進某一台server執行az login
,那他就會出現以下訊息:
還有一種情況,如果 Azure 發現使用者開瀏覽器的 IP 和執行 az login
的電腦不是同一個 IP,例如:你在家連線進到公司的電腦登入,這時會登入失敗。為了避免這種情況,可以執行az longin -i
,這樣就能直接在 terminal 輸入帳號密碼登入,不需要再開瀏覽器了。
在往後幾天裡,資源和資源群組都會不時都會出現,所以在這裡先做簡單的名詞解釋。
由於所有的資源都必須放入資源群組,所以,在這裡就先介紹如何建立資源群組。
標籤
,以便將來使用服務太多時,可以針對標籤搜尋檢閱+建立
,建立資源群組有些新手的電腦安裝的作業系統是 windows ,在後續各種有關 Linux 或 Unix 的操作會比較不方便。一般會會利用一些硬體虛擬化的軟體,安裝 Ubuntu ,在虛擬的 Ubuntu Linux 的環境內操作。不過,有時候可能反而不方便,我看過有些人,因為為了不同的需求,創建了不同的虛擬環境,佔用了不少資源跟空間。這邊提供另一個簡單的做法,我們可以直接在 Azure 上建立一個 Ubuntu Server 的虛擬機器,在此環境內執行後續幾天的操作。
以下簡單介紹幾個步驟,讓 python 的使用者,可以從建立虛擬機器開始,到可以簡單使用 jupyter notebook。
建立資源:搜尋Ubuntu Server 18.04 LTS
選擇虛擬機器規格:進入基本
設定畫面,大部分都是以預設值為主,但我為了要省錢選擇台幣500元左右的機器。
3. 基本
設定的中間,可以選擇連接埠,看需求決定,但方便起見,我是全都勾起來。
4. 基本
設定的下方,會讓你選擇驗證類型,指的是你希望用什麼方式登入。這邊就隨個人喜好設定了,我通常習慣使用密碼登入。
5. 建立好之後,在 https://portal.azure.com/#home 找到自己剛建立的虛擬機器
6. 在左手邊的選單,找到網路
,然後點選輸入連接埠規則
,按新增輸入連接埠規則
7. 因為之後要能夠使用jupyter notebook
,所以在目的地連接埠範圍
填上jupyter notebook
預設 port:8888,然後名稱填jupyter
,最後按新增
。
8. 另外,在這一頁也能看到公用IP,之後可以用此IP用ssh
連線。
執行ssh <your name>@<your public IP>
如果是Windwos使用者,就直接用PuTTY
連線吧~
接下來,連線之後,在虛擬機器內設定環境。
sudo apt-get update
sudo apt-get install python3.7
sudo apt install python3-pip
設定 pip3.7
alias pip3.7='python3.7 -m pip'
但每次登入,alias的設定都會失效,所以需要編輯 ~/.bashrc
,加入上述指令。
執行source ~/.bashrc
,讓上述設定立即生效。
安裝jupter
pip3.7 install jupyter
sudo apt install jupyter-notebook
pip3.7 install ipykernel --upgrade
python3.7 -m ipykernel install --user
執行jupyter
jupyter notebook --ip 0.0.0.0
這時應該會出現類似以下畫面:
從 Azure portal 進入虛擬機器的頁面,可從概觀看到公用 IP ,把公用 IP 接上上圖紅框內的內容(:8888?/token=.....
),在瀏覽器打開。這樣就可以透過jupyter進行各種測試或操作了。
另外,也可以試著在 Windows 安裝Windows Subsystem for Linux
(WSL
),這樣也可以直接在 Windows 上執行使用 Linux 系統,不需要額外使用虛擬機器軟體。安裝與使用方法,請點這裡,就不贅述了。# Azure 虛擬機器
有些新手的電腦安裝的作業系統是 windows ,在後續各種有關 Linux 或 Unix 的操作會比較不方便。一般會會利用一些硬體虛擬化的軟體,安裝 Ubuntu ,在虛擬的 Ubuntu Linux 的環境內操作。不過,有時候可能反而不方便,我看過有些人,因為為了不同的需求,創建了不同的虛擬環境,佔用了不少資源跟空間。這邊提供另一個簡單的做法,我們可以直接在 Azure 上建立一個 Ubuntu Server 的虛擬機器,在此環境內執行後續幾天的操作。
以下簡單介紹幾個步驟,讓 python 的使用者,可以從建立虛擬機器開始,到可以簡單使用 jupyter notebook。
建立資源:搜尋Ubuntu Server 18.04 LTS
選擇虛擬機器規格:進入基本
設定畫面,大部分都是以預設值為主,但我為了要省錢選擇台幣500元左右的機器。
3. 基本
設定的中間,可以選擇連接埠,看需求決定,但方便起見,我是全都勾起來。
4. 基本
設定的下方,會讓你選擇驗證類型,指的是你希望用什麼方式登入。這邊就隨個人喜好設定了,我通常習慣使用密碼登入。
5. 建立好之後,在 https://portal.azure.com/#home 找到自己剛建立的虛擬機器
6. 在左手邊的選單,找到網路
,然後點選輸入連接埠規則
,按新增輸入連接埠規則
7. 因為之後要能夠使用jupyter notebook
,所以在目的地連接埠範圍
填上jupyter notebook
預設 port:8888,然後名稱填jupyter
,最後按新增
。
8. 另外,在這一頁也能看到公用IP,之後可以用此IP用ssh
連線。
執行ssh <your name>@<your public IP>
如果是Windwos使用者,就直接用PuTTY
連線吧~
接下來,連線之後,在虛擬機器內設定環境。
sudo apt-get update
sudo apt-get install python3.7
sudo apt install python3-pip
設定 pip3.7
alias pip3.7='python3.7 -m pip'
但每次登入,alias的設定都會失效,所以需要編輯 ~/.bashrc
,加入上述指令。
執行source ~/.bashrc
,讓上述設定立即生效。
安裝jupter
pip3.7 install jupyter
sudo apt install jupyter-notebook
pip3.7 install ipykernel --upgrade
python3.7 -m ipykernel install --user
執行jupyter
jupyter notebook --ip 0.0.0.0
這時應該會出現類似以下畫面:
從 Azure portal 進入虛擬機器的頁面,可從概觀看到公用 IP ,把公用 IP 接上上圖紅框內的內容(:8888?/token=.....
),在瀏覽器打開。這樣就可以透過jupyter進行各種測試或操作了。
另外,也可以試著在 Windows 安裝Windows Subsystem for Linux
(WSL
),這樣也可以直接在 Windows 上執行使用 Linux 系統,不需要額外使用虛擬機器軟體。安裝與使用方法,請點這裡,就不贅述了。
Azure Web App 提供一個方便部署服務的做法,使用者可以透過git
來部署。之後要製作的 chatbot ,會直接部署在 Azure Web App 。部署之後,Azure Web App 會將使用者的程式,打包成docker image
,在虛擬機器中展開成container
執行。
# 在終端機中輸入
az webapp deployment user set \
--user-name <username> --password <password>
# 取得在Azure Web App內的git url
az webapp deployment source config-local-git \
--name <webappname> --resource-group <yourResourceGroup>
git push
到 Azure Web App 就是以此帳號密碼登入。# 在終端機中輸入
az webapp deployment source config-local-git \
--name <webappname> --resource-group <yourResourceGroup>
# 得到 URL
{
"url": "https://<username>@<webappname>.scm.azurewebsites.net/linecv.git"
}
git remote add azure <your_git_url>
git push azure master
az webapp log tail \
--name <webappname> --resource-group <yourResourceGroup>
App Service 紀錄
進入設定頁面。檔案系統
,分別調整配額(MB)和保留期限(天)。tunnel
,並提供某個 port 和固定的帳號密碼(root/Docker!),以便連線連線。az webapp create-remote-connection \
-n <你的Web App名稱> --resource-group <你的資源群組> &
ssh root@127.0.0.1 -p 57281
如果推上去的程式碼有問題,無法啟動,那就無法建立連線,甚至已經建立的連線也會斷掉。這時,請重新上傳可以執行的程式碼,程式碼可以執行後,連線才會暢通。
Flask
網頁建立好 Web App 之後,可以先部署簡單的Flask
,來看看效果。
Flask
網頁會需要兩個檔案,一個是要部署的python
檔案,另一個是requirements.txt
,讓系統在部屬的同時,可以安裝需要用到的python
套件。
application.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
"hello world"
return "Hello World!!!!!"
requirements.txt
Flask
git
將這兩個檔案上傳。概觀
看到。如果部署成功,我們就可以開始下一個步驟~打造自己的chatbot
接下來,如果要在 Azure Web App 上打造 chatbot ,那就必須會用到 Line Massaging API ,透過 Line Massaging API 才能讓 chatbot 與使用者溝通。
webhook event
到 chatbot server點選 Basic Setting:Channel Secret
點選 Messaging API:按下 issue
,得到Channel access token
pip3 install line-bot-sdk
把之前的 "hello world" 的Flask
網頁改寫成以下的樣子,並且部署到 Azure Web App ,就能讓 chatbot 與 Line Platform 溝通。
application.py
from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
app = Flask(__name__)
LINE_SECRET = "YOUR line secret"]
LINE_TOKEN = "YOU line_token"
LINE_BOT = LineBotApi(LINE_TOKEN)
HANDLER = WebhookHandler(LINE_SECRET)
@app.route("/callback", methods=["POST"])
def callback():
# X-Line-Signature: 數位簽章
signature = request.headers["X-Line-Signature"]
print(signature)
body = request.get_data(as_text=True)
print(body)
try:
HANDLER.handle(body, signature)
except InvalidSignatureError:
print("Check the channel secret/access token.")
abort(400)
return "OK"
接著就要到剛剛建立的 Messaging API channel ,設定webhook
。
在application.py
加上以下這一段,就可以讓chatbot學你說話。透過 HANDLER
,可以辨別 chatbot 接受到的訊息種類,基本的訊息種類有:
我們可以讓 chatbot 針對接收到的訊息種類做相對應的動作,在這邊是針對文字訊息做處理。收到文字訊息後,如果有收到特定文字,便給予特定答覆,其餘則直接學對方說話,回覆相同的文字。要回覆給使用者的字串需要經由 TextMessage
包裝成物件之後,才能透過 reply_message
回覆給使用者。
from linebot.models import (
MessageEvent,
TextMessage,
TextSendMessage,
)
# message 可以針對收到的訊息種類
@HANDLER.add(MessageEvent, message=TextMessage)
def handle_message(event):
url_dict = {
"ITHOME":"https://www.ithome.com.tw/",
"HELP":"https://developers.line.biz/zh-hant/docs/messaging-api/"}
# 將要發出去的文字變成TextSendMessage
try:
url = url_dict[event.message.text.upper()]
message = TextSendMessage(text=url)
except:
message = TextSendMessage(text=event.message.text)
# 回覆訊息
LINE_BOT.reply_message(event.reply_token, message)
雖然已經能讓 Line chatbot 回話了,但考慮到之後要做的功能,有必要幫 chatbot 回覆的內容美化一下。Flex message 就能做到美化的效果,透過 JSON 格式編輯版面,可以客製化的互動對話介面,適用於各種電子載體。
Flex message 完整的結構,可以視情況選擇哪一個部分要放什麼東西,甚至可以省略不需要的部分:
View as JSON
,複製內容,存成 JSON 檔以下稍微介紹 Flex message 內的零件:
用 python 讀 JSON 檔,依情境修改內容,透過FlexSendMessage
送出
範例:
with open("templates/detect_result.json", "r") as f_r:
bubble = json.load(f_r)
f_r.close()
# 依情況更動 components
bubble["body"]["contents"][0]["contents"][0]["text"] = output
bubble["header"]["contents"][0]["contents"][0]["url"] = link
LINE_BOT.reply_message(
event.reply_token,
[
FlexSendMessage(alt_text="Report", contents=bubble)
]
)
有了這工具之後,我們就可以把之後使用 Azure 認知服務的結果包裝成 Flex message 發送給使用者了。
Azure 認知服務透過 API 或 SDK 提供 AI 服務,讓使用者不需要有人工智慧或者資料科學的基本能力,就能透過 Azure 認知服務,使用各式各樣的 AI 模型解決問題。分為五大類:
之前所提到的Azure Web App
算是整個 chatbot 的平台,在Azure Web App
這個平台,透過 API 或 SDK 與其他雲端服務溝通,得到相對應的結果。而 Azure 的認知服務,在這次示範的 chatbot 中就扮演重要的角色,只要會基本的python
,就能輕鬆串接各項 AI 服務。大致上的流程,如上圖與以下的說明:
Flask
架設 chatbot serverAzure Blob
, 取得 影像 URL第一步當然還是要建立相對應的 Azure 認知服務資源,這部分就要去看說明文件才會知道有哪些資源,每個資源包含了哪些服務。以這次 chatbot 所設定的功能來看,以下列出需要建立認知服務的資源,以及其資源涵蓋的功能:
大部分的認知服務的使用方式都十分雷同,大致上的流程如下:
python
套件。以下列出 chatbot 會用到的套件,可以先放到requirements.txt
之中備用:azure-cognitiveservices-vision-computervision
azure-cognitiveservices-vision-face
azure-cognitiveservices-speech
接下來,幾天就會針對認知服務的使用與整合加以一一說明。
物體偵測主要就做兩件事情
python
套件pip3 install azure-cognitiveservices-vision-computervision
"""
Azure object detection
"""
import os
from io import BytesIO
import requests
from PIL import Image, ImageDraw, ImageFont
from azure.cognitiveservices.vision.computervision import ComputerVisionClient
from msrest.authentication import CognitiveServicesCredentials
# 匯入必要套件,主要都是跟讀檔、繪圖和 Azure 的相關套件
# 一開始除了匯入套件以外,還需要利用金鑰SUBSCRIPTION_KEY和端點ENDPOINT,取得使用電腦視覺服務的權限。
SUBSCRIPTION_KEY = os.getenv("SUBSCRIPTION_KEY")
ENDPOINT = os.getenv("ENDPOINT")
CV_CLIENT = ComputerVisionClient(
ENDPOINT, CognitiveServicesCredentials(SUBSCRIPTION_KEY)
)
def main():
"""
Azure object detection
"""
# 透過圖片的URL取得圖片
url = "https://i.imgur.com/Js5H6Qa.jpg"
response = requests.get(url)
img = Image.open(BytesIO(response.content))
# 開始設定繪圖相關的部分,由於會需要在圖片上寫字,需要準備字型檔
draw = ImageDraw.Draw(img)
font_size = int(5e-2 * img.size[1])
fnt = ImageFont.truetype("../static/TaipeiSansTCBeta-Regular.ttf", size=font_size)
# 透過電腦視覺的功能取得物件,偵測的結果會包含匡出物體的左上角座標(x, y),以及方匡的寬跟高(w, h),過這四個值即可畫出方匡,並且標示辨識結果以及辨識的信心程度。
object_detection = CV_CLIENT.detect_objects(url)
if len(object_detection.objects) > 0:
for obj in object_detection.objects:
left = obj.rectangle.x
top = obj.rectangle.y
right = obj.rectangle.x + obj.rectangle.w
bot = obj.rectangle.y + obj.rectangle.h
name = obj.object_property
confidence = obj.confidence
print("{} at location {}, {}, {}, {}".format(name, left, right, top, bot))
draw.rectangle([left, top, right, bot], outline=(255, 0, 0), width=3)
draw.text(
[left, top + font_size],
"{0} {1:0.1f}".format(name, confidence * 100),
fill=(255, 0, 0),
font=fnt,
)
# 最後存檔
img.save("output.png")
print("Done!")
print("Please check ouptut.png")
if __name__ == "__main__":
main()
由於目前 Azure 電腦視覺的功能都只能輸入圖片的 URL ,無法從自己的電腦輸入圖片,所以必須要找個地方上傳圖片,以便取得 URL 。接下來,我們可以利用 Azure Blob 來取得圖片的連結。
Azure Storage Account 是利用 blob 來儲存資料的空間,而 blob 則是一種可以儲存大量文字或二進位資料的物件,資料可以是各種影音、文件或壓縮檔案。在申請建立資源時,資源名稱為 Storage Account (儲存體帳戶),但在相關說明文件是在 Azure Blob 的說明文件之中。(謎之聲:用Imgur
的 API 也可以啊~~)
python
套件pip3 install azure-storage-blob
取得串接字串才能用 azure blob python
套件存取 blob。
存取金鑰
顯示金鑰
連接字串
container client
,並設定權限。import os
from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient
import json
import requests
from azure.storage.blob import AccessPolicy, ContainerSasPermissions
# 這邊就是填剛剛複製的連接字串
connect_str = 'Your connect string'
blob_service_client = BlobServiceClient.from_connection_string(connect_str)
# 自己命名 container 名稱,建立 container client
container_client = blob_service_client.create_container(<container name>)
# 設定 blob 讀寫權限,方便之後使用檔案的URL
access_policy = AccessPolicy(permission=ContainerSasPermissions(read=True, write=True))
identifiers = {'both': access_policy}
container_client.set_container_access_policy(identifiers, public_access='blob')
with open('your.jpg', "rb") as data:
blob_client.upload_blob(data)
data.close()
# 印出檔案的 URL
print(blob_client.url)
在之後為了方便起見,可以把上述程式碼做成 function ,整合到 chatbot。接下來跟電腦視覺相關的服務,都可以搭配 blob 的功能來使用。下一篇,我們來看圖說故事。
a cat sleeping on a wooden structure
Image Description 影像描述,顧名思義是利用電腦視覺分析影像,產生出人類看得懂的句子,以描述圖片中的內容。通常這樣的作法被稱為 Image Captioning,意味著幫影像下標題。其原理大致上是以 Convolutional Neural Network- CNN,作為 Encoder 擷取圖片中的特徵,在透過 Recurrent neural network- RNN,作為 Decoder 生成文句。
這邊用的金鑰和端點跟之前物體偵測所用的是一樣的。
# 套件:azure-cognitiveservices-vision-computervision
from azure.cognitiveservices.vision.computervision \
import ComputerVisionClient
from msrest.authentication import (
CognitiveServicesCredentials
)
# 利用金鑰SUBSCRIPTION_KEY和端點ENDPOINT,取得使用電腦視覺服務的權限。
SUBSCRIPTION_KEY = "YOUR SUBSCRIPTION_KEY"
ENDPOINT = "YOUR ENDPOINT"
CV_CLIENT = ComputerVisionClient(
ENDPOINT, CognitiveServicesCredentials(SUBSCRIPTION_KEY)
)
# 利用 describe_image 取得描述影像的句子與信心程度
description_results = CV_CLIENT.describe_image(url)
output = ""
for caption in description_results.captions:
output += "'{}' with confidence {:.2f}% \n".format(
caption.text, caption.confidence * 100
)
print(output)
把物體偵測和影像描述結合在一起,加到 chatbot server 之中,就可以得到以下效果。下一篇,我們就可以把之前所說的各項功能綜合起來,完成可以看圖說故事的 chatbot。
大致上的概念是要利用 Line 把圖片傳給 chatbot ,再把圖片傳到 Azure 認知服務,分別執行 object detection 和 image captioning,匡出圖片中的物體,並且描述圖片內容。
這邊要注意的是,有時候新手會直接把密碼、金鑰和端點直接放進去程式碼中,然後直接推到 Azure Web App,甚至直接推到 githib ,我想這不是一個好的習慣。雖然 Azure Web App 的使用者是自己,不是公開的情況,但如果是放到 github 的話,那自己的密碼、金鑰和端點就全曝光了。所以,最好還是養成好習慣,不要直接將自己的金鑰和密碼以明碼的方式放在自己的程式碼中上傳。
這裡介紹一個簡單的做法:另外開一個 json 檔案config.json
,把一些比較敏感的變數放進此檔案中,在上傳到 Azure Web App。
config.json
{
"line": {
"line_secret": "your line secret",
"line_token": "your line token",
},
"azure": {
"cv_key": "your subscription key of computer vision",
"cv_end": "your endpoint of computer vision",
"blob_connect": "your connect string",
"blob_container": "your blob container name"
}
az webapp create-remote-connection \
-n linecv --resource-group Tibame &
scp
上傳config.json
,這邊要注意只能上傳到/home
,這樣在 Web App 的 chatbot 才能讀到此檔案。scp -P <port> config.json ben@127.0.0.1:/home/config.json
Python
套件requirements.txt
Flask==1.0.2
line-bot-sdk
azure-cognitiveservices-vision-computervision
azure-storage-blob
Pillow
application.py
import os
import json
from flask import Flask, request, abort
from azure.cognitiveservices.vision.computervision import ComputerVisionClient
from azure.storage.blob import BlobServiceClient
from msrest.authentication import CognitiveServicesCredentials
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
MessageEvent,
FlexSendMessage,
ImageMessage,
)
from PIL import Image, ImageDraw, ImageFont
APP = Flask(__name__)
CONFIG = json.load(open("/home/config.json", "r"))
SUBSCRIPTION_KEY = CONFIG["azure"]["cv_key"]
ENDPOINT = CONFIG["azure"]["cv_end"]
CV_CLIENT = ComputerVisionClient(
ENDPOINT, CognitiveServicesCredentials(SUBSCRIPTION_KEY)
)
CONNECT_STR = CONFIG["azure"]["blob_connect"]
CONTAINER = CONFIG["azure"]["blob_container"]
BLOB_SERVICE = BlobServiceClient.from_connection_string(CONNECT_STR)
LINE_SECRET = CONFIG["line"]["line_secret"]
LINE_TOKEN = CONFIG["line"]["line_token"]
LINE_BOT = LineBotApi(LINE_TOKEN)
HANDLER = WebhookHandler(LINE_SECRET)
@APP.route("/")
def hello():
"hello world"
return "Hello World!!!!!"
# 為了上傳圖片到 Azure blob
def upload_blob(container, path):
"""
Upload files to Azure blob
"""
blob_client = BLOB_SERVICE.get_blob_client(container=container, blob=path)
with open(path, "rb") as data:
blob_client.upload_blob(data, overwrite=True)
data.close()
return blob_client.url
# 影像描述
def azure_describe(url):
"""
Output azure image description result
"""
description_results = CV_CLIENT.describe_image(url)
output = ""
for caption in description_results.captions:
output += "'{}' with confidence {:.2f}% \n".format(
caption.text, caption.confidence * 100
)
return output
# 物體偵測
def azure_object_detection(url, filename):
"""
Azure object detection, and output images with bounding boxes
"""
img = Image.open(filename)
draw = ImageDraw.Draw(img)
font_size = int(5e-2 * img.size[1])
fnt = ImageFont.truetype("static/TaipeiSansTCBeta-Regular.ttf", size=font_size)
object_detection = CV_CLIENT.detect_objects(url)
if len(object_detection.objects) > 0:
for obj in object_detection.objects:
left = obj.rectangle.x
top = obj.rectangle.y
right = obj.rectangle.x + obj.rectangle.w
bot = obj.rectangle.y + obj.rectangle.h
name = obj.object_property
confidence = obj.confidence
print("{} at location {}, {}, {}, {}".format(name, left, right, top, bot))
draw.rectangle([left, top, right, bot], outline=(255, 0, 0), width=3)
draw.text(
[left, top + font_size],
"{} {}".format(name, confidence),
fill=(255, 0, 0),
font=fnt,
)
img.save(filename)
# 把畫完方框的圖片傳至 blob
link = upload_blob(CONTAINER, filename)
# 為了避免一堆圖檔塞爆 Web App,用完就刪掉
os.remove(filename)
return link
# Callback for Line chatbot
@APP.route("/callback", methods=["POST"])
def callback():
"""
LINE bot webhook callback
"""
# get X-Line-Signature header value
signature = request.headers["X-Line-Signature"]
print(signature)
body = request.get_data(as_text=True)
print(body)
try:
HANDLER.handle(body, signature)
except InvalidSignatureError:
print(
"Invalid signature. Please check your channel access token/channel secret."
)
abort(400)
return "OK"
# 處理影像訊息
@HANDLER.add(MessageEvent, message=ImageMessage)
def handle_content_message(event):
"""
Reply Image message with results of image description and objection detection
"""
# event 是使用者與 Line 之間的互動事件,可以印出 event 的物件,觀察 event。
# message ID 會作為後續圖片存檔的名稱, user ID 則是作為之後人臉登入的依據。
print(event.message)
print(event.source.user_id)
print(event.message.id)
# 讀取為了產生 flex message 的 json 檔
with open("templates/detect_result.json", "r") as f_h:
bubble = json.load(f_h)
f_h.close()
# 以 Line message ID 作為檔案名稱
filename = "{}.jpg".format(event.message.id)
# 圖片訊息以 binary 的形式傳輸,取得之後先存成圖檔
message_content = LINE_BOT.get_message_content(event.message.id)
with open(filename, "wb") as f_h:
for chunk in message_content.iter_content():
f_h.write(chunk)
f_h.close()
# 開啟檔案,為了取得影像指寸
img = Image.open(filename)
# 上傳圖片到 Blob
link = upload_blob(CONTAINER, filename)
# 執行物體偵測和影像描述
link_ob = azure_object_detection(link, filename)
output = azure_describe(link)
link = link_ob
# 以 flex message 輸出結果
bubble["body"]["contents"][0]["text"] = output
bubble["header"]["contents"][0]["url"] = link
# 以影像原尺寸,作為 Flex Message 輸出的依據
bubble["header"]["contents"][0]["aspectRatio"] = "{}:{}".format(
img.size[0], img.size[1]
)
LINE_BOT.reply_message(
event.reply_token, [FlexSendMessage(alt_text="Report", contents=bubble)]
)
將上述的python
程式,連同相對應的requirements.txt
,透過git push
部署到 Azure Web App , chatbot 就能依照圖片的內容,以通順的英文句子產生說明囉~~
接下來,讓 chatbot 也可以認出圖片中的文字!
The quick brown fox jumps over the lazy dog. 這句話涵蓋了 a 到 z ,共 26 個字母,這句話原本是用來檢測鍵盤有沒有故障。
光學字元辨識是透過影像處理,擷取並辨別影像上的文字,大致上比較簡單的流程:
![]() |
![]() |
![]() |
---|---|---|
原始影像 | 二值化 | 濾雜訊 |
![]() |
![]() |
![]() |
---|---|---|
原始影像 | 前處理 | 文字切分 |
文字辨識:辨別圖像中的文字為何
後處理:文字辨識不見得會完全正確,這時可以根據上下文,再搭配已經建立好的詞庫,校正文字辨識的結果。
先取得金鑰 (SUBSCRIPTION KEY) 和 端點 (ENDPOINT),作法與物體偵測一樣,都是從電腦視覺服務的頁面取得。
# 套件:azure-cognitiveservices-vision-computervision
from azure.cognitiveservices.vision.computervision \
import ComputerVisionClient
from msrest.authentication import (
CognitiveServicesCredentials
)
from azure.cognitiveservices.vision.computervision.models \
import OperationStatusCodes
from io import BytesIO
import requests
from PIL import Image, ImageDraw, ImageFont
# 利用金鑰SUBSCRIPTION_KEY和端點ENDPOINT,取得使用電腦視覺服務的權限。
SUBSCRIPTION_KEY = "YOUR SUBSCRIPTION_KEY"
ENDPOINT = "YOUR ENDPOINT"
CV_CLIENT = ComputerVisionClient(
ENDPOINT, CognitiveServicesCredentials(SUBSCRIPTION_KEY)
)
# 讀取 URL 得到圖片
url = "https://i.imgur.com/qyWiqQv.jpg"
response = requests.get(url)
img = Image.open(BytesIO(response.content))
draw = ImageDraw.Draw(img)
font_size = int(5e-2 * img.size[1])
fnt = ImageFont.truetype(
"../static/TaipeiSansTCBeta-Regular.ttf",
size=font_size)
# 開始利用 Azure 電腦視覺執行 OCR
ocr_results = CV_CLIENT.read(url, raw=True)
operation_location_remote = \
ocr_results.headers["Operation-Location"]
operation_id = operation_location_remote.split("/")[-1]
# 因為讀取文字有多有少,所以時間會不一,透過 operation_id 可以確認目前進度
status = ["notStarted", "running"]
while True:
get_handw_text_results = \
CV_CLIENT.get_read_result(operation_id)
if get_handw_text_results.status not in status:
break
time.sleep(1)
# 當執行狀態 status 為 succeeded ,就可以把結果標示在原本的照片上了
succeeded = OperationStatusCodes.succeeded
if get_handw_text_results.status == succeeded:
res = get_handw_text_results.analyze_result.read_results
for text_result in res:
for line in text_result.lines:
bounding_box = line.bounding_box
bounding_box += bounding_box[:2]
draw.line(
line.bounding_box,
fill=(255, 0, 0),
width=int(font_size / 10)
)
left = line.bounding_box[0]
top = line.bounding_box[1]
draw.text(
[left, top - font_size],
line.text,
fill=(0, 255, 255),
font=fnt,
)
# bounding_box是四邊形的頂點 [x1, y1, x2, y2, x3, y3, x4, y4],這邊的四邊形並非長方形,要使用 draw.line 畫出封閉四邊形。
# draw.line 需要知道起點位置,才能畫出封閉形狀。
img.save("output.png")
辨識完文字之後,看不懂的文字,還是看不懂,只好想辦法翻譯一下了。下一篇,使用 Azure 來翻譯。
Translator 是 Azure 提供的雲端翻譯服務,可以透過 REST API 翻譯字串。
4. 自行命名。
5. 找到可以選擇定價層 Free F0 的區域,並選擇 Free F0。
https://api.cognitive.microsofttranslator.com/
還有另一個文件翻譯的 API,其實也能做到文字翻譯的功能,但其主要功能是翻譯文件,可以把放在 Azure blob 的文件翻譯後,存入Azure blob。
這邊就相對簡單了,利用requests
套件對 translator API 傳送需要翻譯的字串。
import requests
string = "무엇을 합니까?"
TRANS_KEY = "YOUR TRANSLATOR SUBSCRIPTION KEY"
trans_url = "https://api.cognitive.microsofttranslator.com/translate"
params = {"api-version": "2.0", "to": ["zh-Hant"]}
headers = {
"Ocp-Apim-Subscription-Key": TRANS_KEY,
"Content-type": "application/json",
"Ocp-Apim-Subscription-Region": "eastus2",
}
# 字串需要包成 dict ,可以把不同的字串分別包裝成 dict,放入 list 之中
body = [{"text": string}]
req = requests.post(trans_url, params=params, headers=headers, json=body)
response = req.json()
ans = []
for i in response:
ans.append(i["translations"][0]["text"])
language = response[0]["detectedLanguage"]["language"]
print("language is {}".format(language))
print("translation is {}".format(ans))
language is ko
translation is 做什麼?
我一開始是想隨便找首韓文歌的歌詞,試著翻譯看看,但怎麼翻譯都怪怪的,於是問問我家的韓文系書券。她說,歌詞比較像詩,有些歌詞的意思不是字面上的翻譯,需要稍微轉個彎,才是作詞者的意思,就算是她看得時候也得稍微想一下。好吧~~不要為難 Azure 。
翻譯完,知道意思了,還可以再更進一步,讓 Azure 教你怎麼念!下一篇,介紹 Text-to-Speech 。
Text-to-Speech 文字轉換語音是 Azure Speech 語音服務中其中一個項目。在語音服務之中,除了提供文字和語音的互相轉換以外,還有一些更進階的服務,如語音翻譯、從話語中理解發話者的意圖、辨識發話者身份,等等。在這次要執行的 Text-to-Speech 是為了把之前用 OCR 辨識到的韓文,直接轉換成語音,讓使用者可以不但可以從翻譯知道意思,也可以學習發音。
python
套件pip3 install azure-cognitiveservices-speech
一開始需要設定 speech config,輸入金鑰和當初申請時所選擇的區域代碼,會將字串傳給 Speech 轉換成語音,語音內容可以在本地端存成wav
檔。
關於選擇區域的代碼,可以執行az account list-locations -o table
,會列出所有區域相對應的代碼,在這邊我選擇的是美國東部 2 ,對應到的代碼就是eastus2
。下面有列出對照表可供參考。
另外,這裡也需要設定發音的語系,關於語系設定的代碼,可以從此語言支援文件查詢。
from azure.cognitiveservices.speech import (
SpeechConfig,
SpeechSynthesizer,
)
# 輸入金鑰和區域,設定config
SPEECH_KEY = "你的金鑰"
speech_config = SpeechConfig(subscription=SPEECH_KEY, region="eastus2")
# 因為後續要針對韓文發音,所以設定成"ko-KR"
speech_config.speech_synthesis_language = "ko-KR"
# 設定轉換後存檔路徑
file_name = "your_path.wav"
audio_config = AudioOutputConfig(filename=file_name)
synthesizer = SpeechSynthesizer(
speech_config=speech_config, audio_config=audio_config
)
# 開始轉換成語音
synthesizer.speak_text_async(string)
DisplayName | Name | RegionalDisplayName |
---|---|---|
East US | eastus | (US) East US |
East US 2 | eastus2 | (US) East US 2 |
South Central US | southcentralus | (US) South Central US |
West US 2 | westus2 | (US) West US 2 |
West US 3 | westus3 | (US) West US 3 |
Australia East | australiaeast | (Asia Pacific) Australia East |
Southeast Asia | southeastasia | (Asia Pacific) Southeast Asia |
North Europe | northeurope | (Europe) North Europe |
Sweden Central | swedencentral | (Europe) Sweden Central |
UK South | uksouth | (Europe) UK South |
West Europe | westeurope | (Europe) West Europe |
Central US | centralus | (US) Central US |
North Central US | northcentralus | (US) North Central US |
West US | westus | (US) West US |
South Africa North | southafricanorth | (Africa) South Africa North |
Central India | centralindia | (Asia Pacific) Central India |
East Asia | eastasia | (Asia Pacific) East Asia |
Japan East | japaneast | (Asia Pacific) Japan East |
Jio India West | jioindiawest | (Asia Pacific) Jio India West |
Korea Central | koreacentral | (Asia Pacific) Korea Central |
Canada Central | canadacentral | (Canada) Canada Central |
France Central | francecentral | (Europe) France Central |
Germany West Central | germanywestcentral | (Europe) Germany West Central |
Norway East | norwayeast | (Europe) Norway East |
Switzerland North | switzerlandnorth | (Europe) Switzerland North |
UAE North | uaenorth | (Middle East) UAE North |
Brazil South | brazilsouth | (South America) Brazil South |
Central US (Stage) | centralusstage | (US) Central US (Stage) |
East US (Stage) | eastusstage | (US) East US (Stage) |
East US 2 (Stage) | eastus2stage | (US) East US 2 (Stage) |
North Central US (Stage) | northcentralusstage | (US) North Central US (Stage) |
South Central US (Stage) | southcentralusstage | (US) South Central US (Stage) |
West US (Stage) | westusstage | (US) West US (Stage) |
West US 2 (Stage) | westus2stage | (US) West US 2 (Stage) |
Asia | asia | Asia |
Asia Pacific | asiapacific | Asia Pacific |
Australia | australia | Australia |
Brazil | brazil | Brazil |
Canada | canada | Canada |
Europe | europe | Europe |
Global | global | Global |
India | india | India |
Japan | japan | Japan |
United Kingdom | uk | United Kingdom |
United States | unitedstates | United States |
East Asia (Stage) | eastasiastage | (Asia Pacific) East Asia (Stage) |
Southeast Asia (Stage) | southeastasiastage | (Asia Pacific) Southeast Asia (Stage) |
Central US EUAP | centraluseuap | (US) Central US EUAP |
East US 2 EUAP | eastus2euap | (US) East US 2 EUAP |
West Central US | westcentralus | (US) West Central US |
South Africa West | southafricawest | (Africa) South Africa West |
Australia Central | australiacentral | (Asia Pacific) Australia Central |
Australia Central 2 | australiacentral2 | (Asia Pacific) Australia Central 2 |
Australia Southeast | australiasoutheast | (Asia Pacific) Australia Southeast |
Japan West | japanwest | (Asia Pacific) Japan West |
Jio India Central | jioindiacentral | (Asia Pacific) Jio India Central |
Korea South | koreasouth | (Asia Pacific) Korea South |
South India | southindia | (Asia Pacific) South India |
West India | westindia | (Asia Pacific) West India |
Canada East | canadaeast | (Canada) Canada East |
France South | francesouth | (Europe) France South |
Germany North | germanynorth | (Europe) Germany North |
Norway West | norwaywest | (Europe) Norway West |
Sweden South | swedensouth | (Europe) Sweden South |
Switzerland West | switzerlandwest | (Europe) Switzerland West |
UK West | ukwest | (Europe) UK West |
UAE Central | uaecentral | (Middle East) UAE Central |
Brazil Southeast | brazilsoutheast | (South America) Brazil Southeast |
現在已經具備 OCR、翻譯和文字轉換語音的工具了,所以下一篇我們來組裝韓文翻譯機器人。
這篇會針對韓文翻譯機器人的功能,整合 Azure 的OCR、翻譯和文字轉換語音的工具,分別針對韓文的文字和含有韓文的圖片,進行翻譯並提供發音音檔。處理的流程分別如下:
上傳config.json
到 Azure Web App,詳情可看Chatbot integration- 看圖學英文的說明。
config.json
{
"line": {
"line_secret": "your line secret",
"line_token": "your line token",
},
"azure": {
"cv_key": "your subscription key of computer vision",
"cv_end": "your endpoint of computer vision",
"blob_connect": "your connect string",
"blob_container": "your blob container name",
"trans_key": "your subscription key of translator",
"speech_key": "your subscription key of speech"
}
python
套件requirements.txt
Flask==1.0.2
line-bot-sdk
azure-cognitiveservices-vision-computervision
azure-cognitiveservices-speech
azure-storage-blob
Pillow
langdetect
import os
import json
import time
import requests
from flask import Flask, request, abort
from azure.cognitiveservices.vision.computervision import ComputerVisionClient
from azure.cognitiveservices.vision.computervision.models import OperationStatusCodes
from azure.storage.blob import BlobServiceClient
from azure.cognitiveservices.speech import (
SpeechConfig,
SpeechSynthesizer,
)
from azure.cognitiveservices.speech.audio import AudioOutputConfig
from msrest.authentication import CognitiveServicesCredentials
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
MessageEvent,
TextMessage,
TextSendMessage,
FlexSendMessage,
ImageMessage,
)
from PIL import Image
from langdetect import detect
app = Flask(__name__)
CONFIG = json.load(open("/home/config.json", "r"))
# 取得電腦視覺的用戶權限
SUBSCRIPTION_KEY = CONFIG["azure"]["cv_key"]
ENDPOINT = CONFIG["azure"]["cv_end"]
CV_CLIENT = ComputerVisionClient(
ENDPOINT, CognitiveServicesCredentials(SUBSCRIPTION_KEY)
)
# 連結 blob service
CONNECT_STR = CONFIG["azure"]["blob_connect"]
CONTAINER = CONFIG["azure"]["blob_container"]
BLOB_SERVICE = BlobServiceClient.from_connection_string(CONNECT_STR)
# 取得翻譯工具的金鑰
TRANS_KEY = CONFIG["azure"]["trans_key"]
# 設定 Azure 語音的 config
SPEECH_KEY = CONFIG["azure"]["speech_key"]
SPEECH_CONFIG = SpeechConfig(subscription=SPEECH_KEY, region="eastus2")
SPEECH_CONFIG.speech_synthesis_language = "ko-KR"
LINE_SECRET = CONFIG["line"]["line_secret"]
LINE_TOKEN = CONFIG["line"]["line_token"]
LINE_BOT = LineBotApi(LINE_TOKEN)
HANDLER = WebhookHandler(LINE_SECRET)
@app.route("/")
def hello():
"hello world"
return "Hello World!!!!!"
# 上傳檔案到 Azure blob
def upload_blob(container, path):
"""
Upload files to Azure blob
"""
blob_client = BLOB_SERVICE.get_blob_client(container=container, blob=path)
with open(path, "rb") as data:
blob_client.upload_blob(data, overwrite=True)
data.close()
return blob_client.url
# 透過 Azure 電腦視覺執行 OCR
def azure_ocr(url):
"""
Azure OCR: get characters from image url
"""
ocr_results = CV_CLIENT.read(url, raw=True)
operation_location_remote = ocr_results.headers["Operation-Location"]
operation_id = operation_location_remote.split("/")[-1]
while True:
get_handw_text_results = CV_CLIENT.get_read_result(operation_id)
if get_handw_text_results.status not in ["notStarted", "running"]:
break
time.sleep(1)
text = []
if get_handw_text_results.status == OperationStatusCodes.succeeded:
for text_result in get_handw_text_results.analyze_result.read_results:
for line in text_result.lines:
text.append(line.text)
if len(text) == 0:
return text
return []
# 將字串翻譯成中文
def azure_translation(string, message_id):
"""
Translation with azure API
"""
trans_url = "https://api.cognitive.microsofttranslator.com/translate"
params = {"api-version": "2.0", "to": ["zh-Hant"]}
headers = {
"Ocp-Apim-Subscription-Key": TRANS_KEY,
"Content-type": "application/json",
"Ocp-Apim-Subscription-Region": "eastus2",
}
body = [{"text": string}]
req = requests.post(trans_url, params=params, headers=headers, json=body)
response = req.json()
output = ""
speech_button = ""
ans = []
for i in response:
ans.append(i["translations"][0]["text"])
language = response[0]["detectedLanguage"]["language"]
# 如果是韓文的話,就透過 Azure Speech 取得發音
if language == "ko":
output = " ".join(string) + "\n" + " ".join(ans)
speech_button = azure_speech(string, message_id)
return output, speech_button
# 將字串轉換成音訊檔,並且上傳到 Azure blob
def azure_speech(string, message_id):
"""
Azure speech: text to speech, and save wav file to azure blob
"""
file_name = "{}.wav".format(message_id)
audio_config = AudioOutputConfig(filename=file_name)
synthesizer = SpeechSynthesizer(
speech_config=SPEECH_CONFIG, audio_config=audio_config
)
synthesizer.speak_text_async(string)
# 上傳 Azure blob,並取得 URL
link = upload_blob(CONTAINER, file_name)
# 將 URL 包裝成 Flex message的按扭,以便最後輸出
output = {
"type": "button",
"flex": 2,
"style": "primary",
"color": "#1E90FF",
"action": {"type": "uri", "label": "Voice", "uri": link},
"height": "sm",
}
os.remove(file_name)
return output
@app.route("/callback", methods=["POST"])
def callback():
"""
LINE bot webhook callback
"""
# get X-Line-Signature header value
signature = request.headers["X-Line-Signature"]
print(signature)
body = request.get_data(as_text=True)
print(body)
try:
HANDLER.handle(body, signature)
except InvalidSignatureError:
print(
"Invalid signature. Please check your channel access token/channel secret."
)
abort(400)
return "OK"
@HANDLER.add(MessageEvent, message=TextMessage)
def handle_message(event):
"""
Reply text message
"""
with open("templates/detect_result.json", "r") as f_h:
bubble = json.load(f_h)
f_h.close()
# 利用 langdetect 此套件的 detect 判斷是否為韓文
if detect(event.message.text) == "ko":
output, speech_button = azure_translation(event.message.text, event.message.id)
# header 是放圖片的部分,沒有圖片的話,就先去除
bubble.pop("header")
# 放入翻譯的結果
bubble["body"]["contents"][0]["text"] = output
# 放入語音連結的按鈕
bubble["body"]["contents"].append(speech_button)
# 調整 body 的高度
bubble["body"]["height"] = "{}px".format(150)
message = FlexSendMessage(alt_text="Report", contents=bubble)
else:
message = TextSendMessage(text=event.message.text)
LINE_BOT.reply_message(event.reply_token, message)
@HANDLER.add(MessageEvent, message=ImageMessage)
def handle_content_message(event):
"""
Reply Image message with results of image description and objection detection
"""
print(event.message)
print(event.source.user_id)
print(event.message.id)
with open("templates/detect_result.json", "r") as f_h:
bubble = json.load(f_h)
f_h.close()
filename = "{}.jpg".format(event.message.id)
message_content = LINE_BOT.get_message_content(event.message.id)
with open(filename, "wb") as f_h:
for chunk in message_content.iter_content():
f_h.write(chunk)
f_h.close()
img = Image.open(filename)
link = upload_blob(CONTAINER, filename)
text = azure_ocr(link)
output, speech_button = azure_translation(" ".join(text), event.message.id)
# 整合圖片、翻譯結果和音訊按鈕到 flex message
bubble["body"]["contents"].append(speech_button)
bubble["body"]["height"] = "{}px".format(150)
bubble["body"]["contents"][0]["text"] = output
bubble["header"]["contents"][0]["url"] = link
bubble["header"]["contents"][0]["aspectRatio"] = "{}:{}".format(
img.size[0], img.size[1]
)
LINE_BOT.reply_message(
event.reply_token, [FlexSendMessage(alt_text="Report", contents=bubble)]
)
最後應該會得到如下圖的效果,這個圖片是從一本年代久遠的韓文講義找到的內容。有趣的是,OCR 有把咖啡杯上的 coffee 辨識出來,而產生的韓文語音,也用韓文發音 coffee ,跟커피發音相同,聽起來比較像kopee。
接下來要來試試看人臉辨識了,明天見。
人臉辨識也是 Azure 電腦視覺的其中一個功能,但或許因為其特殊性,人臉辨識的功能又被分出來,獨立成另一個服務- Face。人臉辨識的技術其實發展已久,是個成熟的技術,已經圍繞在一般人的身邊,手機解鎖、門禁管理和海關的自動通關,都是人臉辨識的應用。
人臉辨識的過程可以拆成幾個部分實現,一開始需要 server 端將需要辨識的人臉編碼,取得特徵向量,將特徵向量與人臉的身份配對存入資料庫。而用戶端可透過攝影機取得人臉影像,將人臉轉換成特徵向量,然後與資料庫的特徵向量比對,找出最相近的特徵向量,最後帶出相對應的身份。
再稍微細分一點,人臉辨識的過程,首先會需要偵測人臉,擷取人臉的影像,偵測人臉上的特徵點 (face landmarks),然後將人臉旋轉校正對齊,再將人臉影像的尺寸重新調整成均一大小,最後轉換成特徵向量。其中,偵測人臉的方法、face landmarks的數量、轉換特徵向量的做法,每家做法都不同,各有各的門道。
Azure 臉部服務,大致上分成人臉偵測和人臉辨識,在後續的實作上會先執行人臉偵測,然後再辨識偵測到的人臉是誰。在人臉偵測的部分,除了找出人臉的位置,同時也可以取得臉部特徵點(face landmarks)和臉部屬性(Attribution)。
Face Landmark
這邊拿我的臉示範一下。
Face Attribution
以下列舉目前 Azure 可以偵測的各項臉部屬性。
基本資料
毛髮
動作與表情
裝飾
臉部完整程度
影像品質
進入https://portal.azure.com/#home
點選建立資源
搜尋並選擇 face
自行命名
找到可以選擇定價層 Free F0 的區域,並選擇Free F0。
給予標籤
檢閱 + 建立
pip3 install azure-cognitiveservices-vision-face
這邊針對人臉辨識的需求作示範。首先我們會先建立 Person Group ,並且註冊人臉,第二步則是利用建立好的 Person Group 辨識人臉。
import sys
import time
from azure.cognitiveservices.vision.face import FaceClient
from msrest.authentication import CognitiveServicesCredentials
from azure.cognitiveservices.vision.face.models import TrainingStatusType
KEY = "YOUR KEY"
ENDPOINT = "YOR ENDPOINT"
FACE_CLIENT = FaceClient(
ENDPOINT, CognitiveServicesCredentials(KEY))
# 先建立 Person Group
PERSON_GROUP_ID = "請自行命名"
FACE_CLIENT.person_group.create(
person_group_id=PERSON_GROUP_ID, name=PERSON_GROUP_ID)
# 在 Person Group 中新增一人
person = FACE_CLIENT.person_group_person.create(
PERSON_GROUP_ID, "YOUR NAME")
# 新增照片給指定之人
my_pic = open("YOUR PHOTO", 'r+b')
FACE_CLIENT.person_group_person.add_face_from_stream(
PERSON_GROUP_ID, person.person_id, my_pic)
# 開始訓練,一開始放進去的照片量較大的話,會需要較長時間,可以透過 get_training_status 確認當下狀態
FACE_CLIENT.person_group.train(PERSON_GROUP_ID)
while (True):
training_status = FACE_CLIENT.person_group.get_training_status(PERSON_GROUP_ID)
print("Training status: {}.".format(training_status.status))
print()
if (training_status.status is TrainingStatusType.succeeded):
break
elif (training_status.status is TrainingStatusType.failed):
sys.exit('Training the person group has failed.')
time.sleep(5)
人臉辨識所需要用到的照片需要符合以下需求:
img = open("YOUR PHOTO", "r+b")
detected_face = FACE_CLIENT.face.detect_with_stream(
img, detection_model="detection_01"
)
# 臉部服務會給每一張偵測到的臉一個face ID
results = FACE_CLIENT.face.identify([detected_face[0].face_id], PERSON_GROUP_ID)
result = results[0].as_dict()
# 如果在資料庫中有找到相像的人,會給予person ID
# 再拿此person ID去查詢名字
person = FACE_CLIENT.person_group_person.get(
PERSON_GROUP_ID, result["candidates"][0]["person_id"]
)
print(person.name)
可以辨識人臉之後,就可以做出人臉登入的效果,這會需要把偵測的結果紀錄下來,一般會記錄在資料庫之中。下一篇,介紹一個過去我曾經用過的做法。
MongoDB
是一種NoSQL
的資料庫,用來處理半結構化資料的資料庫系統,其儲存的資料結構可以不需要事先定義,甚至可以自由新增欄位,不需要回頭修改 schema ,可以自由定義資料結構(謎之聲:這樣真的是挺隨便的啊…)。因為很自由,所以其實適用於一開始還在開發階段,或者還在確認概念是否可行的階段,這種時候通常資料結構都還不太確定。另一方面,也適用於資料之間沒有複雜關聯性的情況,或者服務比較注重資料的可用性和取得資料的速度。我想相對而言,在 Line chatbot 上要實作人臉登入,符合上述的情況,這的確只是一個小專案,資料結構可能隨著不同的使用者,可以做不同的設計,且比起資料結構,我可能更在意 chatbot 能不能快速取得資料並且回應。
Azure Cosmos DB 是微軟的全球分佈式(globally distributed)多模型資料庫服務,支援多種資料庫,可用不同語法操作,而MongoDB
也是其中一項服務。有時候,我們所設計的程式或服務,需要在全世界各個角落都能快速回應,且必須維持高度可用性,絕對不能斷線,這時候 Azure Cosmos DB 全球分佈的特性就能派上用場了。若要在多個 Azure 區域中部署自己的資料,可以在自己的帳戶中新增 Azure 的服務區域,如此一來資料就能快速支援不同區域了。
這邊 Azure Cosmos DB 提供的MongoDB
服務,基本上可以想像成在 Azure 上建立MongoDB
server,不同的是這個 MongoDB Server 的底層依舊是 Azure Cosmos DB,但所有操作都與一般MongoDB
無異。
主要連接字串其實就是連結MongoDB
的 URI。
主要連接字串
pip3 install pymongo
這邊不會完整教學MongoDB
的用法,只會針對接下來會用到的部分做介紹。
userId
存入資料庫from pymongo import MongoClient
uri = "mongodb://mongoit:...."
client = MongoClient(uri, retryWrites=False)
# 取得名為 face_register 的資料庫,若原本沒有此資料庫,此舉也能產生此資料庫
db = client['face_register']
# 取得名為 line 的 collection,若原本沒有此 collection,此舉也能產生此 collection
collect = db['line']
post = {"name": "Triathlon", "userId": "1234567890"}}
collect.insert_one(post)
特別一提的是,這邊所使用的userId
,是 Line 針對每個 chatbot 的每個使用者所做出來的 unique ID,所以同一個使用者在不同的 Line chatbot 之中,會有不同的userId
,而且也跟在 Line App 上看到的 user ID 不同。我們可以利用 chatbot 取得 event
時,透過 event.source.user_id
取得 userId
。將人名與userId
存入資料庫之後,下一篇文章提到的人臉登入,才可以進行身份比對。
collect.find_one({"name": "Triathlon"})
from datetime import datetime
collect_login = db["daily_login"]
now = datetime.now()
post = {"userId": "1234567890", "time": now.timestamp()}
collect_login.insert_one(post)
yesterday = datetime.now() - timedelta(days=1)
# 檢查符合條件的有多少筆:要符合User ID 且 timestamp 在一天之內的
result = collect_login.count_documents(
{"$and": [{"userId": "1234567890"}, {"time": {"$gte": yesterday.timestamp()}}]}
)
這邊需要用$and
和$gte
來分別表示and
和>=
,跟其他語法相比算是比較特別的地方。其它常用用法可參考:https://docs.mongodb.com/manual/reference/operator/query/
以上只是簡單的示範,接下來就必須考慮有關人臉登入的幾件事情:第一,需要用人臉和 Line user ID 做雙重比對;第二,需要紀錄登入時間;第三,需要確認最近一段時間內,是否曾經登入。下一篇,我們來試著實作人臉登入。
這邊要做的事情不是用人臉登入 Line ,而是比較像針對 chatbot 權限的使用管理。有一種情況是 chatbot 提供了各項服務,可能大部分的服務可以讓大家使用,但有些特別服務是要另外申請會員,甚至需要付費才能使用,此時我們就可以要求使用者需要登入才能使用,人臉登入就派上用場了。
雖然 Line 有提供Line Login
,來達到會員登入的串接功能,不過前提是需要有自己的會員登入入口網頁。這邊的人臉登入先考慮比較簡單的情況,跳過需要登入網站的情況和Line Login
。
接下來,會以之前曾經介紹過的韓文翻譯機器人為例子,讓使用者用人臉登入之後,才能翻譯韓文。假設,已經利用 Azure Face 註冊好自己的人臉,也將人名與userId
配對存入MongoDB
,後續人臉登入的處理流程如下:
MongoDB
,確認人名是否已在資料庫。userId
,是否與當前 chatbot 使用者的 user ID 相符。MongoDB
。上傳config.json
到 Azure Web App,詳情可看Chatbot integration- 看圖學英文的說明。
config.json
{
"line": {
"line_secret": "your line secret",
"line_token": "your line token",
},
"azure": {
"face_key": "your subscription key of Azure Face service",
"face_end": "your endpoint of Azure Face service",
"blob_connect": "your connect string",
"blob_container": "your blob container name",
"trans_key": "your subscription key of translator",
"speech_key": "your subscription key of speech",
"mongo_uri":"your mongon uri"
}
Python
套件requirements.txt
Flask==1.0.2
line-bot-sdk
azure-cognitiveservices-vision-face
azure-cognitiveservices-speech
azure-storage-blob
Pillow
pymongo
langdetect
application.py
from datetime import datetime, timezone, timedelta
import os
import json
import requests
from flask import Flask, request, abort
from azure.cognitiveservices.vision.face import FaceClient
from azure.storage.blob import BlobServiceClient
from azure.cognitiveservices.speech import (
SpeechConfig,
SpeechSynthesizer,
)
from azure.cognitiveservices.speech.audio import AudioOutputConfig
from msrest.authentication import CognitiveServicesCredentials
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
MessageEvent,
TextMessage,
TextSendMessage,
FlexSendMessage,
ImageMessage,
)
from pymongo import MongoClient
from PIL import Image
from langdetect import detect
app = Flask(__name__)
CONFIG = json.load(open("/home/config.json", "r"))
# 取得 Azure Face 的權限
FACE_KEY = CONFIG["azure"]["face_key"]
FACE_END = CONFIG["azure"]["face_end"]
FACE_CLIENT = FaceClient(FACE_END, CognitiveServicesCredentials(FACE_KEY))
PERSON_GROUP_ID = "triathlon"
# 連接MongoDB
MONGO = MongoClient(CONFIG["azure"]["mongo_uri"], retryWrites=False)
DB = MONGO["face_register"]
CONNECT_STR = CONFIG["azure"]["blob_connect"]
CONTAINER = CONFIG["azure"]["blob_container"]
BLOB_SERVICE = BlobServiceClient.from_connection_string(CONNECT_STR)
TRANS_KEY = CONFIG["azure"]["trans_key"]
SPEECH_KEY = CONFIG["azure"]["speech_key"]
SPEECH_CONFIG = SpeechConfig(subscription=SPEECH_KEY, region="eastus2")
SPEECH_CONFIG.speech_synthesis_language = "ko-KR"
LINE_SECRET = CONFIG["line"]["line_secret"]
LINE_TOKEN = CONFIG["line"]["line_token"]
LINE_BOT = LineBotApi(LINE_TOKEN)
HANDLER = WebhookHandler(LINE_SECRET)
@app.route("/")
def hello():
"hello world"
return "Hello World!!!!!"
# 查詢名為 line 的 collection 之中,是否有某個人名
def check_registered(name):
"""
Check if a specific name is in the database
"""
collect_register = DB["line"]
return collect_register.find_one({"name": name})
# 確認從 MongoDB 對應到的 User ID 是否與從 Line 取得的 User ID 相符
# 若相符,則將當下的 timestamp 連同 User ID 紀錄於 MongoDB
def face_login(name, user_id):
"""
Insert face recognition result to MongoDB
"""
result = check_registered(name)
if result:
if result["userId"] == user_id:
collect_login = DB["daily_login"]
now = datetime.now()
post = {"userId": user_id, "time": now.timestamp()}
collect_login.insert_one(post)
# 檢查該 User ID 最近一天內是否有登入
def is_login(user_id):
"""
Check login status from MongoDB
"""
collect_login = DB["daily_login"]
yesterday = datetime.now() - timedelta(days=1)
result = collect_login.count_documents(
{"$and": [{"userId": user_id}, {"time": {"$gte": yesterday.timestamp()}}]}
)
return result > 0
# 上傳檔案到 Azure Blob
def upload_blob(container, path):
"""
Upload files to Azure blob
"""
blob_client = BLOB_SERVICE.get_blob_client(container=container, blob=path)
with open(path, "rb") as data:
blob_client.upload_blob(data, overwrite=True)
data.close()
return blob_client.url
# 文字轉換成語音
def azure_speech(string, message_id):
"""
Azure speech: text to speech, and save wav file to azure blob
"""
file_name = "{}.wav".format(message_id)
audio_config = AudioOutputConfig(filename=file_name)
synthesizer = SpeechSynthesizer(
speech_config=SPEECH_CONFIG, audio_config=audio_config
)
synthesizer.speak_text_async(string)
link = upload_blob(CONTAINER, file_name)
output = {
"type": "button",
"flex": 2,
"style": "primary",
"color": "#1E90FF",
"action": {"type": "uri", "label": "Voice", "uri": link},
"height": "sm",
}
os.remove(file_name)
return output
# 翻譯韓文成中文
def azure_translation(string, message_id):
"""
Translation with azure API
"""
trans_url = "https://api.cognitive.microsofttranslator.com/translate"
params = {"api-version": "2.0", "to": ["zh-Hant"]}
headers = {
"Ocp-Apim-Subscription-Key": TRANS_KEY,
"Content-type": "application/json",
"Ocp-Apim-Subscription-Region": "eastus2",
}
body = [{"text": string}]
req = requests.post(trans_url, params=params, headers=headers, json=body)
response = req.json()
output = ""
speech_button = ""
ans = []
for i in response:
ans.append(i["translations"][0]["text"])
language = response[0]["detectedLanguage"]["language"]
if language == "ko":
output = " ".join(string) + "\n" + " ".join(ans)
speech_button = azure_speech(string, message_id)
return output, speech_button
# 人臉辨識
def azure_face_recognition(filename):
"""
Azure face recognition
"""
img = open(filename, "r+b")
detected_face = FACE_CLIENT.face.detect_with_stream(
img, detection_model="detection_01"
)
# 如果偵測不到人臉,或人臉太多,直接回傳空字串
if len(detected_face) != 1:
return ""
results = FACE_CLIENT.face.identify([detected_face[0].face_id], PERSON_GROUP_ID)
# 找不到相對應的人臉,回傳 unknown
if len(results) == 0:
return "unknown"
result = results[0].as_dict()
if len(result["candidates"]) == 0:
return "unknown"
# 如果信心程度低於 0.5,也當作不認識
if result["candidates"][0]["confidence"] < 0.5:
return "unknown"
# 前面的 result 只會拿到 person ID,要進一步比對,取得人名
person = FACE_CLIENT.person_group_person.get(
PERSON_GROUP_ID, result["candidates"][0]["person_id"]
)
return person.name
@app.route("/callback", methods=["POST"])
def callback():
"""
LINE bot webhook callback
"""
# get X-Line-Signature header value
signature = request.headers["X-Line-Signature"]
print(signature)
body = request.get_data(as_text=True)
print(body)
try:
HANDLER.handle(body, signature)
except InvalidSignatureError:
print(
"Invalid signature. Please check your channel access token/channel secret."
)
abort(400)
return "OK"
# Line chatbot 接收影像後,開始執行人臉辨識
@HANDLER.add(MessageEvent, message=ImageMessage)
def handle_content_message(event):
"""
Reply Image message with results of image description and objection detection
"""
print(event.message)
print(event.source.user_id)
print(event.message.id)
with open("templates/detect_result.json", "r") as f_h:
bubble = json.load(f_h)
f_h.close()
filename = "{}.jpg".format(event.message.id)
message_content = LINE_BOT.get_message_content(event.message.id)
with open(filename, "wb") as f_h:
for chunk in message_content.iter_content():
f_h.write(chunk)
f_h.close()
img = Image.open(filename)
link = upload_blob(CONTAINER, filename)
# 人臉辨識後取得人名
name = azure_face_recognition(filename)
output = ""
if name != "":
now = datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M")
output = "{0}, {1}".format(name, now)
# 取得人名後,進行登入
face_login(name, event.source.user_id)
# 包裝成 flex message
bubble["body"]["contents"][0]["text"] = output
bubble["header"]["contents"][0]["url"] = link
bubble["header"]["contents"][0]["aspectRatio"] = "{}:{}".format(
img.size[0], img.size[1]
)
LINE_BOT.reply_message(
event.reply_token, [FlexSendMessage(alt_text="Report", contents=bubble)]
)
@HANDLER.add(MessageEvent, message=TextMessage)
def handle_message(event):
"""
Reply text message
"""
with open("templates/detect_result.json", "r") as f_h:
bubble = json.load(f_h)
f_h.close()
# 如果傳來的文字是韓文,且此使用者一天之內曾經登入的話,就可使用翻譯韓文的功能
if (detect(event.message.text) == "ko") and is_login(event.source.user_id):
output, speech_button = azure_translation(event.message.text, event.message.id)
bubble.pop("header")
bubble["body"]["contents"][0]["text"] = output
bubble["body"]["contents"].append(speech_button)
bubble["body"]["height"] = "{}px".format(150)
message = FlexSendMessage(alt_text="Report", contents=bubble)
else:
message = TextSendMessage(text=event.message.text)
LINE_BOT.reply_message(event.reply_token, message)
登入前,丟出韓文訊息,完全不會幫忙翻譯
人臉登入後,會出現以下畫面:
登入之後,即會得到翻譯結果
到此為止,之前提過的 Azure 認知服務都應用到 Line chatbot 了。這其實算是偷懶,直接利用 Azure 已經訓練好的模型。但實際上,要從零開始訓練自己的模型,就需要許多步驟了。所幸,Azure Machine Learning 提供了平台,方便使用者可以在此平台上訓練模型。後續幾篇文章,會在 Azure Machine Learning 一一示範,如何收集資料,訓練模型,最後使用模型。
大概試一下就會知道 Azure 電腦視覺中的物體辨識並不是萬能的,雖然生活中的物體大多數可以辨識,但還是會有很多東西是認不得的。這時,如果有辨識特殊物體的需求,就可以利用 Custom Vision 訓練一個專屬模型。
Transfer Learning 轉移學習
服務項目
流程
進入https://customvision.ai/
用Azure 帳號登入
{
"ENDPOINT": "<your endpoint>",
"training_key": "<your training key>",
"prediction_key": "<your prediction key>",
"prediction_resource_id": "<your prediction resource id>",
"publish_iteration_name": "publish iteration name can be defined by yourself",
"project_name": "project name can be defined by yourself",
"annotation_file": "annotation.json",
"label": [
"fork",
"scissors"
],
"image_folder": "your image folder"
}
{
"<label>": {
"<file name>": [
<left>, <top>, <width>, <height>
],
...
...},
"scissors": {
"scissors_1": [
0.4007353, 0.194068655, 0.259803921, 0.6617647
],
...,
...,}
}
args = parse_args()
config = json.load(open(args.config, "r"))
credentials = ApiKeyCredentials(i
n_headers={"Training-key": config["training_key"]})
trainer = CustomVisionTrainingClient(
config["ENDPOINT"], credentials)
obj_detection_domain = next(
domain
for domain in trainer.get_domains()
if domain.type == "ObjectDetection" and \
domain.name == "General"
)
project = trainer.create_project(
config["project_name"], domain_id=obj_detection_domain.id
)
def add_image(
trainer, label, project_id, annotation, image_folder):
tagged_images_with_regions = []
tag = trainer.create_tag(project_id, label)
for file_name in annotation.keys():
left, top, width, height = annotation[file_name]
regions = [
Region(tag_id=tag.id,
left=left, top=top, width=width, height=height)
]
file_path = os.path.join(
image_folder, label, file_name + ".jpg")
with open(file_path, "rb") as image_contents:
tagged_images_with_regions.append(
ImageFileCreateEntry(
name=file_name,
contents=image_contents.read(),
regions=regions
)
)
image_contents.close()
return tagged_images_with_regions
image_folder = config["image_folder"]
annotations = json.load(open("annotation.json", "r"))
tagged_images_with_regions = []
for label in annotations.keys():
tagged_images_with_regions += add_image(
trainer, label, project.id,
annotations[label], image_folder)
upload_result = trainer.create_images_from_files(
project.id,
ImageFileCreateBatch(images=tagged_images_with_regions)
)
if not upload_result.is_batch_successful:
print("Image batch upload failed.")
for image in upload_result.images:
print("Image status: ", image.status)
iteration = trainer.train_project(project.id)
while iteration.status != "Completed":
iteration = trainer.get_iteration(
project.id, iteration.id)
print("Training status: " + iteration.status)
time.sleep(1)
publish_iteration_name = config["publish_iteration_name"]
prediction_resource_id = config["prediction_resource_id"]
trainer.publish_iteration(
project.id, iteration.id,
publish_iteration_name, prediction_resource_id
)
def get_project_id(config):
credentials = ApiKeyCredentials(
in_headers={"Training-key": config["training_key"]})
trainer = CustomVisionTrainingClient(
config["ENDPOINT"], credentials)
project_id = next(
proj.id
for proj in trainer.get_projects()
if proj.name == config["project_name"]
)
return project_id
prediction_credentials = ApiKeyCredentials(
in_headers={"Prediction-key": config["prediction_key"]}
)
predictor = CustomVisionPredictionClient(
config["ENDPOINT"], prediction_credentials)
project_id = get_project_id(config)
with open(args.image, "rb") as image_contents:
results = predictor.classify_image(
project_id,
config["publish_iteration_name"],
image_contents.read(),
)
image_contents.close()
img = Image.open(args.image)
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(
"../static/TaipeiSansTCBeta-Regular.ttf", size=int(5e-2 * img.size[1])
)
for prediction in results.predictions:
if prediction.probability > 0.5:
bbox = prediction.bounding_box.as_dict()
left = bbox['left'] * img.size[0]
top = bbox['top'] * img.size[1]
right = left + bbox['width'] * img.size[0]
bot = top + bbox['height'] * img.size[1]
draw.rectangle(
[left, top, right, bot],
outline=(255, 0, 0), width=3)
draw.text(
[left, abs(top - 5e-2 * img.size[1])],
"{0} {1:0.2f}".format(
prediction.tag_name,
prediction.probability * 100
),
fill=(255, 0, 0),
font=font,
)
img.show()