この記事はUnifa Advent Calendar 2022の9日目の記事です。
こんにちは!ユニファでインターンをしている濱口です。
普段は事業推進部システム企画課というところで社内の業務システムの構築や開発のお仕事をしています。最近は社内のR&Dチームと一緒に、社内に散らばっているデータソースを集めてデータウェアハウスの基盤を構築したりしています。
今年もDevチームのアドベントカレンダーに参加させていただいております。
システム企画のメンバーが書いた記事がすでにいくつか公開されていますのでこちらも合わせてご覧ください。
はじめに
最近はオンラインでのミーティングや面接が当たり前になっていますが、毎日画面に向かって会話をしたり作業をするのはとても疲れますよね…
システム企画のメンバーのバーチャル背景はとても賑やかで個性に溢れています。その中でもいま一番流行っているのがドラクエの戦闘画面を模したバーチャル背景です。
スクショの画面の右下の方が使用しているのがドラクエ風のバーチャル背景です。
ある社員さんの手作りで、Google Slidesでパラメータなどを調整してバーチャル背景画像を生成しています。
ゲーム中の戦闘画面では勇者とモンスターのHPやMPが表示されていると思いますが、これを使って画面の左上にいまの自分のHPを表示するようにしました。自分のいまの体力を明示的にすることで、「あ、〇〇さん今日はすごい疲れてるな〜」「元気あり余ってるじゃん!!!」みたいことが一目で分かります。ミーティングが始まる前の軽いアイスブレイクにもなったりしています。
このバーチャル背景をもっと簡単にいろんな人に使ってもらいたいな〜と思ったので、HPやMPのパラメータを入れたら自動で生成されるような仕組みを作ってみたいと思います。そんな頻繁にバーチャル背景を変えることはないと思いますが、ミーティングの合間に簡単に作って変えられたら便利ですよね~笑
ちなみに今回は以下の環境で実行しています。
- macOS Ventura 13.0.1
- PyCharm 2022.3
- Python 3.9.14
- Flask 2.2.2
- OpenCV-Python 4.6.0.66
- Pillow 9.3.0
OpenCVについて
OpenCVは画像処理や画像解析ができるオープンソースのライブラリです。画像や動画の中にある物体を検出したり動きやパターンを認識することができます。今回はPythonを使って実装しますが、OpenCVはPython以外にもC++やJavaなど様々な言語をサポートしています。
僕自身、OpenCVは授業などで少し触れたことがある程度でそこまで詳しくないのと、OpenCVに関する情報は検索するとたくさん出てくるので、ここでの詳細な説明は割愛します。
OpenCVを使ってHPやMPの表記を再現する
さっそくコードを書いていきます。
実装の方針としては、あらかじめサンプルの画像を用意しておき、その画像を読み込んで直線や長方形を画面上に配置して戦闘画面を再現してみたいと思います。
画像ごとにサイズが異なるとオブジェクトの配置が大変なので、まず最初に画像を1920px × 1080pxにリサイズします。
その後にrectangle
、line
、putText
を使ってHPやMP、職業、名前を再現していきます。
import cv2 def putText(img, text, org): cv2.putText( img, text=text, org=org, fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, color=(255, 255, 255), thickness=2, lineType=cv2.LINE_8 ) def generate_image(hp, mp, job, level, name): # 背景画像の読み込み img = cv2.imread("image/sample.jpg") # 1080px × 1920pxにリサイズ img = cv2.resize(img, (1920, 1080)) # HPとMPの背景 cv2.rectangle(img, (100, 110), (430, 480), (3, 3, 3), thickness=-1) # HPとMPのシークバー cv2.rectangle(img, (150, 180), (380, 200), (67, 67, 67), thickness=-1) cv2.rectangle(img, (150, 250), (380, 270), (67, 67, 67), thickness=-1) # HPとMPそれぞれの数値に合わせて長さを調整 hp_lenth = int(230 * hp / 100) mp_lenth = int(230 * mp / 100) cv2.rectangle(img, (150, 180), (150 + hp_lenth, 200), (85, 217, 134), thickness=-1) cv2.rectangle(img, (150, 250), (150 + mp_lenth, 270), (231, 172, 109), thickness=-1) # 職業・レベル・名前の枠表示 cv2.rectangle(img, (140, 290), (390, 450), (255, 255, 255), thickness=2) cv2.line(img, (140, 370), (390, 370), (255, 255, 255), thickness=2) # HPとMPの数値表示 putText(img, "HP", (160, 160)) putText(img, "MP", (160, 240)) putText(img, f"{hp}", (330, 160)) putText(img, f"{mp}", (330, 240)) # 職業・レベル・名前の表示 putText(img, f"{job}", (160, 340)) putText(img, f"Lv {level}", (280, 340)) putText(img, f"{name}", (200, 420)) return img def main(): img = generate_image(34, 50, "盗賊", 25, "なまえ") cv2.imshow("sample", img) cv2.imwrite("image/result.jpg", img) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == "__main__": main()
出来上がった背景画像がこちらです。
なかなかいい感じに出来ているのではないでしょうか!?
本家ドラクエの戦闘画面にはまだ程遠いですが、Google Slidesで作成したバーチャル背景はほぼ再現できているかと思います。ただ一つ問題があって、OpenCVは日本語の描画に対応していないのです…
何かいい方法がないかと調べていたら、Pillowを使って解決されている方がいたのでいらっしゃったので参考にさせていただきました。
Pillowを使って日本語文字列を描画する
NumPyとPillowをインストールしてPythonファイルでimportします。
import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont
テキストを描画するためにフォントファイルが必要となるため、任意のフォントファイルを用意してプロジェクトディレクトリ下に置きます。
putText
関数をPillowを使った描画方法に変えていきます。といってもそんなに難しくはないです。
def putText(img, text, org, fontScale, anchor="lm"): imgPIL = Image.fromarray(img) draw = ImageDraw.Draw(imgPIL) fontPIL = ImageFont.truetype(font="font/GenJyuuGothic-Medium.ttf", size=fontScale) draw.text(xy=org, text=text, fill=(255, 255, 255), font=fontPIL, anchor=anchor) return np.array(imgPIL, dtype=np.uint8)
文字列を表示する部分を新しい関数に合わせて書き換えます。
anchor
は指定した座標に対して左揃え・右揃えにするのかなど、文字列を上下左右に整列させることができます。
def generate_image(hp, mp, job, level, name): # 省略 # HPとMPの数値表示 img = putText(img, "HP", (160, 160), 30) img = putText(img, "MP", (160, 230), 30) img = putText(img, f"{hp}", (370, 160), 30, "rm") # 右端揃え img = putText(img, f"{mp}", (370, 230), 30, "rm") # 右端揃え # 職業・レベル・名前の表示 img = putText(img, f"{job}", (160, 330), 30) img = putText(img, f"Lv {level}", (370, 330), 30, "rm") # 右端揃え img = putText(img, f"{name}", (265, 410), 30, "mm") # 中央揃え return img
そして出力された画像がこちら!しっかり日本語が表示されていることが確認できます。
Pillowで日本語の描画ができるなら最初からOpenCVを使わなくてもよかったじゃん、と思ってしまいましたが悲しくなるので触れないことにします。😭😭😭
Flaskを使ってWebアプリケーションにする
せっかくPythonを使って画像処理を行っているので、Flaskを導入して誰でも簡単に使えるWebアプリを作ってみたいと思います。ご存知の方がほとんどだと思いますが、FlaskはPythonでWebアプリを作るためのマイクロフレームワークです。Flaskについての説明や実装の仕方はたくさんの記事が公開されていると思うので、ここでの詳細な説明は割愛します。
pipでFlaskをインストールします。そして作業用のフォルダを作成してPythonファイルを作成します。
僕はPyCharmを使っているので、PyCharmさんにディレクトリ構成とサンプルコードを作成してもらいます。
余談ですが、最近JetBrains系のIDEの最新バージョンが2022.3になって新しいUIのプレビュー版が公開されましたね。僕はさっそく新しいUIに変更して使っています。
Go loverの友人はGoLandの起動時に出てくる絵が可愛い!と喜んでいました笑
本題に戻って実装を続けます。
トップページと、パラメータを受け取って画像を生成する2つのルーティングを設定します。バーチャル背景を生成するコードは別のファイルに記述してimportしています。
/generate
にPOSTリクエストを送るとバーチャル背景が生成されてレスポンスされるようにしています。HPやMPなどの情報はクエリパラメータにのせて送っています。
from flask import Flask, render_template, send_file, request from common.main import generate_image from io import BytesIO import numpy as np app = Flask(__name__) @app.route('/', methods=['GET']) def index(): return render_template('index.html') @app.route('/generate', methods=['POST']) def generate(): hp = int(request.args.get('hp')) mp = int(request.args.get('mp')) job = request.args.get('job') level = request.args.get('level') name = request.args.get('name') try: stream = request.files['image'].stream array = np.asarray(bytearray(stream.read()), dtype=np.uint8) except Exception as e: array = np.asarray([]) img = generate_image(array, hp, mp, job, level, name) return send_file( BytesIO(img), as_attachment=True, download_name='output.jpg', mimetype='image/jpeg', ) if __name__ == '__main__': app.run()
トップページはindex.html
で定義しています。とりあえずで作ったので雑なデザインですいません…。Vue.jsもちょこっと使っています。
HP、MP、職業、レベル、名前を入力し、背景にしたい画像を選択してボタンを押すと、ドラクエ風バーチャル背景がダウンロードできます!
Webアプリとしてはこれで完成です!チームの方に使っていただこうと思います。
おわりに
最後までご覧いただきありがとうございました。
最初にOpenCVの説明をしたものの、本来のOpenCVらしい使い方は一回もせずPillowだけで実装できてしまうような結果になっしまいましたが、バーチャル背景を再現するという目的は達成できたのでいいことにします。笑
Webアプリがいい感じに形になったのでデプロイを試みましたがうまく動かなかったので、デプロイがうまくいったら改めてURLを公開したいと思います〜
僕が初めてドラクエをやったのは小学校1年生の時で、義理の叔父の家にあったPS2でドラゴンクエストⅧをやっていました。自分の家にはテレビゲームがなかったので、叔父の家に泊まりに行くたびにコツコツ続けて、約3年かけて全クリしました!笑
当時は読めない漢字も多く、ほとんど朗読してもらいながらストーリーを進めていました。いま考えたらとても迷惑な話ですよね笑
メンバー募集中
ユニファでは一緒に働いていただけるメンバーを積極的に募集しています!
保育のHackに興味がある方、お待ちしております!
ブランドサイト lookmee.jp
採用情報 unifa-e.com