概要
Owlcat gamesからD&D派生のパスファインダーTRPGを元にしたCRPGが発売されている。
ゲームそのものは素晴らしい出来なのだが、いかんせんポートレートの数が少ない。
公開されてるModは趣味が偏ってるし、自前のポートレート作成を簡単にやりたいんじゃ。
ロードス島とかゴブスレ再現パーティーで冒険したい。
あとアンフェア難易度の癒しが欲しい
作業環境
ハード:
CPU : 11th Gen Intel(R) Core(TM) i7-11370H @ 3.30GHz 3.00 GHz
GPU : NVIDIA GeForce RTX 3070 Laptop (コードでは未使用)
RAM : 40.0GB
ソフト:
OS : windows10 64bit
pyenv : 2.64.8
python : 3.8.9
Visual Studio Code : 1.60.2
使用ライブラリ:
numpy : 1.21.2
opencv-python : 4.5.3.56
ポートレートの仕様について
- Fulllength, Medium, Smallの3枚の画像が必要
- それぞれには既定かつ変更不可のサイズ、形式(PNG)がある
- 画像のファイル名も変更不可
- 制限を満たすなら別々の画像から切り出してもよい
デフォルトで入っている説明ファイル
Nexus Modに公開されているポートレートModの一例
機能要件
必須機能:
- 1枚の入力画像をサイズごとに切り出して保存する
- GUIで選択範囲確認しながら操作したい
あると便利な機能:
- 画像のリサイズ >> 大きすぎる画像を小さくしたり、小さい画像を大きくしたり
- 画像を切り出す枠のリサイズ >> 枠が固定だとはみ出たりするかも
実装
import cv2
import numpy as np
import os
import glob
def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8):
"""
https://qiita.com/SKYS/items/cbde3775e2143cad7455参照
エラー吐いてくれるように変更
Parameters
----------
filename : str
ファイル名.
flags : int or cv2 flag, optional
読み込み色の指定. The default is cv2.IMREAD_COLOR.
dtype : np.type, optional
読み込み時のデータ型指定.
C言語規格なのでnpから渡す. The default is np.uint8.
Returns
-------
img : 2d array
読み込み画像.
"""
n = np.fromfile(filename, dtype)
img = cv2.imdecode(n, flags)
return img
def imwrite(filename, img, params=None):
"""
Parameters
----------
filename : str
ファイル名.
img : 2d array
書き込みたい画像データ.
params : optional
imencodeに渡すパラメータ.形式やクオリティの指定. The default is None.
ex)
params = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
quality: int
1~100まで.高いほど高品質できれい.
Returns
-------
bool
"""
ext = os.path.splitext(filename)[1]
result, n = cv2.imencode(ext, img, params)
if result:
with open(filename, mode="w+b") as f:
n.tofile(f)
return True
else:
return False
class application:
def __init__(self, img_size=(954, 1792), result_dir="result"):
"""
初期設定
Parameters
----------
img_size : tuple(int, int), optional
ウィンドウ表示する画像のサイズ.
自分のモニター画面サイズより少し小さくすると見やすい.
The default is (954, 1792). >> 1080*1920モニター
result_dir : str, optional
. The default is "result".
Returns
-------
None.
"""
self.org_img = None
self.img = None
self.resize_img = None
self.savedir = None
self.result_dir = result_dir
self.drawing = False
self.setup = False
self.mode = "Fulllength"
self.IMG_SIZE = img_size
GREEN = (0, 255, 0)
RED = (0, 0, 255)
BLUE = (255, 0, 0)
self.COLORPALET = {
"Small": GREEN,
"Medium": RED,
"Fulllength": BLUE,
}
self.RECT_SIZE = {
"Small": None,
"Medium": None,
"Fulllength": None,
}
self.SIZE = {
"Small": (185, 242),
"Medium": (330, 432),
"Fulllength": (692, 1024),
}
def resize(self):
"""
IMG_SIZEの設定値と入力画像サイズを比べて、モード指定後リサイズする
画面の方が大きい >> 元画像を拡大する >> cv2.INTER_CUBIC
画像の方が大きい >> 元画像を縮小する >> cv2.INTER_AREA
拡大縮小時のモード指定について(opencv tutorial 画像の幾何変換)
https://whitewell.sakura.ne.jp/OpenCV/py_tutorials/py_imgproc/py_geometric_transformations/py_geometric_transformations.html#geometric-transformations
self.fは枠サイズの倍率 fxに変えてもいい
Returns
-------
bool
DESCRIPTION.
"""
Y, X = self.IMG_SIZE
y, x = self.org_img.shape[:2]
fx = Y / y
if Y > y:
mode = cv2.INTER_CUBIC
elif y > Y:
mode = cv2.INTER_AREA
else:
return False
self.f = 0.92 # fx
self.resize_img = cv2.resize(
self.org_img,
dsize=None,
fx=fx,
fy=fx,
interpolation=mode,
)
self.img = self.resize_img.copy()
return True
def draw_rect(self):
"""
枠の描画関数
Returns
-------
None.
"""
self.img = self.resize_img.copy()
size_x, size_y = self.SIZE[self.mode]
rect_x, rect_y = (
int(self.ix + size_x * self.f) + 1,
int(self.iy + size_y * self.f) + 1,
)
self.RECT_SIZE[self.mode] = (self.ix, self.iy, rect_x, rect_y)
for rect_mode in self.RECT_SIZE.keys():
if self.RECT_SIZE[rect_mode] is not None:
x, y, xx, yy = self.RECT_SIZE[rect_mode]
cv2.rectangle(
self.img,
(x, y),
(xx, yy),
self.COLORPALET[rect_mode],
1,
)
def rect_func(self, x):
"""
run関数でself.setupがTrueになっていなければ実行しない.
Parameters
----------
x : TYPE
DESCRIPTION.
Returns
-------
None.
"""
if self.setup:
self.f = cv2.getTrackbarPos("scope", "track bar") / 1000
self.draw_rect()
def img_resize(self, x):
self.f_r = cv2.getTrackbarPos("resize", "image") / 1000
self.resize_img = cv2.resize(
self.org_img,
dsize=None,
fx=self.f_r,
fy=self.f_r,
interpolation=cv2.INTER_AREA,
)
self.img = self.resize_img.copy()
def saveimg(self):
"""
画像をPNGで出力する.
allでフラグチェックし、3枚そろっていなければ警告文と足りない枠サイズを表示する.
Returns
-------
None.
"""
if all([self.RECT_SIZE[_key] for _key in self.RECT_SIZE.keys()]):
self.make_savedir(self.savedir)
for rect_mode in self.RECT_SIZE.keys():
x, y, xx, yy = self.RECT_SIZE[rect_mode]
convert_rect = self.SIZE[rect_mode]
if convert_rect[0] > xx - x:
mode = cv2.INTER_CUBIC
else:
mode = cv2.INTER_AREA
convert_img = cv2.resize(
self.resize_img[y:yy, x:xx],
convert_rect,
interpolation=mode,
)
imwrite(os.path.join(self.savedir, f"{rect_mode}.png"), convert_img)
else:
print("Save command must be selected in 3 modes.")
[
print(f"None select : {_key}")
for _key in self.RECT_SIZE.keys()
if self.RECT_SIZE[_key] is None
]
def make_savedir(self, dirname):
os.makedirs(
dirname,
exist_ok=True,
)
def mouse(self, event, x, y, flags, param):
"""
マウスコールバック関数
イベントタイプや座標を取得し他の処理へ渡す
Parameters
----------
event :
opencvのマウス動作タイプ
x : int
イベント時のx座標.
y : int
イベント時のy座標.
flags :
key同時押し操作フラグの論理和.
param :
コールバック関数へ渡される引数.
参照渡しされているので渡し元の変数を書き換える.
クラス構造で設計しているので未使用
Returns
-------
None.
"""
if event == cv2.EVENT_LBUTTONDOWN:
self.drawing = True
self.ix, self.iy = x, y
self.draw_rect()
elif event == cv2.EVENT_MOUSEMOVE:
if self.drawing == True:
self.ix, self.iy = x, y
self.draw_rect()
elif event == cv2.EVENT_LBUTTONUP:
self.drawing = False
def run(self, img_path):
"""
処理したい画像1枚のパスを渡すと、画像を表示する.
画像中の任意位置をクリックすることで切り出し位置の指定ができる.
key操作:
s : Small枠の指定モード. 枠:緑色
m : Medium枠の指定モード. 枠:赤色
f : Fulllength枠の指定モード. 起動直後の初期設定モード. 枠:青色
1 : save
r : 枠の指定、リサイズ用トラックバー等の全リセット、初期化
q or ESC : 終了
track bar操作:
track bar は1000で割った値を倍率として切り出し枠の調整を行う.
ただし、現在指定しているモードの枠のみに影響する.
Parameters
----------
img_path : str
Returns
-------
None.
"""
self.org_img = imread(img_path)
self.img = self.org_img.copy()
self.savedir = os.path.join(
self.result_dir, os.path.splitext(os.path.basename(img_path))[0]
)
cv2.namedWindow("image")
cv2.namedWindow("track bar")
cv2.setMouseCallback("image", self.mouse)
cv2.createTrackbar("scope", "track bar", 920, 3000, self.rect_func)
self.resize()
self.setup = True
while 1:
cv2.imshow("image", self.img)
k = cv2.waitKey(1) & 0xFF
if k == 27 or k == ord("q"):
break
elif k == ord("s"):
self.mode = "Small"
elif k == ord("m"):
self.mode = "Medium"
elif k == ord("f"):
self.mode = "Fulllength"
elif k == ord("1"):
self.saveimg()
elif k == ord("r"):
self.RECT_SIZE = {
"Small": None,
"Medium": None,
"Fulllength": None,
}
self.resize()
cv2.destroyAllWindows()
def extcheck(string, exts=("jpeg", "jpg", "png", "bmp")):
"""
globで集めたファイルの拡張子チェック関数.
指定はすべて小文字で行う.
Parameters
----------
string : str
ファイル名.
exts : tuple(str, ...) or list(str, ...), optional
抜き出したい拡張子名をタプル、または配列で指定する.
The default is ("jpeg", "jpg", "png", "bmp").
Returns
-------
bool
"""
for ext in exts:
if string.lower().endswith(ext):
return True
return False
if __name__ == "__main__":
_files = glob.glob(r"requests\**", recursive=True)
files = [file for file in _files if extcheck(file)]
for file in files:
app = application()
app.run(file)
del app
使い方
key操作:
s : Small枠の指定モード. 枠:緑色
m : Medium枠の指定モード. 枠:赤色
f : Fulllength枠の指定モード. 起動直後の初期設定モード. 枠:青色
1 : save
r : 枠の指定、リサイズ用トラックバー等の全リセット、初期化
q or ESC : 終了 (複数画像ある場合は次の画像を自動表示)
track bar操作:
track bar は1000で割った値を倍率として切り出し枠の調整を行う.
ただし、現在指定しているモードの枠のみに影響する.
-
適当な画像をrequestsフォルダに入れる(複数可)
https://hobbyjapan.co.jp/dd/column/column180228_02.html
D&D公式から借用したエルフ大好きなソス卿 -
やり直したい場合、各モードを切り替えてクリックすると枠の位置を調整できる
-
最初からやりなおしたい場合はrキーを押す
-
C:\Users\AppData\LocalLow\Owlcat Games\Pathfinder Kingmaker\Portraitsにフォルダを置く