我很快的把书拿了起来,翻开封面。有一行我不认得的笔迹在书页上写着:如果我能保留一点空间让你在这边储存什麽档案的话,那麽,
/tmp/
就是我想为你留下的空间。~节录自《聊天机器人的历史:我爸的暂存空间》
延伸自系列文 《从LINE BOT到资料视觉化:赖田捕手》
《赖田捕手:追加篇》:
今天您将知道:
ImageSendMessage
发送给使用者上一篇我们利用快速回覆按钮设计了一连串和使用者互动的过程,并用 Heroku Postgres 将使用者的输入资料储存起来。但最重要的双色打光 (Dual Lighting Effect) 还是不见踪影?是的,因此今天我们要来把这最重要的一块拼图给拼上了。
虽然我们的 LINE BOT 还不知道该怎麽做到双色打光,但在第 32 天当中,我想大家都应该知道双色打光该怎麽做了。所以第一个任务很简单,让我们把双色打光的程序码装备到 LINE BOT 身上。先来看看我们目前的资料夹结构:
D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│ runtime.txt
│ requirements.txt
│ Procfile
│ Alma.py
│
└───app
│ __init__.py
│ models_for_line.py
│ routes.py
│
└───custom_models
AlmaTalks.py
CallDatabase.py
因为我想要维持分工明确的程序码,所以就开一个新的档案AlmaRenders.py
,将所有用来执行双色打光的程序码都放进来吧:
D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│ runtime.txt
│ requirements.txt
│ Procfile
│ Alma.py
│
└───app
│ __init__.py
│ models_for_line.py
│ routes.py
│
└───custom_models
AlmaTalks.py
AlmaRenders.py
CallDatabase.py
而档案里的详细内容则是:
app/custom_models/AlmaRenders.py
import numpy as np
from PIL import Image
from PIL import ImageDraw
def sigmoid(x, alpha):
return 1 /(1 + np.exp(-x * alpha))
def create_gradient_layer(layer_im, gradient_factor, first_tone, second_tone):
layer_gradient = Image.new('RGB', layer_im.size)
draw = ImageDraw.Draw(layer_gradient)
for i in range(layer_im.size[0]):
value = sigmoid(i - layer_im.size[0] / 2, gradient_factor / layer_im.size[0])
fill_color = np.array(first_tone) * value + np.array(second_tone) * (1 - value)
draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int')))
return layer_gradient
以及最重要的dual_tone_run
:
app/custom_models/AlmaRenders.py
from PIL import Image
from PIL import ImageEnhance
from PIL import ImageOps
def dual_tone_run(im, mode, gradient_factor, first_tone, second_tone):
color_dict = {
'red': {'blend': (100, 10, 0),
'composite': (100, 10, 0),
'composite_invert': (255, 0, 0)},
'orange': {'blend': (100, 50, 0),
'composite': (100, 50, 0),
'composite_invert': (255, 120, 0)},
'yellow': {'blend': (100, 100, 0),
'composite': (100, 100, 0),
'composite_invert': (255, 255, 0)},
'green': {'blend': (10, 100, 0),
'composite': (10, 100, 0),
'composite_invert': (0, 255, 0)},
'blue': {'blend': (0, 10, 100),
'composite': (0, 10, 100),
'composite_invert': (0, 0, 255)},
'purple': {'blend': (50, 0, 100),
'composite': (50, 0, 100),
'composite_invert': (120, 0, 255)}
}
layer_im = im.convert('RGBA')
first_tone = color_dict[first_tone][mode]
second_tone = color_dict[second_tone][mode]
layer_gradient = create_gradient_layer(layer_im, gradient_factor, first_tone, second_tone).convert('RGBA')
if mode == 'blend':
dual_tone = Image.blend(layer_im, layer_gradient, 0.5)
elif mode == 'composite':
dual_tone = Image.composite(layer_im, layer_gradient, layer_im.convert('L'))
elif mode == 'composite_invert':
dual_tone = Image.composite(layer_im, layer_gradient, ImageOps.invert(layer_im.convert('L')).convert('L'))
# 和第32天不同的地方
return ImageEnhance.Color(dual_tone).enhance(2)
在dual_tone_run
当中的color_dict
是执行双色打光时的色码对照表。当使用者选择线性叠图 (blend
) 当作双色打光模式,并且选择红色 (red
) 时,LINE BOT 就会根据color_dict
选到(100, 0, 0)
这个色码做为稍後执行双色打光的其中一种颜色。我发觉当使用线性叠图 (blend
) 跟滤镜叠图 (composite
) 模式时,颜色不要用得太鲜艳,结果会比较好。反之,当使用反式线性叠图 (invert_composite
) 模式时,颜色需要相当鲜艳,结果才会好看。当然这是根据我的个人喜好选出来的颜色,大家也可以按照自己的需求给定想要的颜色。
另外,和第 32 天不太一样的地方,我们在最後面多了一个ImageEnhance.Color(image).enhance(2)
,用来将图片的颜色变得更饱满鲜艳一点。这是 Python 图像处理资源库PIL
内建的一个非常好玩的功能。PIL
一共提供 4 种ImageEnhance
的类别,包括PIL.ImageEnhance.Color
、PIL.ImageEnhance.Contrast
、PIL.ImageEnhance.Brightness
、以及PIL.ImageEnhance.Sharpness
,可以分别用来调整图像的色彩、对比、亮度、以及锐利度。使用的方式基本相同,以色彩为例:
from PIL import ImageEnhance
enhancer = ImageEnhance.Color(image)
enhanced_image = enhancer.enhance(factor)
第二行:enhancer = ImageEnhance.Color(image)
:
将想要做色彩调整的影像image
作为参数,放入ImageEnhance.Color
当中,初始化而得到enhancer
这个物件。
第三行:enhanced_image = enhancer.enhance(factor)
:
新的enhancer
物件具有一个内建的函式enhance
。用大於 0 的任一数值factor
当作参数,呼叫enhance
这个方法,就可以得到一张根据factor
调整过後的影像enhanced_image
。在调整色彩时,将factor
设为 0 会得到灰阶的影像。factor
设为 1 则会得到原始影像,大於 1 则会得到强化色彩的影像。其他类别的执行逻辑也是如此,大家可以自己试试看。
当装备上双色打光这个人人称羡的配件之後,我们的 LINE BOT 是不是就可以帮使用者进行新潮又前卫的影像处理了呢?先让我们来顺一顺整段流程。
AlmaTalks.phase_start
会被触发,替使用者初始化一笔资料。QuickReply
,在AlmaTalks.phase_intermediate
的带领之下,一次一次的更新资料。QuickReplyButton
设定好最後一个参数,也就是second_tone
,这时AlmaTalks.phase_finish
会被触发。也就是到这个时候,LINE BOT 要开始动起来,为影像添加双色打光效果。让我们仔细看一看这个phase_finish
:
app/custom_models/AlmaTalks.py
from app.custom_models import AlmaRenders
def phase_finish(event):
user_id = event.source.user_id
postback_data = event.postback.data
current_phase = postback_data.split('=')[0]
record = CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1])
mode = record[2]
gradient_factor = int(record[3])
first_tone = record[4]
second_tone = record[5]
# 所以我说那个im在哪里呢?
im_dual_tone = AlmaRenders.dual_tone_run(im, mode, gradient_factor, first_tone, second_tone)
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=str(record))
)
我们用CallDatabase.update_record
更新使用者输入的参数,同时也拿到了更新之後的资料,包括模式、颜色梯度、第一种颜色、第二种颜色。这时候就可以利用dual_tone_run
,对使用者一开始传送过来的图片做影像处理了,对吧?
等等,所以我说那个一开始传送过来的图片在哪里呢?没错,就是dual_tone_run
在执行的时候需要的第一个参数im
。没有im
的dual_tone_run
,根本没有执行的必要。
虽然很残酷,但事实就是如此。
那我们该怎麽拿到使用者一开始传送过来的图片呢?幸运的是,这个问题,LINE 已经帮我们设想好了:
message_content = line_bot_api.get_message_content(message_id)
with open(file_path, 'wb') as fd:
for chunk in message_content.iter_content():
fd.write(chunk)
每一次使用者和 LINE BOT 的互动,每一个讯息,都有属於自己的编号,也就是message_id
。只要透过这个message_id
,就可以拿到当次互动的讯息内容,比如说图片、影片、音讯等资料。而我们就是要透过message_id
来拿到使用者一开始传送过来的图片。而这个message_id
会放在哪里呢?答案是event.message.id
。
如果是一般的文字讯息事件 (TextMessage
),LINE BOT 会收到这样的内容:
{
"events": [
{
"type": "message",
"replyToken": "代表reply token的一串代码",
"source": {
"userId": "代表user id 的一串代码",
"type": "user"
},
"timestamp": 1609663876391,
"mode": "active",
"message": {
"type": "text",
"id": "13316587131627",
"text": "使用者输入的文字讯息"
}
}
],
"destination": "代表LINE BOT的一串代码"
}
而如果是收到图片 (ImageMessage
),LINE BOT 则会看到:
{
"events": [
{
"type": "message",
"replyToken": "代表reply token的一串代码",
"source": {
"userId": "代表user id 的一串代码",
"type": "user"
},
"timestamp": 1609675744047,
"mode": "active",
"message": {
"type": "image",
"id": "13317467950018",
"contentProvider": {
"type": "line"
}
}
}
],
"destination": "代表LINE BOT的一串代码"
}
和一般的文字讯息不同,图片讯息里面完全没有显示任何和图片有关的资料。要知道使用者究竟传了什麽图片给 LINE BOT,只能透过event.message.id
。
现在回头看看AlmaTalks.phase_start
,有没有突然明白为什麽我们要在初始化使用者资料的时候顺便存入event.message.id
了吗?
利用line_bot_api.get_message_content(message_id)
拿到使用者传送给 LINE BOT 的图片内容之後,下一步我们得把这张图片存起来,这样我们才可以真正来使用这张图片。至於可以把图片存在哪里,我想答案已经呼之欲出了,那就是 Heroku 的暂存空间里,也就是/tmp/
。
Heroku 所提供的空间大概长这样:
/
│ bin
│ dev
│ etc
│ lib
│ lib64
│ lost+found
│ proc
│ sbin
│ sys
│ tmp
│ usr
│ var
└───app
│ runtime.txt
│ requirements.txt
│ Procfile
│ Alma.py
│
└───app
│ __init__.py
│ models_for_line.py
│ routes.py
│
└───custom_models
AlmaTalks.py
CallDatabase.py
这种档案架构,对 Linux 系统熟悉的人应该不会陌生。不过我们现在先不管这些,不晓得大家有没有注意到,我们推向 Heroku 的档案,就放在/app/
里呢!
而在这些空间里面,最能让我们自由运用的,就是/tmp/
了。所以让我们试着把使用者发送过来的图片存到/tmp/
来吧:
app/custom_models/AlmaRenders.py
def get_image(message_content, file_name):
file_path = f"/tmp/{file_name}.png"
with open(file_path, 'wb') as fd:
for chunk in message_content.iter_content():
fd.write(chunk)
im = Image.open(file_path)
return im
然後稍微修改一下AlmaTalks.py
:
AlmaTalks.py
from app.custom_models import AlmaRenders
def phase_finish(event):
user_id = event.source.user_id
postback_data = event.postback.data
current_phase = postback_data.split('=')[0]
record = CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1])
# 所以我说那个im在这里
im = AlmaRenders.get_image(message_content, event.reply_token)
mode = record[2]
gradient_factor = int(record[3])
first_tone = record[4]
second_tone = record[5]
im_dual_tone = AlmaRenders.dual_tone_run(im, mode, gradient_factor, first_tone, second_tone)
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=str(record))
)
太棒了,我们现在可以顺利执行dual_tone_run
了。
好的,现在我们的 LINE BOT 学会了双色打光,而且还可以根据使用者输入的条件,对使用者传送过来的图片佐以不同效果的双色打光,是不是很厉害啊。不过问题是,对於没有看到成品的使用者而言,这根本就不厉害啊。这是一个现实而且冷酷的世界,与其讨论过程如何如何,大家更看重的是成果?
那麽,该怎麽做才能够让使用者看到成果呢?直觉来想,当然是用 LINE 所提供的方法ImageSendMessage
:
image_message = ImageSendMessage(
original_content_url='https://example.com/original.jpg',
preview_image_url='https://example.com/preview.jpg'
)
上面那一段是在line-bot-sdk
官方文件当中所给出的用法。也就是说,要传送图片给使用者,我们必须要为图片创造出一个网址 (URL),而且必须是符合 HTTPS 传输协定的网址才行。
如果说是传送存在网路上的图片那还简单,现在我们要传送的是 LINE BOT 画出来的图片,这该怎麽做呢?
其中一种方法是,那我们就先将图片上传到网路上。由於 Imgur 提供了免费的网路空间,让大家可以自由上传自己的图片。只要我们教会 LINE BOT 如何把图片上传到 Imgur,就可以利用ImageSendMessage
这个方法将图片送到使用者手上。有兴趣的可以参考 twtrubiks 大大在这里所提供的详细教学。
意思是,今天我们要讨论另一种方法。
这个方法可以充分利用我们今天上半场学到的内容,也就是将图片存在 Heroku 暂存空间。接着,再透过flask
来为这张放在暂存空间里的图片创造出一个网址。等等,flask
是什麽?大家不要忘了flask
啊。虽然我们很少提,但 LINE BOT 之所以能够在 Heroku 上接收 LINE 传送过来的资料,靠的就是flask
。
flask
是 Python 用来建立网路框架相当轻便好用的资料库。利用flask
,我们在 Heroku 当中创造出路由,让 LINE 可以将资料传送过来。
等等,创造出路由?这不就是我们需要的吗。那详细来说到底可以怎麽来实作呢?不罗嗦,让我们直接看一段程序码:
app/custom_models/AlmaRenders.py
import os
def save_image(im, reply_token):
file_path = f'/tmp/{reply_token}_DualTone.png'
im.save(file_path)
return f'https://{os.getenv("YOUR_HEROKU_APP_NAME")}.herokuapp.com/result/{reply_token}'
首先,在AlmaRenders.py
这个档案里多补上一个函式save_image
,用来储存 LINE BOT 做完双色打光之後的图片,一样,就存在 Heroku 的暂存空间里。为了不让不同使用者之间产生出来的图片互相混淆,我们可以将reply_token
放进图片档的档名当中,作为识别。
接着让我们仔细看一看最後一行:
f'https://{os.getenv("YOUR_HEROKU_APP_NAME")}.herokuapp.com/result/{reply_token}'
这是一个虚假的网址,但也不全是凭空产生的。当我们在 Heroku 上新建立一个 APP,Heroku 就会帮我们创造出一个网址https://你-APP-的名字.herokuapp.com
,用来连接到我们的 APP。也就是说,os.getenv("YOUR_HEROKU_APP_NAME")
要填入的,就是你-APP-的名字
。当然你也可以不一定要用环境变数来写这一段程序码。直接把你-APP-的名字
填在上面也可以。我这边这麽做,是为了方便客制化。要怎麽设定环境变数,大家可以参考第 31 天。
而在https://你-APP-的名字.herokuapp.com
後面的路由,则是我们要请flask
帮忙创造出来的。
因此这个函式执行的结果是,会把双色打光的图片存到 Heroku 暂存空间,接着传回一个虚假的网址。接着就要靠强大的flask
来把这个虚假的网址变成实际存在,可以代表图片的网址了。
还记的我们把负责路由的程序码都放在routers.py
这个档案里面吗,现在就是要在这个档案当中多加入一段程序码,来帮忙产生我们需要的路由:
app/routers.py
from app import app
from flask import send_from_directory
@app.route("/result/<token>")
def get_image_url(token):
return send_from_directory('/tmp/', filename=f'{token}_DualTone.png')
第三行:@app.route("/result/<token>")
当有人呼叫https://你-APP-的名字.herokuapp.com
这个网域底下的result/<token>
,也就是https://你-APP-的名字.herokuapp.com/result/<token>
的时候,就执行下面的函式。这个路由特殊之处,在於可以接受变数。被角括号<>
所包起来的位置就是变数,以这边为例就是<token>
。这个变数可以作为函式的参数而使用。
第四行:def get_image_url(token):
定义一个函式,并且接受一个从网址传来的参数token
。
第五行:return send_from_directory('/tmp/', filename=f'{token}_DualTone.png')
将指定资料夹底下的档案当作呼叫网址的结果,传送回去。这边所选择的档案,当然就是我们先前存在 Heroku 暂存空间的/tmp/{token}_DualTone.png
。
send_from_directory
是flask
所提供的安全传送档案的函式,一般接收 3 个参数,如下所示:
@app.route('/uploads/<filename>')
def download_file(filename):
return send_from_directory(directory=app.config['UPLOAD_FOLDER'],
filename=filename, as_attachment=True)
第一个参数directory
限定了档案存放的资料夹。只有在这个资料夹当中的档案才能被传送给发来请求的使用者。以我们来说,当然是暂存资料夹/tmp/
。第二个参数filename
就是指定的档案名称。第三个参数as_attachment
则可以控制档案是否以附件的形式下载,预设值是False
。这边我们希望直接开启图片档,而不是以附件形式下载,所以不用特别加上这一个参数。
稍微整理一下头绪,是不是觉得所有拼图都拚上了呢?
那麽应该不用我再多说ImageSendMessage
当中的参数该怎麽填了吧:
app/custom_models/AlmaTalks.py
from app.custom_models import AlmaRenders
from linebot.models import TextSendMessage, ImageSendMessage
def phase_finish(event):
user_id = event.source.user_id
postback_data = event.postback.data
current_phase = postback_data.split('=')[0]
record = CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1])
message_content = line_bot_api.get_message_content(record[1])
# 取得使用者发送过来的图片
im = AlmaRenders.get_image(message_content, event.reply_token)
# 执行双色打光
mode = record[2]
gradient_factor = int(record[3])
first_tone = record[4]
second_tone = record[5]
im_dual_tone = AlmaRenders.dual_tone_run(im, mode, gradient_factor, first_tone, second_tone)
# 将双色打光图片存到暂存空间
im_url = AlmaRenders.save_image(im_dual_tone, event.reply_token)
# 将使用者输入的设定回传给使用者
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=str(record))
)
# 将双色打光图片回传给使用者
line_bot_api.push_message(
user_id,
ImageSendMessage(
original_content_url=im_url,
preview_image_url=im_url
)
)
我们的 LINE BOT 真的是非常贴心。根据上面那段程序码,最後使用者不仅会收到自己当初输入的条件,还会拿到一张藉由该条件产生出来的双色打光图片。这时大家或许会问,reply_message
跟push_message
有什麽差别呢?reply_message
是依靠reply_token
来回覆使用者讯息的,而push_message
则是透过user_id
来将讯息传送给指定的使用者。讲到这里,大家可能还是一头雾水,那麽再直白一点好了:reply_message
是免费的,而push_message
则有使用限制,一个月一个 LINE BOT 最多可以推送 500 则讯息,再多就要收钱了。那为什麽不都用reply_message
就好了呢?答案是,reply_token
只能用一次。因此这里的逻辑是这样的:LINE BOT 可以根据使用者发送过来的每一则讯息做出一个回覆 (reply_message
),但根据同一个讯息想要做出第二个回覆,或是想要主动发送讯息给使用者,就只能用push_message
了。
所以我才说我们的 LINE BOT 真的是非常贴心,不惜动用push_message
也要发送两则讯息 (参数跟图片) 给使用者。当然大家在设计的时候不一定要这样做,毕竟使用者在意的只是成果 (图片)?过程如何 (参数) 就随意吧。
那麽来看看今天的成果吧!
图一、双色打光草泥马
图二、感谢 Logos By Nick 的热情教学
好的,相信大家知道接下来又要进入最重要的工商时间了。是的,感谢 iT邦帮忙 和 博硕文化,LINE Bot by Python 全攻略 集结成书了,欢迎有兴趣的大家前往购书喔。
下雨的周六...偷懒最适合... Implement strStr() 题目连结:https://l...
前言 Python是一种易於学习且功能强大的程序语言,可以呼叫使用相当完整的标准资料库,我们也称之为...
commit 版本的时候可以写下一些讯息,以便他人或未来自己查看的时候可以快速理解。但是,有时候写程...
#733 - Flood Fill 连结: https://leetcode.com/proble...
如标题!这篇就是要来聊聊为什麽Flex没有,而grid却有 以下我们都会以讨论justify-sel...