大致上的概念是要利用 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)
# 特地留下 hello world,程式碼有問題時,我會從這邊開始檢查
@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 也可以認出圖片中的文字!