1433 lines
50 KiB
Python
1433 lines
50 KiB
Python
|
from ..Config import *
|
|||
|
|
|||
|
try:
|
|||
|
import cv2 as base
|
|||
|
import cv2.data as BaseData
|
|||
|
except ImportError:
|
|||
|
InternalImportingThrow("OpenCV", ["opencv-python"])
|
|||
|
raise
|
|||
|
try:
|
|||
|
from PIL import ImageFile, Image
|
|||
|
except ImportError:
|
|||
|
InternalImportingThrow("OpenCV", ["Pillow"])
|
|||
|
raise
|
|||
|
|
|||
|
from ..File import tool_file
|
|||
|
|
|||
|
# OpenCV Image format is BGR
|
|||
|
# PIL Image format is RBG
|
|||
|
|
|||
|
VideoWriter = base.VideoWriter
|
|||
|
def mp4_with_MPEG4_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"mp4v")
|
|||
|
def avi_with_Xvid_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"XVID")
|
|||
|
def avi_with_DivX_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"DIVX")
|
|||
|
def avi_with_MJPG_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"MJPG")
|
|||
|
def mp4_or_avi_with_H264_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"X264")
|
|||
|
def avi_with_H265_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"H264")
|
|||
|
def wmv_with_WMV1_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"WMV1")
|
|||
|
def wmv_with_WMV2_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"WMV2")
|
|||
|
def oggTheora_with_THEO_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"THEO")
|
|||
|
def flv_with_FLV1_fourcc() -> int:
|
|||
|
return VideoWriter.fourcc(*"FLV1")
|
|||
|
class VideoWriterInstance(VideoWriter):
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
file_name: Union[tool_file, str],
|
|||
|
fourcc: int,
|
|||
|
fps: float,
|
|||
|
frame_size: tuple[int, int],
|
|||
|
is_color: bool = True
|
|||
|
):
|
|||
|
super().__init__(str(file_name), fourcc, fps, frame_size, is_color)
|
|||
|
def __del__(self):
|
|||
|
self.release()
|
|||
|
|
|||
|
AffineFeature_feature2D = base.AffineFeature.create
|
|||
|
SIFT_Feature2D = base.SIFT.create
|
|||
|
ORB_Feature2D = base.ORB.create
|
|||
|
BRISK_Feature2D = base.BRISK.create
|
|||
|
AKAZE_Feature2D = base.AKAZE.create
|
|||
|
KAZE_Feature2D = base.KAZE.create
|
|||
|
MSER_Feature2D = base.MSER.create
|
|||
|
FastFeatureDetector_Feature2D = base.FastFeatureDetector.create
|
|||
|
AgastFeatureDetector_Feature2D = base.AgastFeatureDetector.create
|
|||
|
GFTTDetector_Feature2D = base.GFTTDetector.create
|
|||
|
SimpleBlobDetector_Feature2D = base.SimpleBlobDetector.create
|
|||
|
class Feature2DInstance[featrue:base.Feature2D]:
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
feature2D:Union[featrue, Callable[[],featrue]]
|
|||
|
):
|
|||
|
if isinstance(feature2D, base.Feature2D):
|
|||
|
self.mainFeature2D = feature2D
|
|||
|
else:
|
|||
|
self.mainFeature2D = feature2D()
|
|||
|
def detect[Mat_or_Mats:Union[
|
|||
|
MatLike,
|
|||
|
base.UMat,
|
|||
|
Sequence[MatLike],
|
|||
|
Sequence[base.UMat]
|
|||
|
]](
|
|||
|
self,
|
|||
|
image: Mat_or_Mats,
|
|||
|
mask: Optional[Mat_or_Mats] = None
|
|||
|
) -> Sequence[base.KeyPoint]:
|
|||
|
return self.mainFeature2D.detect(image, mask)
|
|||
|
def compute[Mat_or_Mats:Union[
|
|||
|
MatLike,
|
|||
|
base.UMat,
|
|||
|
Sequence[MatLike],
|
|||
|
Sequence[base.UMat]
|
|||
|
]](
|
|||
|
self,
|
|||
|
image: Mat_or_Mats,
|
|||
|
keypoints: Optional[Sequence[base.KeyPoint]] = None,
|
|||
|
descriptors: Optional[Mat_or_Mats] = None
|
|||
|
) -> Tuple[Sequence[base.KeyPoint], MatLike]:
|
|||
|
return self.mainFeature2D.compute(image, keypoints, descriptors)
|
|||
|
def detectAndCompute[_Mat:Union[
|
|||
|
MatLike,
|
|||
|
base.UMat,
|
|||
|
]](
|
|||
|
self,
|
|||
|
image: _Mat,
|
|||
|
mask: Optional[_Mat] = None,
|
|||
|
descriptors: Optional[_Mat] = None,
|
|||
|
useProvidedKeypoints:bool = False
|
|||
|
) -> Tuple[Sequence[base.KeyPoint], MatLike]:
|
|||
|
return self.mainFeature2D.detectAndCompute(image, mask, descriptors, useProvidedKeypoints)
|
|||
|
|
|||
|
def wait_key(delay:int):
|
|||
|
return base.waitKey(delay)
|
|||
|
def until_esc():
|
|||
|
return wait_key(0)
|
|||
|
|
|||
|
def is_current_key(key:str, *, wait_delay:int = 1):
|
|||
|
return wait_key(wait_delay) & 0xFF == ord(key[0])
|
|||
|
|
|||
|
class light_cv_view:
|
|||
|
def __init__(self, filename_or_index:Union[str, tool_file, int]):
|
|||
|
self.__capture: base.VideoCapture = None
|
|||
|
self.stats: bool = True
|
|||
|
self.retarget(filename_or_index)
|
|||
|
def __del__(self):
|
|||
|
self.release()
|
|||
|
|
|||
|
@override
|
|||
|
def ToString(self):
|
|||
|
return f"View<{self.width}x{self.height}>"
|
|||
|
|
|||
|
def __bool__(self):
|
|||
|
return self.stats
|
|||
|
|
|||
|
def is_open(self):
|
|||
|
return self.__capture.isOpened()
|
|||
|
|
|||
|
def release(self):
|
|||
|
if self.__capture is not None:
|
|||
|
self.__capture.release()
|
|||
|
def retarget(self, filename_or_index:Union[str, tool_file, int]):
|
|||
|
self.release()
|
|||
|
if isinstance(filename_or_index, int):
|
|||
|
self.__capture = base.VideoCapture(filename_or_index)
|
|||
|
else:
|
|||
|
self.__capture = base.VideoCapture(str(filename_or_index))
|
|||
|
return self
|
|||
|
|
|||
|
def next_frame(self) -> MatLike:
|
|||
|
self.stats, frame =self.__capture.read()
|
|||
|
if self.stats:
|
|||
|
return frame
|
|||
|
else:
|
|||
|
return None
|
|||
|
|
|||
|
def get_captrue_info(self, id:int):
|
|||
|
return self.__capture.get(id)
|
|||
|
def get_prop_pos_msec(self):
|
|||
|
return self.get_captrue_info(0)
|
|||
|
def get_prop_pos_frames(self):
|
|||
|
return self.get_captrue_info(1)
|
|||
|
def get_prop_avi_ratio(self):
|
|||
|
return self.get_captrue_info(2)
|
|||
|
def get_prop_frame_width(self):
|
|||
|
return self.get_captrue_info(3)
|
|||
|
def get_prop_frame_height(self):
|
|||
|
return self.get_captrue_info(4)
|
|||
|
def get_prop_fps(self):
|
|||
|
return self.get_captrue_info(5)
|
|||
|
def get_prop_fourcc(self):
|
|||
|
return self.get_captrue_info(6)
|
|||
|
def get_prop_frame_count(self):
|
|||
|
return self.get_captrue_info(7)
|
|||
|
def get_prop_format(self):
|
|||
|
return self.get_captrue_info(8)
|
|||
|
def get_prop_mode(self):
|
|||
|
return self.get_captrue_info(9)
|
|||
|
def get_prop_brightness(self):
|
|||
|
return self.get_captrue_info(10)
|
|||
|
def get_prop_contrast(self):
|
|||
|
return self.get_captrue_info(11)
|
|||
|
def get_prop_saturation(self):
|
|||
|
return self.get_captrue_info(12)
|
|||
|
def get_prop_hue(self):
|
|||
|
return self.get_captrue_info(13)
|
|||
|
def get_prop_gain(self):
|
|||
|
return self.get_captrue_info(14)
|
|||
|
def get_prop_exposure(self):
|
|||
|
return self.get_captrue_info(15)
|
|||
|
def get_prop_convert_rgb(self):
|
|||
|
return self.get_captrue_info(16)
|
|||
|
|
|||
|
def setup_capture(self, id:int, value):
|
|||
|
self.__capture.set(id, value)
|
|||
|
return self
|
|||
|
def set_prop_pos_msec(self, value:int):
|
|||
|
return self.setup_capture(0, value)
|
|||
|
def set_prop_pos_frames(self, value:int):
|
|||
|
return self.setup_capture(1, value)
|
|||
|
def set_prop_avi_ratio(self, value:float):
|
|||
|
return self.setup_capture(2, value)
|
|||
|
def set_prop_frame_width(self, value:int):
|
|||
|
return self.setup_capture(3, value)
|
|||
|
def set_prop_frame_height(self, value:int):
|
|||
|
return self.setup_capture(4, value)
|
|||
|
def set_prop_fps(self, value:int):
|
|||
|
return self.setup_capture(5, value)
|
|||
|
def set_prop_fourcc(self, value):
|
|||
|
return self.setup_capture(6, value)
|
|||
|
def set_prop_frame_count(self, value):
|
|||
|
return self.setup_capture(7, value)
|
|||
|
def set_prop_format(self, value):
|
|||
|
return self.setup_capture(8, value)
|
|||
|
def set_prop_mode(self, value):
|
|||
|
return self.setup_capture(9, value)
|
|||
|
def set_prop_brightness(self, value):
|
|||
|
return self.setup_capture(10, value)
|
|||
|
def set_prop_contrast(self, value):
|
|||
|
return self.setup_capture(11, value)
|
|||
|
def set_prop_saturation(self, value):
|
|||
|
return self.setup_capture(12, value)
|
|||
|
def set_prop_hue(self, value):
|
|||
|
return self.setup_capture(13, value)
|
|||
|
def set_prop_gain(self, value):
|
|||
|
return self.setup_capture(14, value)
|
|||
|
def set_prop_exposure(self, value):
|
|||
|
return self.setup_capture(15, value)
|
|||
|
def set_prop_convert_rgb(self, value:int):
|
|||
|
return self.setup_capture(16, value)
|
|||
|
def set_prop_rectification(self, value:int):
|
|||
|
return self.setup_capture(17, value)
|
|||
|
|
|||
|
@property
|
|||
|
def width(self) -> float:
|
|||
|
return self.get_prop_frame_width()
|
|||
|
@width.setter
|
|||
|
def width(self, value:float) -> float:
|
|||
|
self.set_prop_frame_width(value)
|
|||
|
return value
|
|||
|
@property
|
|||
|
def height(self):
|
|||
|
return self.get_prop_frame_height()
|
|||
|
@height.setter
|
|||
|
def height(self, value:float) -> float:
|
|||
|
self.set_prop_frame_height(value)
|
|||
|
return value
|
|||
|
|
|||
|
@property
|
|||
|
def frame_size(self) -> Tuple[float, float]:
|
|||
|
return self.get_prop_frame_width(), self.get_prop_frame_height()
|
|||
|
@property
|
|||
|
def shape(self):
|
|||
|
return self.frame_size
|
|||
|
@frame_size.setter
|
|||
|
def frame_size(self, value:Tuple[float, float]) -> Tuple[float, float]:
|
|||
|
self.set_prop_frame_width(value[0])
|
|||
|
self.set_prop_frame_height(value[1])
|
|||
|
return value
|
|||
|
|
|||
|
class light_cv_camera(light_cv_view, any_class):
|
|||
|
def __init__(self, index:int = 0):
|
|||
|
self.writer: VideoWriter = None
|
|||
|
super().__init__(int(index))
|
|||
|
|
|||
|
@override
|
|||
|
def release(self):
|
|||
|
super().release()
|
|||
|
if self.writer is not None:
|
|||
|
self.writer.release()
|
|||
|
|
|||
|
def current_frame(self):
|
|||
|
return self.next_frame()
|
|||
|
|
|||
|
def recording(
|
|||
|
self,
|
|||
|
stop_pr: Callable[[], bool],
|
|||
|
writer: Union[VideoWriter, Callable[[MatLike], Any]],
|
|||
|
):
|
|||
|
writer_stats = False
|
|||
|
if isinstance(writer, VideoWriter):
|
|||
|
self.writer = writer
|
|||
|
writer_stats = True
|
|||
|
while self.is_open():
|
|||
|
if stop_pr():
|
|||
|
break
|
|||
|
frame = self.current_frame()
|
|||
|
base.imshow("__recording__", frame)
|
|||
|
if writer_stats:
|
|||
|
writer.write(frame)
|
|||
|
else:
|
|||
|
writer(frame)
|
|||
|
base.destroyWindow("__recording__")
|
|||
|
return self
|
|||
|
|
|||
|
@override
|
|||
|
def ToString(self):
|
|||
|
return f"Camera<{self.width}x{self.height}>"
|
|||
|
|
|||
|
def get_zero_mask(shape, *args, **kwargs) -> MatLike:
|
|||
|
return np.zeros(shape, *args, **kwargs)
|
|||
|
def get_one_mask(shape, value, *args, **kwargs) -> MatLike:
|
|||
|
return np.ones(shape, value, *args, **kwargs)
|
|||
|
|
|||
|
class ImageObject(left_np_ndarray_reference):
|
|||
|
@property
|
|||
|
def __image(self) -> MatLike:
|
|||
|
return self.ref_value
|
|||
|
@__image.setter
|
|||
|
def __image(self, value:MatLike) -> MatLike:
|
|||
|
self.ref_value = value
|
|||
|
return value
|
|||
|
|
|||
|
@overload
|
|||
|
def __init__(self, imagePath:str, flags:Optional[int] = None):...
|
|||
|
@overload
|
|||
|
def __init__(self, image:tool_file, flags:Optional[int] = None):...
|
|||
|
@overload
|
|||
|
def __init__(self, camera:light_cv_camera):...
|
|||
|
@overload
|
|||
|
def __init__(self, image:MatLike, flags:Optional[int] = None):...
|
|||
|
@overload
|
|||
|
def __init__(self, image:Self):...
|
|||
|
@overload
|
|||
|
def __init__(self, image:Image.Image):...
|
|||
|
@overload
|
|||
|
def __init__(self, image:ImageFile.ImageFile):...
|
|||
|
@overload
|
|||
|
def __init__(self, image:np.ndarray):...
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
image: Optional[Union[
|
|||
|
str,
|
|||
|
Self,
|
|||
|
light_cv_camera,
|
|||
|
tool_file,
|
|||
|
MatLike,
|
|||
|
np.ndarray,
|
|||
|
ImageFile.ImageFile,
|
|||
|
Image.Image
|
|||
|
]],
|
|||
|
flags: Optional[Any] = None
|
|||
|
) -> None:
|
|||
|
super().__init__()
|
|||
|
self.__camera: light_cv_camera = None
|
|||
|
self.current: MatLike = None
|
|||
|
self.__gray: MatLike = None
|
|||
|
if isinstance(image, light_cv_camera):
|
|||
|
self.lock_from_camera(image)
|
|||
|
else:
|
|||
|
self.load_image(image, flags)
|
|||
|
|
|||
|
def internal_check_when_image_is_none_throw_error(self):
|
|||
|
if self.image is None:
|
|||
|
raise ValueError("Image is None")
|
|||
|
return self
|
|||
|
|
|||
|
@override
|
|||
|
def SymbolName(self):
|
|||
|
return "Image"
|
|||
|
@override
|
|||
|
def ToString(self):
|
|||
|
current = self.image
|
|||
|
if current is None:
|
|||
|
return "null"
|
|||
|
return f"Image<{current.shape[1]}x{current.shape[0]}:\n"+str(
|
|||
|
self.image)+"\n>"
|
|||
|
|
|||
|
@property
|
|||
|
def camera(self) -> light_cv_camera:
|
|||
|
if self.__camera is None or self.__camera.is_open() is False:
|
|||
|
return None
|
|||
|
else:
|
|||
|
return self.__camera
|
|||
|
@property
|
|||
|
def image(self) -> MatLike:
|
|||
|
if self.current is not None:
|
|||
|
return self.current
|
|||
|
elif self.camera is None:
|
|||
|
return self.__image
|
|||
|
else:
|
|||
|
return self.__camera.current_frame()
|
|||
|
|
|||
|
@image.setter
|
|||
|
def image(self, image: Optional[Union[
|
|||
|
str,
|
|||
|
Self,
|
|||
|
tool_file,
|
|||
|
MatLike,
|
|||
|
np.ndarray,
|
|||
|
ImageFile.ImageFile,
|
|||
|
Image.Image
|
|||
|
]]):
|
|||
|
self.load_image(image)
|
|||
|
|
|||
|
def load_from_nparray(
|
|||
|
self,
|
|||
|
array_: np.ndarray,
|
|||
|
code: Optional[int] = None,
|
|||
|
*args, **kwargs
|
|||
|
):
|
|||
|
self.__gray = None
|
|||
|
if code is None:
|
|||
|
self.__image = array_
|
|||
|
else:
|
|||
|
self.__image = base.cvtColor(array_, code, *args, **kwargs).astype(np.uint8)
|
|||
|
return self
|
|||
|
def load_from_PIL_image(
|
|||
|
self,
|
|||
|
image: Image.Image,
|
|||
|
code: Optional[int] = None,
|
|||
|
*args, **kwargs
|
|||
|
):
|
|||
|
if code is None:
|
|||
|
self.load_from_nparray(np.array(image), *args, **kwargs)
|
|||
|
else:
|
|||
|
self.load_from_nparray(base.cvtColor(np.array(image), code, *args, **kwargs).astype(np.uint8))
|
|||
|
return self
|
|||
|
def load_from_PIL_ImageFile(
|
|||
|
self,
|
|||
|
image: ImageFile.ImageFile,
|
|||
|
rect: Optional[Tuple[float, float, float, float]] = None
|
|||
|
):
|
|||
|
return self.load_from_PIL_image(image.crop(rect))
|
|||
|
def load_from_cv2_image(self, image: MatLike):
|
|||
|
self.__gray = None
|
|||
|
self.__image = image
|
|||
|
return self
|
|||
|
def lock_from_camera(self, camera: light_cv_camera):
|
|||
|
self.__camera = camera
|
|||
|
return self
|
|||
|
|
|||
|
@property
|
|||
|
def height(self) -> int:
|
|||
|
return self.shape[0]
|
|||
|
@property
|
|||
|
def width(self) -> int:
|
|||
|
return self.shape[1]
|
|||
|
@property
|
|||
|
def channel_depth(self) -> int:
|
|||
|
return self.shape[2]
|
|||
|
|
|||
|
@property
|
|||
|
def pixel_count(self) -> int:
|
|||
|
return self.image.size
|
|||
|
@property
|
|||
|
def dtype(self):
|
|||
|
return self.image.dtype
|
|||
|
|
|||
|
def is_enable(self):
|
|||
|
return self.image is not None
|
|||
|
def is_invalid(self):
|
|||
|
return self.is_enable() is False
|
|||
|
def __bool__(self):
|
|||
|
return self.is_enable()
|
|||
|
def __MatLike__(self):
|
|||
|
return self.image
|
|||
|
|
|||
|
@overload
|
|||
|
def load_image(self, image:str, flags:int = -1):...
|
|||
|
@overload
|
|||
|
def load_image(self, image:tool_file, flags:int = -1):...
|
|||
|
@overload
|
|||
|
def load_image(self, image:light_cv_camera):...
|
|||
|
@overload
|
|||
|
def load_image(self, image:MatLike):...
|
|||
|
@overload
|
|||
|
def load_image(self, image:Self):...
|
|||
|
@overload
|
|||
|
def load_image(self, image:Image.Image):...
|
|||
|
@overload
|
|||
|
def load_image(self, image:ImageFile.ImageFile):...
|
|||
|
def load_image(
|
|||
|
self,
|
|||
|
image: Optional[Union[
|
|||
|
str,
|
|||
|
tool_file,
|
|||
|
Self,
|
|||
|
MatLike,
|
|||
|
np.ndarray,
|
|||
|
ImageFile.ImageFile,
|
|||
|
Image.Image
|
|||
|
]],
|
|||
|
flags: int = -1
|
|||
|
):
|
|||
|
"""加载图片"""
|
|||
|
if image is None:
|
|||
|
self.__image = None
|
|||
|
return self
|
|||
|
elif isinstance(image, ImageObject):
|
|||
|
self.__image = image.image
|
|||
|
elif isinstance(image, MatLike):
|
|||
|
self.__image = image
|
|||
|
elif isinstance(image, np.ndarray):
|
|||
|
self.load_from_nparray(image, flags)
|
|||
|
elif isinstance(image, ImageFile.ImageFile):
|
|||
|
self.load_from_PIL_ImageFile(image, flags)
|
|||
|
elif isinstance(image, Image.Image):
|
|||
|
self.load_from_PIL_image(image, flags)
|
|||
|
elif isinstance(image, loss_file):
|
|||
|
self.__image = None
|
|||
|
else:
|
|||
|
self.__image = base.imread(str(image), flags)
|
|||
|
return self
|
|||
|
def save_image(
|
|||
|
self,
|
|||
|
save_path: Union[str, tool_file],
|
|||
|
is_path_must_exist: bool = False
|
|||
|
) -> Self:
|
|||
|
"""保存图片"""
|
|||
|
if isinstance(save_path, loss_file):
|
|||
|
return self
|
|||
|
if is_path_must_exist:
|
|||
|
Wrapper2File(save_path).try_create_parent_path()
|
|||
|
if self.is_enable():
|
|||
|
base.imwrite(str(save_path), self.image)
|
|||
|
return self
|
|||
|
|
|||
|
def show_image(
|
|||
|
self,
|
|||
|
window_name: str = "Image",
|
|||
|
delay: Union[int,str] = 0,
|
|||
|
image_show_func: Callable[[Self], None] = None,
|
|||
|
*args, **kwargs
|
|||
|
):
|
|||
|
"""显示图片"""
|
|||
|
if self.is_invalid():
|
|||
|
return self
|
|||
|
if self.camera is not None:
|
|||
|
while (wait_key(1) & 0xFF != ord(str(delay)[0])) and self.camera is not None:
|
|||
|
# dont delete this line, self.image is camera flame now, see<self.current = None>
|
|||
|
self.current = self.image
|
|||
|
if image_show_func is not None:
|
|||
|
image_show_func(self)
|
|||
|
if self.current is not None:
|
|||
|
base.imshow(window_name, self.current)
|
|||
|
# dont delete this line, see property<image>
|
|||
|
self.current = None
|
|||
|
else:
|
|||
|
base.imshow(window_name, self.image)
|
|||
|
base.waitKey(delay = int(delay), *args, **kwargs)
|
|||
|
if base.getWindowProperty(window_name, base.WND_PROP_VISIBLE) > 0:
|
|||
|
base.destroyWindow(window_name)
|
|||
|
return self
|
|||
|
|
|||
|
# 绝对值转换
|
|||
|
def convert_scale_abs(self):
|
|||
|
"""绝对值转换"""
|
|||
|
return ImageObject(base.convertScaleAbs(self.image))
|
|||
|
|
|||
|
# 图像边缘检测
|
|||
|
def edge_detect_with_sobel(
|
|||
|
self,
|
|||
|
*,
|
|||
|
ksize: Optional[int] = None,
|
|||
|
scale: Optional[float] = None,
|
|||
|
delta: Optional[float] = None,
|
|||
|
borderType: Optional[int] = None,
|
|||
|
**kwargs
|
|||
|
):
|
|||
|
if ksize is not None:
|
|||
|
kwargs["ksize"] = ksize
|
|||
|
if scale is not None:
|
|||
|
kwargs["scale"] = scale
|
|||
|
if delta is not None:
|
|||
|
kwargs["delta"] = delta
|
|||
|
if borderType is not None:
|
|||
|
kwargs["borderType"] = borderType
|
|||
|
gray = self.get_grayscale()
|
|||
|
dx = base.Sobel(gray, base.CV_16S, 1, 0, **kwargs)
|
|||
|
dy = base.Sobel(gray, base.CV_16S, 0, 1, **kwargs)
|
|||
|
return ImageObject(dx).convert_scale_abs().merge_with_blending(
|
|||
|
ImageObject(dy).convert_scale_abs(), (0.5, 0.5))
|
|||
|
def edge_detect_with_roberts(self):
|
|||
|
kernelx = np.array([[-1,0],[0,1]], dtype=int)
|
|||
|
kernely = np.array([[0,-1],[1,0]], dtype=int)
|
|||
|
gray = self.get_grayscale()
|
|||
|
dx = base.filter2D(gray, base.CV_16S, kernelx)
|
|||
|
dy = base.filter2D(gray, base.CV_16S, kernely)
|
|||
|
return ImageObject(dx).convert_scale_abs().merge_with_blending(
|
|||
|
ImageObject(dy).convert_scale_abs(), (0.5, 0.5))
|
|||
|
def edge_detect_with_laplacian(
|
|||
|
self,
|
|||
|
kernalSize: int = 3
|
|||
|
):
|
|||
|
gray = self.get_grayscale()
|
|||
|
return ImageObject(base.convertScaleAbs(
|
|||
|
base.Laplacian(gray, base.CV_16S, ksize=kernalSize)
|
|||
|
))
|
|||
|
def edge_detect_with_canny(
|
|||
|
self,
|
|||
|
threshold1: float,
|
|||
|
threshold2: float,
|
|||
|
**kwargs
|
|||
|
):
|
|||
|
return ImageObject(base.Canny(
|
|||
|
self.get_grayscale(), threshold1, threshold2, **kwargs
|
|||
|
))
|
|||
|
|
|||
|
# 分离通道
|
|||
|
def split(self):
|
|||
|
"""分离通道"""
|
|||
|
return base.split(self.image)
|
|||
|
def split_to_image_object(self):
|
|||
|
"""分离通道"""
|
|||
|
return [ImageObject(channel) for channel in self.split()]
|
|||
|
@property
|
|||
|
def channels(self):
|
|||
|
return self.split()
|
|||
|
@property
|
|||
|
def blue_channel(self):
|
|||
|
return self.channels[0]
|
|||
|
@property
|
|||
|
def green_channel(self):
|
|||
|
return self.channels[1]
|
|||
|
@property
|
|||
|
def red_channel(self):
|
|||
|
return self.channels[2]
|
|||
|
@property
|
|||
|
def alpha_channel(self):
|
|||
|
return self.channels[3]
|
|||
|
def get_blue_image(self):
|
|||
|
return ImageObject(self.blue_channel)
|
|||
|
def get_green_image(self):
|
|||
|
return ImageObject(self.green_channel)
|
|||
|
def get_red_image(self):
|
|||
|
return ImageObject(self.red_channel)
|
|||
|
def get_alpha_image(self):
|
|||
|
return ImageObject(self.alpha_channel)
|
|||
|
|
|||
|
# 混合通道
|
|||
|
def merge_channels_from_list(self, channels:List[MatLike]):
|
|||
|
"""合并通道"""
|
|||
|
self.image = base.merge(channels)
|
|||
|
return self
|
|||
|
def merge_channels(self, blue:MatLike, green:MatLike, red:MatLike):
|
|||
|
"""合并通道"""
|
|||
|
return self.merge_channels_from_list([blue, green, red])
|
|||
|
def merge_channel_list(self, bgr:List[MatLike]):
|
|||
|
"""合并通道"""
|
|||
|
return self.merge_channels_from_list(bgr)
|
|||
|
|
|||
|
# Transform
|
|||
|
def get_resize_image(self, width:int, height:int):
|
|||
|
if self.is_enable():
|
|||
|
return ImageObject(base.resize(self.image, (width, height)))
|
|||
|
return None
|
|||
|
def get_rotate_image(self, angle:float):
|
|||
|
if self.is_invalid():
|
|||
|
return None
|
|||
|
(h, w) = self.image.shape[:2]
|
|||
|
center = (w // 2, h // 2)
|
|||
|
M = base.getRotationMatrix2D(center, angle, 1.0)
|
|||
|
return ImageObject(base.warpAffine(self.image, M, (w, h)))
|
|||
|
def resize_image(self, width:int, height:int):
|
|||
|
"""调整图片大小"""
|
|||
|
new_image = self.get_resize_image(width, height)
|
|||
|
if new_image is not None:
|
|||
|
self.image = new_image.image
|
|||
|
return self
|
|||
|
def rotate_image(self, angle:float):
|
|||
|
"""旋转图片"""
|
|||
|
new_image = self.get_rotate_image(angle)
|
|||
|
if new_image is not None:
|
|||
|
self.image = new_image.image
|
|||
|
return self
|
|||
|
|
|||
|
# 图片翻折
|
|||
|
def flip(self, flip_code:int):
|
|||
|
"""翻转图片"""
|
|||
|
if self.is_enable():
|
|||
|
self.image = base.flip(self.image, flip_code)
|
|||
|
return self
|
|||
|
def horizon_flip(self):
|
|||
|
"""水平翻转图片"""
|
|||
|
return self.flip(1)
|
|||
|
def vertical_flip(self):
|
|||
|
"""垂直翻转图片"""
|
|||
|
return self.flip(0)
|
|||
|
def both_flip(self):
|
|||
|
"""双向翻转图片"""
|
|||
|
return self.flip(-1)
|
|||
|
|
|||
|
# 合并序列
|
|||
|
def horizontal_stack(self, *images:Self):
|
|||
|
"""水平合并图片"""
|
|||
|
return ImageObject(base.hconcat(self.image, *[image.image for image in images]))
|
|||
|
def vertical_stack(self, *images:Self):
|
|||
|
"""垂直合并图片"""
|
|||
|
return ImageObject(base.vconcat(self.image, *[image.image for image in images]))
|
|||
|
|
|||
|
# 色彩空间猜测
|
|||
|
def guess_color_space(self) -> str:
|
|||
|
"""猜测色彩空间"""
|
|||
|
if self.is_invalid():
|
|||
|
return None
|
|||
|
image = self.image
|
|||
|
# 计算每个通道的像素值分布
|
|||
|
hist_b = base.calcHist([image], [0], None, [256], [0, 256])
|
|||
|
hist_g = base.calcHist([image], [1], None, [256], [0, 256])
|
|||
|
hist_r = base.calcHist([image], [2], None, [256], [0, 256])
|
|||
|
|
|||
|
# 计算每个通道的像素值总和
|
|||
|
sum_b = np.sum(hist_b)
|
|||
|
sum_g = np.sum(hist_g)
|
|||
|
sum_r = np.sum(hist_r)
|
|||
|
|
|||
|
# 根据像素值总和判断色彩空间
|
|||
|
if sum_b > sum_g and sum_b > sum_r:
|
|||
|
#print("The image might be in BGR color space.")
|
|||
|
return "BGR"
|
|||
|
elif sum_g > sum_b and sum_g > sum_r:
|
|||
|
#print("The image might be in GRAY color space.")
|
|||
|
return "GRAY"
|
|||
|
else:
|
|||
|
#print("The image might be in RGB color space.")
|
|||
|
return "RGB"
|
|||
|
|
|||
|
# 颜色转化
|
|||
|
def get_convert(self, color_convert:int):
|
|||
|
"""颜色转化"""
|
|||
|
if self.is_invalid():
|
|||
|
return None
|
|||
|
return ImageObject(base.cvtColor(self.image, color_convert))
|
|||
|
def convert_to(self, color_convert:int):
|
|||
|
"""颜色转化"""
|
|||
|
if self.is_invalid():
|
|||
|
return None
|
|||
|
self.image = self.get_convert(color_convert)
|
|||
|
return self
|
|||
|
|
|||
|
def is_grayscale(self):
|
|||
|
return self.dimension == 2
|
|||
|
def get_grayscale(self, curColor=base.COLOR_BGR2GRAY) -> MatLike:
|
|||
|
if self.is_invalid():
|
|||
|
return None
|
|||
|
if self.__gray is None and self.camera is None:
|
|||
|
self.__gray = base.cvtColor(self.image, curColor)
|
|||
|
return self.__gray
|
|||
|
def convert_to_grayscale(self):
|
|||
|
"""将图片转换为灰度图"""
|
|||
|
self.__image = self.get_grayscale()
|
|||
|
return self
|
|||
|
|
|||
|
def get_convert_flag(
|
|||
|
self,
|
|||
|
targetColorTypeName:Literal[
|
|||
|
"BGR", "RGB", "GRAY", "YCrCb"
|
|||
|
]
|
|||
|
) -> Optional[int]:
|
|||
|
"""获取颜色转化标志"""
|
|||
|
flag = self.guess_color_space()
|
|||
|
if flag is None:
|
|||
|
return None
|
|||
|
|
|||
|
if targetColorTypeName == "BGR":
|
|||
|
if flag == "RGB":
|
|||
|
return base.COLOR_RGB2BGR
|
|||
|
elif flag == "GRAY":
|
|||
|
return base.COLOR_GRAY2BGR
|
|||
|
elif flag == "YCrCb":
|
|||
|
return base.COLOR_YCrCb2BGR
|
|||
|
elif targetColorTypeName == "RGB":
|
|||
|
if flag == "BGR":
|
|||
|
return base.COLOR_BGR2RGB
|
|||
|
elif flag == "GRAY":
|
|||
|
return base.COLOR_GRAY2RGB
|
|||
|
elif flag == "YCrCb":
|
|||
|
return base.COLOR_YCrCb2RGB
|
|||
|
elif targetColorTypeName == "GRAY":
|
|||
|
if flag == "RGB":
|
|||
|
return base.COLOR_RGB2GRAY
|
|||
|
elif flag == "RGB":
|
|||
|
return base.COLOR_BGR2GRAY
|
|||
|
return None
|
|||
|
|
|||
|
# 原址裁切
|
|||
|
def sub_image(self, x:int, y:int ,width:int ,height:int):
|
|||
|
"""裁剪图片"""
|
|||
|
if self.is_invalid():
|
|||
|
return self
|
|||
|
self.image = self.image[y:y+height, x:x+width]
|
|||
|
return self
|
|||
|
|
|||
|
# 直方图
|
|||
|
def equalizeHist(self, is_cover = False) -> MatLike:
|
|||
|
"""直方图均衡化"""
|
|||
|
if self.is_invalid():
|
|||
|
return self
|
|||
|
result:MatLike = base.equalizeHist(self.image)
|
|||
|
if is_cover:
|
|||
|
self.image = result
|
|||
|
return result
|
|||
|
def calcHist(
|
|||
|
self,
|
|||
|
channel: Union[List[int], int],
|
|||
|
mask: Optional[MatLike] = None,
|
|||
|
hist_size: Sequence[int] = [256],
|
|||
|
ranges: Sequence[float] = [0, 256]
|
|||
|
) -> MatLike:
|
|||
|
"""计算直方图"""
|
|||
|
if self.is_invalid():
|
|||
|
return None
|
|||
|
return base.calcHist(
|
|||
|
[self.image],
|
|||
|
channel if isinstance(channel, list) else [channel],
|
|||
|
mask,
|
|||
|
hist_size,
|
|||
|
ranges)
|
|||
|
|
|||
|
# 子集操作
|
|||
|
def sub_image_with_rect(self, rect:Tuple[float, float, float, float]):
|
|||
|
"""裁剪图片"""
|
|||
|
if self.is_invalid():
|
|||
|
return self
|
|||
|
self.image = self.image[rect[1]:rect[1]+rect[3], rect[0]:rect[0]+rect[2]]
|
|||
|
return self
|
|||
|
def sub_image_with_box(self, box:Tuple[float, float, float, float]):
|
|||
|
"""裁剪图片"""
|
|||
|
if self.is_invalid():
|
|||
|
return self
|
|||
|
self.image = self.image[box[1]:box[3], box[0]:box[2]]
|
|||
|
return self
|
|||
|
def sub_cover_with_rect(self, image:Union[Self, MatLike], rect:Tuple[float, float, float, float]):
|
|||
|
"""覆盖图片"""
|
|||
|
if self.is_invalid():
|
|||
|
raise ValueError("Real Image is none")
|
|||
|
if isinstance(image, MatLike):
|
|||
|
image = ImageObject(image)
|
|||
|
self.image[rect[1]:rect[1]+rect[3], rect[0]:rect[0]+rect[2]] = image.image
|
|||
|
return self
|
|||
|
def sub_cover_with_box(self, image:Union[Self, MatLike], box:Tuple[float, float, float, float]):
|
|||
|
"""覆盖图片"""
|
|||
|
if self.is_invalid():
|
|||
|
raise ValueError("Real Image is none")
|
|||
|
if isinstance(image, MatLike):
|
|||
|
image = ImageObject(image)
|
|||
|
self.image[box[1]:box[3], box[0]:box[2]] = image.image
|
|||
|
return self
|
|||
|
|
|||
|
def operator_cv(self, func:Callable[[MatLike], Any], *args, **kwargs):
|
|||
|
func(self.image, *args, **kwargs)
|
|||
|
return self
|
|||
|
|
|||
|
# 序列合并
|
|||
|
@override
|
|||
|
def _inject_stack_uniform_item(self):
|
|||
|
return np.uint8(self.image)
|
|||
|
|
|||
|
# 从另一图像合并
|
|||
|
def merge_with_blending(self, other:Self, weights:Tuple[float, float]):
|
|||
|
return ImageObject(base.addWeighted(self.image, weights[0], other.image, weights[1], 0))
|
|||
|
# 从另一图像合并(遮罩)
|
|||
|
def merge_with_mask(self, other:Self, mask:Self):
|
|||
|
return ImageObject(base.bitwise_and(self.image, other.image, mask.image))
|
|||
|
|
|||
|
# 滤波
|
|||
|
def filter(self, ddepth:int, kernel:MatLike, *args, **kwargs):
|
|||
|
return base.filter2D(self.image, ddepth, kernel, *args, **kwargs)
|
|||
|
def filter_blur(self, kernalSize:Tuple[float, float]):
|
|||
|
return base.blur(self.image, kernalSize)
|
|||
|
def filter_gaussian(self, kernalSize:Tuple[float, float], sigmaX:float, sigmaY:float):
|
|||
|
return base.GaussianBlur(self.image, kernalSize, sigmaX, sigmaY)
|
|||
|
def filter_median(self, kernalSize:int):
|
|||
|
return base.medianBlur(self.image, kernalSize)
|
|||
|
def filter_bilateral(self, d:float, sigmaColor:float, sigmaSpace:float):
|
|||
|
return base.bilateralFilter(self.image, d, sigmaColor, sigmaSpace)
|
|||
|
def filter_sobel(self, dx:int, dy:int, kernalSize:int):
|
|||
|
return base.Sobel(self.image, -1, dx, dy, ksize=kernalSize)
|
|||
|
def filter_canny(self, threshold1:float, threshold2:float):
|
|||
|
return base.Canny(self.image, threshold1, threshold2)
|
|||
|
def filter_laplacian(self, kernalSize:int):
|
|||
|
return base.Laplacian(self.image, -1, ksize=kernalSize)
|
|||
|
def filter_scharr(self, dx:int, dy:int):
|
|||
|
return base.Scharr(self.image, -1, dx, dy)
|
|||
|
def filter_box_blur(self, kernalSize:Tuple[float, float]):
|
|||
|
return base.boxFilter(self.image, -1, ksize=kernalSize, normalize=0)
|
|||
|
|
|||
|
# 阈值
|
|||
|
def threshold(
|
|||
|
self,
|
|||
|
threshold:float,
|
|||
|
type:int
|
|||
|
):
|
|||
|
return base.threshold(self.image, threshold, 255, type)
|
|||
|
def adaptiveThreshold(
|
|||
|
self,
|
|||
|
adaptiveMethod: int = base.ADAPTIVE_THRESH_MEAN_C,
|
|||
|
thresholdType: int = base.THRESH_BINARY,
|
|||
|
blockSize: int = 11,
|
|||
|
C: float = 2,
|
|||
|
):
|
|||
|
return base.adaptiveThreshold(self.image, 255, adaptiveMethod, thresholdType, blockSize, C)
|
|||
|
# 获取二值化
|
|||
|
def Separate2EnableScene(self,*, is_front=True, is_back=False):
|
|||
|
'''
|
|||
|
return mask -> front, back
|
|||
|
'''
|
|||
|
if is_back == is_front:
|
|||
|
is_back = not is_front
|
|||
|
gray = self.get_grayscale()
|
|||
|
if is_front:
|
|||
|
return base.threshold(gray, 255.0/2.0, 255, base.THRESH_BINARY)
|
|||
|
else:
|
|||
|
return base.threshold(gray, 255.0/2.0, 255, base.THRESH_BINARY_INV)
|
|||
|
def Separate2EnableScenes_with_Otsu(self,*, is_front=True, is_back=False):
|
|||
|
'''
|
|||
|
return mask -> front, back
|
|||
|
'''
|
|||
|
if is_back == is_front:
|
|||
|
is_back = not is_front
|
|||
|
gray = self.get_grayscale()
|
|||
|
if is_front:
|
|||
|
return base.threshold(gray, 0, 255, base.THRESH_BINARY | base.THRESH_OTSU)
|
|||
|
else:
|
|||
|
return base.threshold(gray, 0, 255, base.THRESH_BINARY_INV | base.THRESH_OTSU)
|
|||
|
# 获取二值化遮罩
|
|||
|
def SeparateFrontBackScenes(self):
|
|||
|
'''
|
|||
|
return mask -> front, back
|
|||
|
'''
|
|||
|
gray = self.get_grayscale()
|
|||
|
_, front = base.threshold(gray, 255.0/2.0, 255, base.THRESH_BINARY)
|
|||
|
_, back = base.threshold(gray, 255.0/2.0, 255, base.THRESH_BINARY_INV)
|
|||
|
return np.where(gray>=front), np.where(gray>=back)
|
|||
|
def SeparateFrontBackScenes_with_Otsu(self):
|
|||
|
'''
|
|||
|
return mask -> front, back
|
|||
|
'''
|
|||
|
gray = self.get_grayscale()
|
|||
|
_, front = base.threshold(gray, 0, 255, base.THRESH_BINARY | base.THRESH_OTSU)
|
|||
|
_, back = base.threshold(gray, 0, 255, base.THRESH_BINARY_INV | base.THRESH_OTSU)
|
|||
|
return np.where(gray>=front), np.where(gray>=back)
|
|||
|
# 获取核
|
|||
|
def get_kernel(self, shape:int, kernalSize:Tuple[float, float]):
|
|||
|
return base.getStructuringElement(shape, kernalSize)
|
|||
|
def get_rect_kernal(self, kernalSize:Tuple[float, float]):
|
|||
|
return self.get_kernel(base.MORPH_RECT, kernalSize)
|
|||
|
def get_cross_kernal(self, kernalSize:Tuple[float, float]):
|
|||
|
return self.get_kernel(base.MORPH_CROSS, kernalSize)
|
|||
|
def get_ellipse_kernal(self, kernalSize:Tuple[float, float]):
|
|||
|
return self.get_kernel(base.MORPH_ELLIPSE, kernalSize)
|
|||
|
# 膨胀
|
|||
|
def dilate(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.dilate(self.image, kernel, *args, **kwargs)
|
|||
|
# 腐蚀
|
|||
|
def erode(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.erode(self.image, kernel, *args, **kwargs)
|
|||
|
# 开运算
|
|||
|
def open_operator(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.morphologyEx(self.image, base.MORPH_OPEN, kernel, *args, **kwargs)
|
|||
|
# 闭运算
|
|||
|
def close_operator(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.morphologyEx(self.image, base.MORPH_CLOSE, kernel, *args, **kwargs)
|
|||
|
# 梯度运算
|
|||
|
def gradient_operator(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.morphologyEx(self.image, base.MORPH_GRADIENT, kernel, *args, **kwargs)
|
|||
|
# 顶帽运算
|
|||
|
def tophat_operator(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.morphologyEx(self.image, base.MORPH_TOPHAT, kernel, *args, **kwargs)
|
|||
|
# 黑帽运算
|
|||
|
def blackhat_operator(self, kernel:Optional[MatLike]=None, *args, **kwargs):
|
|||
|
if kernel is None:
|
|||
|
kernel = self.get_rect_kernal((3, 3))
|
|||
|
return base.morphologyEx(self.image, base.MORPH_BLACKHAT, kernel, *args, **kwargs)
|
|||
|
|
|||
|
# 绘制轮廓
|
|||
|
def drawContours(
|
|||
|
self,
|
|||
|
contours: Sequence[MatLike],
|
|||
|
contourIdx: int = -1,
|
|||
|
color: Union[MatLike, Tuple[int]] = (0, 0, 0),
|
|||
|
thickness: int = 1,
|
|||
|
lineType: int = base.LINE_8,
|
|||
|
hierarchy: Optional[MatLike] = None,
|
|||
|
maxLevel: int = base.FILLED,
|
|||
|
offset: Optional[Point] = None,
|
|||
|
is_draw_on_self:bool = False
|
|||
|
) -> MatLike:
|
|||
|
image = self.image if is_draw_on_self else self.image.copy()
|
|||
|
return base.drawContours(image, contours, contourIdx, color, thickness, lineType, hierarchy, maxLevel, offset)
|
|||
|
# 修改自身的绘制
|
|||
|
def draw_rect(
|
|||
|
self,
|
|||
|
rect: Rect,
|
|||
|
color: Union[MatLike, Tuple[int]] = (0, 0, 0),
|
|||
|
thickness: int = 1,
|
|||
|
lineType: int = base.LINE_8,
|
|||
|
) -> MatLike:
|
|||
|
base.rectangle(self.image, rect, color, thickness, lineType)
|
|||
|
return self
|
|||
|
# 获取轮廓
|
|||
|
def get_contours(
|
|||
|
self,
|
|||
|
*,
|
|||
|
mode: int = base.RETR_LIST,
|
|||
|
method: int = base.CHAIN_APPROX_SIMPLE,
|
|||
|
is_front: bool = True,
|
|||
|
contours: Optional[Sequence[MatLike]] = None,
|
|||
|
hierarchy: Optional[MatLike] = None,
|
|||
|
offset: Optional[Point] = None
|
|||
|
) -> Tuple[Sequence[MatLike], MatLike]:
|
|||
|
_, bin = self.Separate2EnableScene(is_front=is_front)
|
|||
|
if offset is not None:
|
|||
|
return base.findContours(bin, mode, method, contours, hierarchy, offset)
|
|||
|
else:
|
|||
|
return base.findContours(bin, mode, method, contours, hierarchy)
|
|||
|
def get_contours_mask(
|
|||
|
self,
|
|||
|
width: int,
|
|||
|
*,
|
|||
|
mode: int = base.RETR_LIST,
|
|||
|
method: int = base.CHAIN_APPROX_SIMPLE,
|
|||
|
is_front: bool = True,
|
|||
|
contours: Optional[Sequence[MatLike]] = None,
|
|||
|
hierarchy: Optional[MatLike] = None,
|
|||
|
offset: Optional[Point] = None,
|
|||
|
) -> Tuple[Sequence[MatLike], MatLike]:
|
|||
|
find_contours, _ = self.get_contours(
|
|||
|
mode=mode,
|
|||
|
method=method,
|
|||
|
is_front=is_front,
|
|||
|
contours=contours,
|
|||
|
hierarchy=hierarchy,
|
|||
|
offset=offset
|
|||
|
)
|
|||
|
return base.drawContours(get_zero_mask(self.shape, dtype=np.uint8), find_contours, -1, (255, 255, 255), width)
|
|||
|
def get_contours_fill_inside_mask(
|
|||
|
self,
|
|||
|
*,
|
|||
|
mode: int = base.RETR_LIST,
|
|||
|
method: int = base.CHAIN_APPROX_SIMPLE,
|
|||
|
is_front: bool = True,
|
|||
|
contours: Optional[Sequence[MatLike]] = None,
|
|||
|
hierarchy: Optional[MatLike] = None,
|
|||
|
offset: Optional[Point] = None
|
|||
|
) -> Tuple[Sequence[MatLike], MatLike]:
|
|||
|
return self.get_contours_mask(
|
|||
|
mode=mode,
|
|||
|
method=method,
|
|||
|
is_front=is_front,
|
|||
|
contours=contours,
|
|||
|
hierarchy=hierarchy,
|
|||
|
offset=offset,
|
|||
|
width=-1
|
|||
|
)
|
|||
|
# 获取轮廓方框
|
|||
|
def get_xy_rect_from_contours(
|
|||
|
self,
|
|||
|
*,
|
|||
|
mode: int = base.RETR_LIST,
|
|||
|
method: int = base.CHAIN_APPROX_SIMPLE,
|
|||
|
is_front: bool = True,
|
|||
|
contours: Optional[Sequence[MatLike]] = None,
|
|||
|
hierarchy: Optional[MatLike] = None,
|
|||
|
offset: Optional[Point] = None
|
|||
|
) -> Sequence[Rect]:
|
|||
|
return [base.boundingRect(contour) for contour in self.get_contours(
|
|||
|
mode=mode,
|
|||
|
method=method,
|
|||
|
is_front=is_front,
|
|||
|
contours=contours,
|
|||
|
hierarchy=hierarchy,
|
|||
|
offset=offset
|
|||
|
)]
|
|||
|
def get_minarea_rect_from_contours(
|
|||
|
self,
|
|||
|
*,
|
|||
|
mode: int = base.RETR_LIST,
|
|||
|
method: int = base.CHAIN_APPROX_SIMPLE,
|
|||
|
is_front: bool = True,
|
|||
|
contours: Optional[Sequence[MatLike]] = None,
|
|||
|
hierarchy: Optional[MatLike] = None,
|
|||
|
offset: Optional[Point] = None
|
|||
|
) -> Sequence[RotatedRect]:
|
|||
|
return [base.minAreaRect(contour) for contour in self.get_contours(
|
|||
|
mode=mode,
|
|||
|
method=method,
|
|||
|
is_front=is_front,
|
|||
|
contours=contours,
|
|||
|
hierarchy=hierarchy,
|
|||
|
offset=offset)]
|
|||
|
|
|||
|
# 图像匹配
|
|||
|
def match_on_scene(
|
|||
|
self,
|
|||
|
scene_image: Self,
|
|||
|
# Feature2D config
|
|||
|
featrue_type: Optional[Union[
|
|||
|
base.Feature2D,
|
|||
|
ClosuresCallable[base.Feature2D],
|
|||
|
Feature2DInstance
|
|||
|
]] = SIFT_Feature2D,
|
|||
|
optout_feature_kp_and_des_ref:
|
|||
|
Optional[left_value_reference[
|
|||
|
Tuple[Sequence[base.KeyPoint], MatLike]
|
|||
|
]] = None,
|
|||
|
# Match Config
|
|||
|
match_min_points: int = 4,
|
|||
|
# Draw rect Config
|
|||
|
rect_color: Tuple[int, int, int] = (0, 255, 0),
|
|||
|
rect_thickness: int = 2,
|
|||
|
# Draw match Config
|
|||
|
out_drawMatches_ref:Optional[left_value_reference[MatLike]
|
|||
|
] = None,
|
|||
|
drawMatches_range: Optional[Tuple[int, int]] = None
|
|||
|
) -> MatLike:
|
|||
|
'''
|
|||
|
本图像作为目标特征
|
|||
|
|
|||
|
Args
|
|||
|
---
|
|||
|
Target Image
|
|||
|
scene_image:
|
|||
|
识别的场景, 此图像将作为目标匹配的场景
|
|||
|
|
|||
|
Feature2D Config
|
|||
|
type:
|
|||
|
特征检测器类型/生成器/实例, 默认为SIFT
|
|||
|
ref:
|
|||
|
左值引用容器, ref_value为空时将用于存储特征点与描述符,
|
|||
|
不为空则提取ref_value作为本次的特征点与描述符
|
|||
|
|
|||
|
Match Config
|
|||
|
min_points:
|
|||
|
匹配的最小点数, 小于此值则无法找到目标物体
|
|||
|
|
|||
|
Draw rect Config
|
|||
|
color:
|
|||
|
矩形框颜色, 默认为绿色
|
|||
|
thickness:
|
|||
|
矩形框厚度, 默认为2
|
|||
|
|
|||
|
Draw match Config
|
|||
|
ref:
|
|||
|
左值引用容器, 将用于存储合成的对比图像
|
|||
|
range:
|
|||
|
匹配点的绘制范围(靠前的匹配度高), 默认为全部绘制
|
|||
|
|
|||
|
Return
|
|||
|
---
|
|||
|
MatLike: 绘制了方框的矩形
|
|||
|
'''
|
|||
|
# 读取目标图和场景图
|
|||
|
target_img = self.get_grayscale()
|
|||
|
scene_img = scene_image.get_grayscale()
|
|||
|
|
|||
|
# 初始化SIFT检测器
|
|||
|
feature2D:Feature2DInstance = featrue_type if (
|
|||
|
isinstance(featrue_type, Feature2DInstance)
|
|||
|
) else Feature2DInstance(featrue_type)
|
|||
|
|
|||
|
# 检测关键点和描述符
|
|||
|
kp1:Sequence[base.KeyPoint] = None
|
|||
|
des1:MatLike = None
|
|||
|
if optout_feature_kp_and_des_ref is None:
|
|||
|
kp1, des1 = feature2D.detectAndCompute(target_img, None)
|
|||
|
else:
|
|||
|
if optout_feature_kp_and_des_ref.ref_value is None:
|
|||
|
kp1, des1 = feature2D.detectAndCompute(target_img, None)
|
|||
|
optout_feature_kp_and_des_ref.ref_value = (kp1, des1)
|
|||
|
else:
|
|||
|
kp1, des1 = optout_feature_kp_and_des_ref.ref_value
|
|||
|
kp2, des2 = feature2D.detectAndCompute(scene_img, None)
|
|||
|
|
|||
|
# 初始化BFMatcher
|
|||
|
bf = base.BFMatcher(base.NORM_L2, crossCheck=True)
|
|||
|
|
|||
|
# 匹配描述符
|
|||
|
matches = bf.match(des1, des2)
|
|||
|
matches = sorted(matches, key=lambda x: x.distance)
|
|||
|
|
|||
|
# 如果匹配点数少于min_match_points个,无法找到目标物体
|
|||
|
if len(matches) < match_min_points:
|
|||
|
return None
|
|||
|
|
|||
|
# 提取匹配点的位置
|
|||
|
src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
|
|||
|
dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
|
|||
|
|
|||
|
# 使用RANSAC算法找到单应性矩阵
|
|||
|
M, _ = base.findHomography(src_pts, dst_pts, base.RANSAC, 5.0)
|
|||
|
|
|||
|
# 获取目标物体的边界框
|
|||
|
h, w = target_img.shape
|
|||
|
pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)
|
|||
|
dst = base.perspectiveTransform(pts, M)
|
|||
|
|
|||
|
# 在scene中绘制边界框
|
|||
|
result = scene_image.image.copy()
|
|||
|
base.polylines(
|
|||
|
result,
|
|||
|
[np.int32(dst)],
|
|||
|
True,
|
|||
|
rect_color,
|
|||
|
rect_thickness,
|
|||
|
base.LINE_AA
|
|||
|
)
|
|||
|
|
|||
|
# 合成的绘制结果
|
|||
|
if out_drawMatches_ref is not None:
|
|||
|
if drawMatches_range is None:
|
|||
|
out_drawMatches_ref.ref_value = base.drawMatches(
|
|||
|
self.image,
|
|||
|
kp1,
|
|||
|
scene_image.image,
|
|||
|
kp2,
|
|||
|
matches,
|
|||
|
None, flags=base.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
|
|||
|
)
|
|||
|
else:
|
|||
|
out_drawMatches_ref.ref_value = base.drawMatches(
|
|||
|
self.image,
|
|||
|
kp1,
|
|||
|
scene_image.image,
|
|||
|
kp2,
|
|||
|
matches[drawMatches_range[0]:drawMatches_range[1]],
|
|||
|
None, flags=base.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
|
|||
|
)
|
|||
|
|
|||
|
# 返回绘制了方框的结果
|
|||
|
return result
|
|||
|
|
|||
|
# 图像卷积
|
|||
|
def conv(self, kernel:MatLike, *args, **kwargs):
|
|||
|
return base.filter2D(self.image, -1, kernel, *args, **kwargs)
|
|||
|
def conv_with_shape(self, shape:Tuple[int, int], *args, **kwargs):
|
|||
|
return base.filter2D(self.image, -1, shape, *args, **kwargs)
|
|||
|
|
|||
|
# np加速
|
|||
|
|
|||
|
def get_new_noise(
|
|||
|
raw_image: Optional[MatLike],
|
|||
|
height: int,
|
|||
|
weight: int,
|
|||
|
*,
|
|||
|
mean: float = 0,
|
|||
|
sigma: float = 25,
|
|||
|
dtype = np.uint8
|
|||
|
) -> MatLike:
|
|||
|
noise = raw_image
|
|||
|
if noise is None:
|
|||
|
noise = np.zeros((height, weight), dtype=dtype)
|
|||
|
base.randn(noise, mean, sigma)
|
|||
|
return base.cvtColor(noise, base.COLOR_GRAY2BGR)
|
|||
|
class NoiseImageObject(ImageObject):
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
height: int,
|
|||
|
weight: int,
|
|||
|
*,
|
|||
|
mean: float = 0,
|
|||
|
sigma: float = 25,
|
|||
|
dtype = np.uint8
|
|||
|
):
|
|||
|
super().__init__(get_new_noise(
|
|||
|
None, height, weight, mean=mean, sigma=sigma, dtype=dtype
|
|||
|
))
|
|||
|
|
|||
|
@override
|
|||
|
def SymbolName(self):
|
|||
|
return "Noise"
|
|||
|
|
|||
|
def Unwrapper(image:Optional[Union[
|
|||
|
str,
|
|||
|
ImageObject,
|
|||
|
tool_file,
|
|||
|
MatLike,
|
|||
|
np.ndarray,
|
|||
|
ImageFile.ImageFile,
|
|||
|
Image.Image
|
|||
|
]]) -> MatLike:
|
|||
|
return image.image if isinstance(image, ImageObject) else ImageObject(image).image
|
|||
|
|
|||
|
def Wrapper(image:Optional[Union[
|
|||
|
str,
|
|||
|
ImageObject,
|
|||
|
tool_file,
|
|||
|
MatLike,
|
|||
|
np.ndarray,
|
|||
|
ImageFile.ImageFile,
|
|||
|
Image.Image
|
|||
|
]]) -> ImageObject:
|
|||
|
return ImageObject(image)
|
|||
|
|
|||
|
class light_cv_window:
|
|||
|
def __init__(self, name:str):
|
|||
|
self.__my_window_name = name
|
|||
|
base.namedWindow(self.__my_window_name)
|
|||
|
def __del__(self):
|
|||
|
self.destroy()
|
|||
|
|
|||
|
def show_image(self, image:Union[ImageObject, MatLike]):
|
|||
|
if self.__my_window_name is None:
|
|||
|
self.__my_window_name = "window"
|
|||
|
if isinstance(image, ImageObject):
|
|||
|
image = image.image
|
|||
|
base.imshow(self.__my_window_name, image)
|
|||
|
return self
|
|||
|
def destroy(self):
|
|||
|
if self.__my_window_name is not None and base.getWindowProperty(self.__my_window_name, base.WND_PROP_VISIBLE) > 0:
|
|||
|
base.destroyWindow(self.__my_window_name)
|
|||
|
return self
|
|||
|
|
|||
|
@property
|
|||
|
def window_rect(self):
|
|||
|
return base.getWindowImageRect(self.__my_window_name)
|
|||
|
@window_rect.setter
|
|||
|
def window_rect(self, rect:Tuple[float, float, float, float]):
|
|||
|
self.set_window_rect(rect[0], rect[1], rect[2], rect[3])
|
|||
|
|
|||
|
def set_window_size(self, weight:int, height:int):
|
|||
|
base.resizeWindow(self.__my_window_name, weight, height)
|
|||
|
return self
|
|||
|
def get_window_size(self) -> Tuple[float, float]:
|
|||
|
rect = self.window_rect
|
|||
|
return rect[2], rect[3]
|
|||
|
|
|||
|
def get_window_property(self, prop_id:int):
|
|||
|
return base.getWindowProperty(self.__my_window_name, prop_id)
|
|||
|
def set_window_property(self, prop_id:int, prop_value:int):
|
|||
|
base.setWindowProperty(self.__my_window_name, prop_id, prop_value)
|
|||
|
return self
|
|||
|
def get_prop_frame_width(self):
|
|||
|
return self.window_rect[2]
|
|||
|
def get_prop_frame_height(self):
|
|||
|
return self.window_rect[3]
|
|||
|
def is_full_window(self):
|
|||
|
return base.getWindowProperty(self.__my_window_name, base.WINDOW_FULLSCREEN) > 0
|
|||
|
def set_full_window(self):
|
|||
|
base.setWindowProperty(self.__my_window_name, base.WINDOW_FULLSCREEN, 1)
|
|||
|
return self
|
|||
|
def set_normal_window(self):
|
|||
|
base.setWindowProperty(self.__my_window_name, base.WINDOW_FULLSCREEN, 0)
|
|||
|
return self
|
|||
|
def is_using_openGL(self):
|
|||
|
return base.getWindowProperty(self.__my_window_name, base.WINDOW_OPENGL) > 0
|
|||
|
def set_using_openGL(self):
|
|||
|
base.setWindowProperty(self.__my_window_name, base.WINDOW_OPENGL, 1)
|
|||
|
return self
|
|||
|
def set_not_using_openGL(self):
|
|||
|
base.setWindowProperty(self.__my_window_name, base.WINDOW_OPENGL, 0)
|
|||
|
return self
|
|||
|
def is_autosize(self):
|
|||
|
return base.getWindowProperty(self.__my_window_name, base.WINDOW_AUTOSIZE) > 0
|
|||
|
def set_autosize(self):
|
|||
|
base.setWindowProperty(self.__my_window_name, base.WINDOW_AUTOSIZE, 1)
|
|||
|
return self
|
|||
|
def set_not_autosize(self):
|
|||
|
base.setWindowProperty(self.__my_window_name, base.WINDOW_AUTOSIZE, 0)
|
|||
|
return self
|
|||
|
|
|||
|
def set_window_rect(self, x:int, y:int, weight:int, height:int):
|
|||
|
base.moveWindow(self.__my_window_name, x, y)
|
|||
|
return self.set_window_size(weight, height)
|
|||
|
|
|||
|
def set_window_pos(self, x:int, y:int):
|
|||
|
base.moveWindow(self.__my_window_name, x, y)
|
|||
|
return self
|
|||
|
|
|||
|
def wait_key(self, wait_time:int=0):
|
|||
|
return base.waitKey(wait_time)
|
|||
|
|
|||
|
def get_haarcascade_frontalface(name_or_default:Optional[str]=None):
|
|||
|
if name_or_default is None:
|
|||
|
name_or_default = "haarcascade_frontalface_default"
|
|||
|
return base.CascadeClassifier(BaseData.haarcascades+'haarcascade_frontalface_default.xml')
|
|||
|
|
|||
|
def detect_human_face(
|
|||
|
image: ImageObject,
|
|||
|
detecter: base.CascadeClassifier,
|
|||
|
scaleFactor: float = 1.1,
|
|||
|
minNeighbors: int = 4,
|
|||
|
*args, **kwargs):
|
|||
|
'''return is Rect[]'''
|
|||
|
return detecter.detectMultiScale(image.image, scaleFactor, minNeighbors, *args, **kwargs)
|
|||
|
|
|||
|
class internal_detect_faces_oop(Callable[[ImageObject], None]):
|
|||
|
def __init__(self):
|
|||
|
self.face_cascade = get_haarcascade_frontalface()
|
|||
|
def __call__(self, image:ImageObject):
|
|||
|
gray = image.convert_to_grayscale()
|
|||
|
faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
|
|||
|
for (x,y,w,h) in faces:
|
|||
|
image.operator_cv(base.rectangle,(x,y),(x+w,y+h),(255,0,0),2)
|
|||
|
|
|||
|
def easy_detect_faces(camera:light_cv_camera):
|
|||
|
ImageObject(camera).show_image("window", 'q', internal_detect_faces_oop())
|
|||
|
|
|||
|
# 示例使用
|
|||
|
if __name__ == "__main__":
|
|||
|
img_obj = ImageObject("path/to/your/image.jpg")
|
|||
|
img_obj.show_image()
|
|||
|
img_obj.resize_image(800, 600)
|
|||
|
img_obj.rotate_image(45)
|
|||
|
img_obj.convert_to_grayscale()
|
|||
|
img_obj.save_image("path/to/save/image.jpg")
|
|||
|
|
|||
|
# Override tool_file to tool_file_ex
|
|||
|
|
|||
|
class tool_file_cvex(tool_file):
|
|||
|
def __init__(self, file_path:str, *args, **kwargs):
|
|||
|
super().__init__(file_path, *args, **kwargs)
|
|||
|
|
|||
|
@override
|
|||
|
def load_as_image(self) -> ImageObject:
|
|||
|
self.data = ImageObject(self)
|
|||
|
return self.data
|
|||
|
@override
|
|||
|
def save_as_image(self, path = None):
|
|||
|
image:ImageObject = self.data
|
|||
|
image.save_image(path if path is not None else self.get_path())
|
|||
|
return self
|
|||
|
|
|||
|
def WrapperFile2CVEX(file:Union[tool_file_or_str, tool_file_cvex]):
|
|||
|
if isinstance(file, tool_file_cvex):
|
|||
|
return file
|
|||
|
elif isinstance(file, str):
|
|||
|
return tool_file_cvex(file)
|
|||
|
elif isinstance(file, tool_file):
|
|||
|
result = tool_file_cvex(str(file))
|
|||
|
result.data = file.data
|
|||
|
return result
|
|||
|
else:
|
|||
|
raise TypeError("file must be tool_file or str")
|