[ref] overlayのリファクタリングとテストを追加

This commit is contained in:
misyaguziya
2025-10-09 16:43:41 +09:00
parent c1cf78cda4
commit 569d8e3f76
5 changed files with 175 additions and 57 deletions

View File

@@ -3,7 +3,7 @@
目的: OpenVR を使ったオーバーレイ表示(複数サイズ: small/largeを管理する `Overlay` クラスを提供します。 目的: OpenVR を使ったオーバーレイ表示(複数サイズ: small/largeを管理する `Overlay` クラスを提供します。
主要メソッド: 主要メソッド:
- __init__(self, settings_dict) - __init__(self, settings_dict: dict)
- init(self) -> None - init(self) -> None
- startOverlay(self) -> None - startOverlay(self) -> None
- shutdownOverlay(self) -> None - shutdownOverlay(self) -> None
@@ -18,6 +18,34 @@
- OpenVR (SteamVR) が稼働していることが前提です。`checkSteamvrRunning()``vrmonitor.exe` の存在チェックを行います。 - OpenVR (SteamVR) が稼働していることが前提です。`checkSteamvrRunning()``vrmonitor.exe` の存在チェックを行います。
- 例外が発生した場合は `errorLogging()` を呼んでスタックトレースを残します。 - 例外が発生した場合は `errorLogging()` を呼んでスタックトレースを残します。
短い使用例:
```py
from models.overlay.overlay_image import OverlayImage
from models.overlay.overlay import Overlay
from PIL import Image
settings = {
"small": {
"x_pos": 0.0, "y_pos": 0.0, "z_pos": 0.0,
"x_rotation": 0.0, "y_rotation": 0.0, "z_rotation": 0.0,
"display_duration": 5, "fadeout_duration": 2,
"opacity": 1.0, "ui_scaling": 1.0, "tracker": "HMD"
}
}
overlay_img = OverlayImage()
overlay = Overlay(settings)
overlay.startOverlay()
# wait until initialized
while not overlay.initialized:
time.sleep(0.5)
# push a simple blank image
overlay.updateImage(Image.new("RGBA", (256, 64), (255,255,255,255)), "small")
```
## モジュール構成(補足) ## モジュール構成(補足)
- overlay.py — OpenVR を使ったオーバーレイ管理。Overlay クラスは複数サイズsmall/largeを扱い、位置/回転/透明度/フェードを制御する。 - overlay.py — OpenVR を使ったオーバーレイ管理。Overlay クラスは複数サイズsmall/largeを扱い、位置/回転/透明度/フェードを制御する。

View File

@@ -3,6 +3,8 @@ import ctypes
import time import time
from psutil import process_iter from psutil import process_iter
from threading import Thread from threading import Thread
from typing import Any, Dict, Optional, Sequence
import openvr import openvr
import numpy as np import numpy as np
from PIL import Image from PIL import Image
@@ -18,14 +20,26 @@ try:
except ImportError: except ImportError:
import overlay_utils as utils import overlay_utils as utils
def mat34Id(array): def mat34Id(array: Sequence[Sequence[float]]) -> Any:
"""Convert a 3x4 nested sequence into an openvr.HmdMatrix34_t instance.
Args:
array: 3x4 numeric sequence
Returns:
openvr HmdMatrix34_t compatible object
"""
arr = openvr.HmdMatrix34_t() arr = openvr.HmdMatrix34_t()
for i in range(3): for i in range(3):
for j in range(4): for j in range(4):
arr[i][j] = array[i][j] arr[i][j] = array[i][j]
return arr return arr
def getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation): def getBaseMatrix(x_pos: float, y_pos: float, z_pos: float, x_rotation: float, y_rotation: float, z_rotation: float) -> np.ndarray:
"""Create a 3x4 base matrix for an overlay given position and Euler rotations.
Returns a numpy array of shape (3,4).
"""
arr = np.zeros((3, 4)) arr = np.zeros((3, 4))
rot = utils.euler_to_rotation_matrix((x_rotation, y_rotation, z_rotation)) rot = utils.euler_to_rotation_matrix((x_rotation, y_rotation, z_rotation))
@@ -38,7 +52,7 @@ def getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation):
arr[2][3] = - z_pos arr[2][3] = - z_pos
return arr return arr
def getHMDBaseMatrix(): def getHMDBaseMatrix() -> np.ndarray:
x_pos = 0.0 x_pos = 0.0
y_pos = -0.4 y_pos = -0.4
z_pos = 1.0 z_pos = 1.0
@@ -48,7 +62,7 @@ def getHMDBaseMatrix():
arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation) arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation)
return arr return arr
def getLeftHandBaseMatrix(): def getLeftHandBaseMatrix() -> np.ndarray:
x_pos = 0.3 x_pos = 0.3
y_pos = 0.1 y_pos = 0.1
z_pos = -0.31 z_pos = -0.31
@@ -58,7 +72,7 @@ def getLeftHandBaseMatrix():
arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation) arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation)
return arr return arr
def getRightHandBaseMatrix(): def getRightHandBaseMatrix() -> np.ndarray:
x_pos = -0.3 x_pos = -0.3
y_pos = 0.1 y_pos = 0.1
z_pos = -0.31 z_pos = -0.31
@@ -69,24 +83,25 @@ def getRightHandBaseMatrix():
return arr return arr
class Overlay: class Overlay:
def __init__(self, settings_dict): """Manage OpenVR overlays for multiple sizes (e.g. 'small'/'large')."""
self.system = None def __init__(self, settings_dict: Dict[str, Dict[str, Any]]) -> None:
self.overlay = None self.system: Optional[Any] = None
self.handle = None self.overlay: Optional[Any] = None
self.init_process = False self.handle: Dict[str, Any] = {}
self.initialized = False self.init_process: bool = False
self.loop = False self.initialized: bool = False
self.thread_overlay = None self.loop: bool = False
self.thread_overlay: Optional[Thread] = None
self.settings = {} self.settings: Dict[str, Dict[str, Any]] = {}
self.lastUpdate = {} self.lastUpdate: Dict[str, float] = {}
self.fadeRatio = {} self.fadeRatio: Dict[str, float] = {}
for key, value in settings_dict.items(): for key, value in settings_dict.items():
self.settings[key] = value self.settings[key] = value
self.lastUpdate[key] = time.monotonic() self.lastUpdate[key] = time.monotonic()
self.fadeRatio[key] = 1 self.fadeRatio[key] = 1.0
def init(self): def init(self) -> None:
try: try:
self.system = openvr.init(openvr.VRApplication_Background) self.system = openvr.init(openvr.VRApplication_Background)
self.overlay = openvr.IVROverlay() self.overlay = openvr.IVROverlay()
@@ -119,7 +134,7 @@ class Overlay:
except Exception: except Exception:
errorLogging() errorLogging()
def updateImage(self, img, size): def updateImage(self, img: Image.Image, size: str) -> None:
if self.initialized is True: if self.initialized is True:
width, height = img.size width, height = img.size
img = img.tobytes() img = img.tobytes()
@@ -139,7 +154,7 @@ class Overlay:
self.updateOpacity(self.settings[size]["opacity"], size) self.updateOpacity(self.settings[size]["opacity"], size)
self.lastUpdate[size] = time.monotonic() self.lastUpdate[size] = time.monotonic()
def clearImage(self, size): def clearImage(self, size: str) -> None:
if self.initialized is True: if self.initialized is True:
self.updateImage(Image.new("RGBA", (1, 1), (0, 0, 0, 0)), size) self.updateImage(Image.new("RGBA", (1, 1), (0, 0, 0, 0)), size)
@@ -151,7 +166,7 @@ class Overlay:
r, g, b = col r, g, b = col
self.overlay.setOverlayColor(self.handle[size], r, g, b) self.overlay.setOverlayColor(self.handle[size], r, g, b)
def updateOpacity(self, opacity, size, with_fade=False): def updateOpacity(self, opacity: float, size: str, with_fade: bool = False) -> None:
self.settings[size]["opacity"] = opacity self.settings[size]["opacity"] = opacity
if self.initialized is True: if self.initialized is True:
@@ -161,12 +176,12 @@ class Overlay:
else: else:
self.overlay.setOverlayAlpha(self.handle[size], self.settings[size]["opacity"]) self.overlay.setOverlayAlpha(self.handle[size], self.settings[size]["opacity"])
def updateUiScaling(self, ui_scaling, size): def updateUiScaling(self, ui_scaling: float, size: str) -> None:
self.settings[size]["ui_scaling"] = ui_scaling self.settings[size]["ui_scaling"] = ui_scaling
if self.initialized is True: if self.initialized is True:
self.overlay.setOverlayWidthInMeters(self.handle[size], self.settings[size]["ui_scaling"]) self.overlay.setOverlayWidthInMeters(self.handle[size], self.settings[size]["ui_scaling"])
def updatePosition(self, x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation, tracker, size): def updatePosition(self, x_pos: float, y_pos: float, z_pos: float, x_rotation: float, y_rotation: float, z_rotation: float, tracker: str, size: str) -> None:
""" """
x_pos, y_pos, z_pos are floats representing the position of overlay x_pos, y_pos, z_pos are floats representing the position of overlay
x_rotation, y_rotation, z_rotation are floats representing the rotation of overlay x_rotation, y_rotation, z_rotation are floats representing the rotation of overlay
@@ -208,13 +223,13 @@ class Overlay:
transform transform
) )
def updateDisplayDuration(self, display_duration, size): def updateDisplayDuration(self, display_duration: float, size: str) -> None:
self.settings[size]["display_duration"] = display_duration self.settings[size]["display_duration"] = display_duration
def updateFadeoutDuration(self, fadeout_duration, size): def updateFadeoutDuration(self, fadeout_duration: float, size: str) -> None:
self.settings[size]["fadeout_duration"] = fadeout_duration self.settings[size]["fadeout_duration"] = fadeout_duration
def checkActive(self): def checkActive(self) -> bool:
try: try:
if self.system is not None and self.initialized is True: if self.system is not None and self.initialized is True:
new_event = openvr.VREvent_t() new_event = openvr.VREvent_t()
@@ -226,7 +241,7 @@ class Overlay:
errorLogging() errorLogging()
return False return False
def evaluateOpacityFade(self, size): def evaluateOpacityFade(self, size: str) -> None:
currentTime = time.monotonic() currentTime = time.monotonic()
if (currentTime - self.lastUpdate[size]) > self.settings[size]["display_duration"]: if (currentTime - self.lastUpdate[size]) > self.settings[size]["display_duration"]:
timeThroughInterval = currentTime - self.lastUpdate[size] - self.settings[size]["display_duration"] timeThroughInterval = currentTime - self.lastUpdate[size] - self.settings[size]["display_duration"]
@@ -235,13 +250,13 @@ class Overlay:
self.fadeRatio[size] = 0 self.fadeRatio[size] = 0
self.overlay.setOverlayAlpha(self.handle[size], self.fadeRatio[size] * self.settings[size]["opacity"]) self.overlay.setOverlayAlpha(self.handle[size], self.fadeRatio[size] * self.settings[size]["opacity"])
def update(self, size): def update(self, size: str) -> None:
if self.settings[size]["fadeout_duration"] != 0: if self.settings[size]["fadeout_duration"] != 0:
self.evaluateOpacityFade(size) self.evaluateOpacityFade(size)
else: else:
self.updateOpacity(self.settings[size]["opacity"], size) self.updateOpacity(self.settings[size]["opacity"], size)
def mainloop(self): def mainloop(self) -> None:
self.loop = True self.loop = True
while self.checkActive() is True and self.loop is True: while self.checkActive() is True and self.loop is True:
startTime = time.monotonic() startTime = time.monotonic()
@@ -251,21 +266,21 @@ class Overlay:
if sleepTime > 0: if sleepTime > 0:
time.sleep(sleepTime) time.sleep(sleepTime)
def main(self): def main(self) -> None:
while self.checkSteamvrRunning() is False: while self.checkSteamvrRunning() is False:
time.sleep(10) time.sleep(10)
self.init() self.init()
if self.initialized is True: if self.initialized is True:
self.mainloop() self.mainloop()
def startOverlay(self): def startOverlay(self) -> None:
if self.initialized is False and self.init_process is False: if self.initialized is False and self.init_process is False:
self.init_process = True self.init_process = True
self.thread_overlay = Thread(target=self.main) self.thread_overlay = Thread(target=self.main)
self.thread_overlay.daemon = True self.thread_overlay.daemon = True
self.thread_overlay.start() self.thread_overlay.start()
def shutdownOverlay(self): def shutdownOverlay(self) -> None:
if self.initialized is True and self.init_process is False: if self.initialized is True and self.init_process is False:
if isinstance(self.thread_overlay, Thread): if isinstance(self.thread_overlay, Thread):
self.loop = False self.loop = False
@@ -281,7 +296,7 @@ class Overlay:
self.system = None self.system = None
self.initialized = False self.initialized = False
def reStartOverlay(self): def reStartOverlay(self) -> None:
self.shutdownOverlay() self.shutdownOverlay()
self.startOverlay() self.startOverlay()

View File

@@ -1,6 +1,6 @@
from os import path as os_path from os import path as os_path
from datetime import datetime from datetime import datetime
from typing import Tuple from typing import Tuple, List, Optional
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
try: try:
from utils import errorLogging from utils import errorLogging
@@ -18,8 +18,14 @@ class OverlayImage:
"Chinese Traditional": "NotoSansTC-Regular.ttf", "Chinese Traditional": "NotoSansTC-Regular.ttf",
} }
def __init__(self, root_path: str=None): def __init__(self, root_path: Optional[str] = None) -> None:
self.message_log = [] """Overlay image helper.
Args:
root_path: optional project root to resolve bundled fonts. If omitted,
defaults to repository `fonts` directory.
"""
self.message_log: List[dict] = []
if root_path is None: if root_path is None:
self.root_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts") self.root_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts")
else: else:
@@ -58,7 +64,7 @@ class OverlayImage:
} }
return colors return colors
def createTextboxSmallLog(self, text:str, language:str, text_color:tuple, base_width:int, base_height:int, font_size:int) -> Image: def createTextboxSmallLog(self, text: str, language: str, text_color: Tuple[int, int, int], base_width: int, base_height: int, font_size: int) -> Image:
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"]) font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
img = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0)) img = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
@@ -92,7 +98,7 @@ class OverlayImage:
draw.text((text_x, text_y), text, text_color, anchor="mm", stroke_width=0, font=font, align="center") draw.text((text_x, text_y), text, text_color, anchor="mm", stroke_width=0, font=font, align="center")
return img return img
def createOverlayImageSmallLog(self, message: str, your_language: str, translation: list = [], target_language: list = []) -> Image: def createOverlayImageSmallLog(self, message: str, your_language: str, translation: List[str] = [], target_language: List[str] = []) -> Image:
# UI設定を取得 # UI設定を取得
ui_size = self.getUiSizeSmallLog() ui_size = self.getUiSizeSmallLog()
width, height, font_size = ui_size["width"], ui_size["height"], ui_size["font_size"] width, height, font_size = ui_size["width"], ui_size["height"], ui_size["font_size"]
@@ -162,7 +168,7 @@ class OverlayImage:
"text_color_time": (120, 120, 120) "text_color_time": (120, 120, 120)
} }
def createTextImageLargeLog(self, message_type:str, size:str, text:str, language:str) -> Image: def createTextImageLargeLog(self, message_type: str, size: str, text: str, language: str) -> Image:
ui_size = self.getUiSizeLargeLog() ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"] font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
text_color = self.getUiColorLargeLog()[f"text_color_{size}"] text_color = self.getUiColorLargeLog()[f"text_color_{size}"]
@@ -200,7 +206,7 @@ class OverlayImage:
draw.multiline_text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font, align=align) draw.multiline_text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font, align=align)
return img return img
def createTextImageMessageType(self, message_type:str, date_time:str) -> Image: def createTextImageMessageType(self, message_type: str, date_time: str) -> Image:
ui_size = self.getUiSizeLargeLog() ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_small"] font_size = ui_size["font_size_small"]
ui_padding = ui_size["padding"] ui_padding = ui_size["padding"]
@@ -242,7 +248,7 @@ class OverlayImage:
draw.text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font) draw.text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font)
return img return img
def createTextboxLargeLog(self, message_type: str, message: str = None, your_language: str = None, translation: list = [], target_language: list = [], date_time: str = None) -> Image: def createTextboxLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = [], date_time: Optional[str] = None) -> Image:
# テキスト画像のリストを作成 # テキスト画像のリストを作成
images = [self.createTextImageMessageType(message_type, date_time)] images = [self.createTextImageMessageType(message_type, date_time)]
@@ -272,7 +278,7 @@ class OverlayImage:
return combined_img return combined_img
def createOverlayImageLargeLog(self, message_type:str, message:str=None, your_language:str=None, translation:list=[], target_language:list=[]) -> Image: def createOverlayImageLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = []) -> Image:
ui_color = self.getUiColorLargeLog() ui_color = self.getUiColorLargeLog()
background_color = ui_color["background_color"] background_color = ui_color["background_color"]
background_outline_color = ui_color["background_outline_color"] background_outline_color = ui_color["background_outline_color"]

View File

@@ -1,11 +1,29 @@
import numpy as np import numpy as np
from typing import Sequence
def toHomogeneous(matrix):
def toHomogeneous(matrix: np.ndarray) -> np.ndarray:
"""Convert a 3x4 base matrix to a 4x4 homogeneous matrix.
Args:
matrix: 3x4 numpy array
Returns:
4x4 numpy array with last row [0, 0, 0, 1]
"""
homogeneous_matrix = np.vstack([matrix, [0, 0, 0, 1]]) homogeneous_matrix = np.vstack([matrix, [0, 0, 0, 1]])
return homogeneous_matrix return homogeneous_matrix
# 移動行列を生成する関数 # 移動行列を生成する関数
def calcTranslationMatrix(translation): def calcTranslationMatrix(translation: Sequence[float]) -> np.ndarray:
"""Create a 4x4 translation matrix from a 3-element translation.
Args:
translation: (tx, ty, tz)
Returns:
4x4 numpy translation matrix
"""
tx, ty, tz = translation tx, ty, tz = translation
return np.array([ return np.array([
[1, 0, 0, tx], [1, 0, 0, tx],
@@ -15,9 +33,10 @@ def calcTranslationMatrix(translation):
]) ])
# X軸周りの回転行列を生成する関数 # X軸周りの回転行列を生成する関数
def calcRotationMatrixX(angle): def calcRotationMatrixX(angle: float) -> np.ndarray:
c = np.cos(np.pi/180*angle) """Rotation matrix around X axis for given angle in degrees."""
s = np.sin(np.pi/180*angle) c = np.cos(np.pi / 180 * angle)
s = np.sin(np.pi / 180 * angle)
return np.array([ return np.array([
[1, 0, 0, 0], [1, 0, 0, 0],
[0, c, -s, 0], [0, c, -s, 0],
@@ -26,9 +45,10 @@ def calcRotationMatrixX(angle):
]) ])
# Y軸周りの回転行列を生成する関数 # Y軸周りの回転行列を生成する関数
def calcRotationMatrixY(angle): def calcRotationMatrixY(angle: float) -> np.ndarray:
c = np.cos(np.pi/180*angle) """Rotation matrix around Y axis for given angle in degrees."""
s = np.sin(np.pi/180*angle) c = np.cos(np.pi / 180 * angle)
s = np.sin(np.pi / 180 * angle)
return np.array([ return np.array([
[c, 0, s, 0], [c, 0, s, 0],
[0, 1, 0, 0], [0, 1, 0, 0],
@@ -37,9 +57,10 @@ def calcRotationMatrixY(angle):
]) ])
# Z軸周りの回転行列を生成する関数 # Z軸周りの回転行列を生成する関数
def calcRotationMatrixZ(angle): def calcRotationMatrixZ(angle: float) -> np.ndarray:
c = np.cos(np.pi/180*angle) """Rotation matrix around Z axis for given angle in degrees."""
s = np.sin(np.pi/180*angle) c = np.cos(np.pi / 180 * angle)
s = np.sin(np.pi / 180 * angle)
return np.array([ return np.array([
[c, -s, 0, 0], [c, -s, 0, 0],
[s, c, 0, 0], [s, c, 0, 0],
@@ -48,7 +69,17 @@ def calcRotationMatrixZ(angle):
]) ])
# 3x4行列の座標を基準として回転や移動を行う関数 # 3x4行列の座標を基準として回転や移動を行う関数
def transform_matrix(base_matrix, translation, rotation): def transform_matrix(base_matrix: np.ndarray, translation: Sequence[float], rotation: Sequence[float]) -> np.ndarray:
"""Apply translation and Euler rotations to a 3x4 base matrix.
Args:
base_matrix: 3x4 base transform matrix
translation: (tx, ty, tz)
rotation: (x_deg, y_deg, z_deg)
Returns:
Transformed 3x4 matrix (numpy.ndarray)
"""
homogeneous_base_matrix = toHomogeneous(base_matrix) homogeneous_base_matrix = toHomogeneous(base_matrix)
translation_matrix = calcTranslationMatrix(translation) translation_matrix = calcTranslationMatrix(translation)
rotation_matrix_x = calcRotationMatrixX(rotation[0]) rotation_matrix_x = calcRotationMatrixX(rotation[0])
@@ -60,10 +91,18 @@ def transform_matrix(base_matrix, translation, rotation):
result_matrix = np.dot(homogeneous_base_matrix, transformation_matrix) result_matrix = np.dot(homogeneous_base_matrix, transformation_matrix)
return result_matrix[:3, :] return result_matrix[:3, :]
def euler_to_rotation_matrix(angles): def euler_to_rotation_matrix(angles: Sequence[float]) -> np.ndarray:
"""Convert Euler angles in degrees to a 3x3 rotation matrix.
Args:
angles: (x_deg, y_deg, z_deg)
Returns:
3x3 rotation matrix
"""
phi = angles[0] * np.pi / 180 phi = angles[0] * np.pi / 180
theta = angles[1] * np.pi / 180 theta = angles[1] * np.pi / 180
psi = angles[2]* np.pi / 180 psi = angles[2] * np.pi / 180
R_x = np.array([[1, 0, 0], R_x = np.array([[1, 0, 0],
[0, np.cos(phi), -np.sin(phi)], [0, np.cos(phi), -np.sin(phi)],
[0, np.sin(phi), np.cos(phi)]]) [0, np.sin(phi), np.cos(phi)]])

View File

@@ -0,0 +1,30 @@
import sys
import time
from PIL import Image
sys.path.append(r"d:\WORKSPACE\WORK\VRChatProject\VRCT\src-python")
from models.overlay import overlay_image, overlay_utils
def test_overlay_image_create():
oi = overlay_image.OverlayImage()
img = oi.createOverlayImageSmallLog("hello", "English", [], [])
assert isinstance(img, Image.Image)
def test_utils_transform():
import numpy as np
base = np.array([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0]
])
res = overlay_utils.transform_matrix(base, (0, 0, 0), (0, 0, 0))
assert res.shape == (3, 4)
if __name__ == '__main__':
test_overlay_image_create()
test_utils_transform()
print('tests passed')