feat(core): 新增一些基础数据类

包括向量、点、矩形、图像等
This commit is contained in:
XcantloadX 2025-05-12 09:12:50 +08:00
parent 1d177be348
commit 2999367415
3 changed files with 370 additions and 0 deletions

View File

@ -0,0 +1,17 @@
from .visual import Image, Template
from .geometry import Point, Rect, Size, Vector2D, Vector3D, is_point, is_rect
from .geometry import RectTuple, PointTuple
__all__ = [
'Image',
'Template',
'Point',
'Rect',
'Size',
'Vector2D',
'Vector3D',
'is_point',
'is_rect',
'RectTuple',
'PointTuple'
]

View File

@ -0,0 +1,290 @@
from typing import Generic, TypeVar, TypeGuard, overload
T = TypeVar('T')
class Vector2D(Generic[T]):
"""2D 坐标类"""
def __init__(self, x: T, y: T, *, name: str | None = None):
self.x = x
self.y = y
self.name: str | None = name
"""坐标的名称。"""
def __getitem__(self, item: int):
if item == 0:
return self.x
elif item == 1:
return self.y
else:
raise IndexError
def __repr__(self) -> str:
return f'Point<"{self.name}" at ({self.x}, {self.y})>'
def __str__(self) -> str:
return f'({self.x}, {self.y})'
class Vector3D(Generic[T]):
"""三元组类。"""
def __init__(self, x: T, y: T, z: T, *, name: str | None = None):
self.x = x
self.y = y
self.z = z
self.name: str | None = name
"""坐标的名称。"""
def __getitem__(self, item: int):
if item == 0:
return self.x
elif item == 1:
return self.y
elif item == 2:
return self.z
else:
raise IndexError
@property
def xyz(self) -> tuple[T, T, T]:
"""
三元组 (x, y, z)OpenCV 格式的坐标
"""
return self.x, self.y, self.z
@property
def xy(self) -> tuple[T, T]:
"""
二元组 (x, y)OpenCV 格式的坐标
"""
return self.x, self.y
class Vector4D(Generic[T]):
"""四元组类。"""
def __init__(self, x: T, y: T, z: T, w: T, *, name: str | None = None):
self.x = x
self.y = y
self.z = z
self.w = w
self.name: str | None = name
"""坐标的名称。"""
def __getitem__(self, item: int):
if item == 0:
return self.x
elif item == 1:
return self.y
elif item == 2:
return self.z
elif item == 3:
return self.w
else:
raise IndexError
Size = Vector2D[int]
"""尺寸。相当于 Vector2D[int]"""
RectTuple = tuple[int, int, int, int]
"""矩形。(x, y, w, h)"""
PointTuple = tuple[int, int]
"""点。(x, y)"""
class Point(Vector2D[int]):
"""点。"""
@property
def xy(self) -> PointTuple:
"""
二元组 (x, y)OpenCV 格式的坐标
"""
return self.x, self.y
def offset(self, dx: int, dy: int) -> 'Point':
"""
偏移坐标
:param dx: 偏移量
:param dy: 偏移量
:return: 偏移后的坐标
"""
return Point(self.x + dx, self.y + dy, name=self.name)
def __add__(self, other: 'Point | PointTuple') -> 'Point':
"""
相加
:param other: 另一个 Point 对象或二元组 (x: int, y: int)
:return: 相加后的点
"""
if isinstance(other, Point):
return Point(self.x + other.x, self.y + other.y, name=self.name)
else:
return Point(self.x + other[0], self.y + other[1], name=self.name)
def __sub__(self, other: 'Point | PointTuple') -> 'Point':
"""
相减
:param other: 另一个 Point 对象或二元组 (x: int, y: int)
:return: 相减后的点
"""
if isinstance(other, Point):
return Point(self.x - other.x, self.y - other.y, name=self.name)
else:
return Point(self.x - other[0], self.y - other[1], name=self.name)
class Rect:
"""
矩形类
"""
def __init__(
self,
x: int | None = None,
y: int | None = None,
w: int | None = None,
h: int | None = None,
*,
xywh: RectTuple | None = None,
name: str | None = None,
):
"""
从给定的坐标信息创建矩形
参数 `x`, `y`, `w`, `h` `xywh` 必须至少指定一组
:param x: 矩形左上角的 X 坐标
:param y: 矩形左上角的 Y 坐标
:param w: 矩形的宽度
:param h: 矩形的高度
:param xywh: 四元组 (x, y, w, h)
:param name: 矩形的名称
:raises ValueError: 提供的坐标参数不完整时抛出
"""
if xywh is not None:
x, y, w, h = xywh
elif (
x is not None and
y is not None and
w is not None and
h is not None
):
pass
else:
raise ValueError('Either xywh or x, y, w, h must be provided.')
self.x1 = x
"""矩形左上角的 X 坐标。"""
self.y1 = y
"""矩形左上角的 Y 坐标。"""
self.w = w
"""矩形的宽度。"""
self.h = h
"""矩形的高度。"""
self.name: str | None = name
"""矩形的名称。"""
@classmethod
def from_xyxy(cls, x1: int, y1: int, x2: int, y2: int) -> 'Rect':
"""
(x1, y1, x2, y2) 创建矩形
:return: 创建结果
"""
return cls(x1, y1, x2 - x1, y2 - y1)
@property
def x2(self) -> int:
"""矩形右下角的 X 坐标。"""
return self.x1 + self.w
@x2.setter
def x2(self, value: int):
self.w = value - self.x1
@property
def y2(self) -> int:
"""矩形右下角的 Y 坐标。"""
return self.y1 + self.h
@y2.setter
def y2(self, value: int):
self.h = value - self.y1
@property
def xywh(self) -> RectTuple:
"""
四元组 (x1, y1, w, h)OpenCV 格式的坐标
"""
return self.x1, self.y1, self.w, self.h
@property
def xyxy(self) -> RectTuple:
"""
四元组 (x1, y1, x2, y2)
"""
return self.x1, self.y1, self.x2, self.y2
@property
def top_left(self) -> Point:
"""
矩形的左上角点
"""
if self.name:
name = "Left-top of rect "+ self.name
else:
name = None
return Point(self.x1, self.y1, name=name)
@property
def bottom_right(self) -> Point:
"""
矩形的右下角点
"""
if self.name:
name = "Right-bottom of rect "+ self.name
else:
name = None
return Point(self.x2, self.y2, name=name)
@property
def left_bottom(self) -> Point:
"""
矩形的左下角点
"""
if self.name:
name = "Left-bottom of rect "+ self.name
else:
name = None
return Point(self.x1, self.y2, name=name)
@property
def right_top(self) -> Point:
"""
矩形的右上角点
"""
if self.name:
name = "Right-top of rect "+ self.name
else:
name = None
return Point(self.x2, self.y1, name=name)
@property
def center(self) -> Point:
"""
矩形的中心点
"""
if self.name:
name = "Center of rect "+ self.name
else:
name = None
return Point(self.x1 + self.w // 2, self.y1 + self.h // 2, name=name)
def __repr__(self) -> str:
return f'Rect<"{self.name}" at (x={self.x1}, y={self.y1}, w={self.w}, h={self.h})>'
def __str__(self) -> str:
return f'(x={self.x1}, y={self.y1}, w={self.w}, h={self.h})'
def is_point(obj: object) -> TypeGuard[Point]:
return isinstance(obj, Point)
def is_rect(obj: object) -> TypeGuard[Rect]:
return isinstance(obj, Rect)

View File

@ -0,0 +1,63 @@
import logging
from cv2.typing import MatLike
from .geometry import Size
from kotonebot.util import cv2_imread
logger = logging.getLogger(__name__)
class Image:
"""
图像类
"""
def __init__(
self,
pixels: MatLike | None = None,
file_path: str | None = None,
lazy_load: bool = False,
name: str | None = None,
description: str | None = None
):
"""
从内存数据或图像文件创建图像类
:param pixels: 图像数据格式必须为 BGR
:param file_path: 图像文件路径
:param lazy_load: 是否延迟加载图像数据
若为 False立即载入否则仅当访问图像数据时才载入仅当从文件创建图像类时生效
:param name: 图像名称
:param description: 图像描述
"""
self.name: str | None = name
"""图像名称。"""
self.description: str | None = description
"""图像描述。"""
self.file_path: str | None = file_path
"""图像的文件路径。"""
self.__pixels: MatLike | None = None
# 立即加载
if not lazy_load and self.file_path:
_ = self.pixels
# 传入像素数据而不是文件
if pixels is not None:
self.__pixels = pixels
@property
def pixels(self) -> MatLike:
"""图像的像素数据。"""
if self.__pixels is None:
if not self.file_path:
raise ValueError('Either pixels or file_path must be provided.')
logger.debug('Loading image "%s" from %s...', self.name or '(unnamed)', self.file_path)
self.__pixels = cv2_imread(self.file_path)
return self.__pixels
@property
def size(self) -> Size:
return Size(self.pixels.shape[1], self.pixels.shape[0])
class Template(Image):
"""
模板图像类
"""