From 90298136590c053a3d33f38a0a327a73ff6d97bc Mon Sep 17 00:00:00 2001 From: Rodion Date: Sun, 7 Sep 2025 01:38:18 +0300 Subject: [PATCH 1/2] 1.3.1 --- .github/workflows/main.yml | 3 +- CHANGELOG.md | 20 ++++++ README.md | 2 +- cv3/_draw.py | 31 ++++++-- cv3/_transform.py | 4 +- cv3/_utils.py | 2 +- cv3/draw.py | 22 +++--- cv3/io.py | 4 +- cv3/opt.py | 29 +++++--- cv3/processing.py | 58 +++++++++++---- cv3/transform.py | 38 ++++++---- cv3/video.py | 46 ++++++++++-- setup.py | 2 +- tests/test_other.py | 113 +++++++++++++++++++++++++++++ tests/test_processing.py | 141 +++++++++++++++++++++++++++++++++++++ tests/test_video.py | 2 +- tests/test_window.py | 2 +- 17 files changed, 449 insertions(+), 70 deletions(-) create mode 100644 tests/test_processing.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3c3f756..cadf57b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,4 +37,5 @@ jobs: pytest tests/test_io.py && \ pytest tests/test_other.py && \ pytest tests/test_transform.py && \ - pytest tests/test_video.py \ No newline at end of file + pytest tests/test_video.py && \ + pytest tests/test_processing.py \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a59d241..b3eb960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,24 @@ # Changelog +## [1.3.1] - 2025-09-07 + +### Added +* default line_type value moved to cv3.opt +* updated threshold function with type parameter and relative values support +* new test_threshold_all_types in test_processing.py +* opt functions validation and tests in test_other +* examples in video.py for VideoCapture, VideoWriter, and Video functions + +### Changed +* got rid of f-strings for compatibility with early python versions +* got rid of TODOs +* small docs corrections + +### Deprecated + +### Removed + +### Fixed + ## [1.3.0] - 2025-08-31 ### Added diff --git a/README.md b/README.md index 86badbf..7f267d1 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ with cv3.Window('Result') as window: This is just a small example of what cv3 can do. Check out the [documentation](https://cv3.readthedocs.io/en/latest/) for a comprehensive overview of all the improvements and additions that cv3 provides over raw OpenCV. -You can also get acquainted with the features in [demo.ipynb](https://github.com/gorodion/pycv/blob/main/demo.ipynb) +You can also get acquainted with the features in [demo.ipynb](https://github.com/gorodion/cv3/blob/main/demo.ipynb) ## Run tests diff --git a/cv3/_draw.py b/cv3/_draw.py index 901f9ea..a9d59a4 100644 --- a/cv3/_draw.py +++ b/cv3/_draw.py @@ -62,17 +62,30 @@ 'italic': cv2.FONT_ITALIC } +_THRESHOLD_TYPE_DICT = { + 'binary': cv2.THRESH_BINARY, + 'binary_inv': cv2.THRESH_BINARY_INV, + 'trunc': cv2.THRESH_TRUNC, + 'tozero': cv2.THRESH_TOZERO, + 'tozero_inv': cv2.THRESH_TOZERO_INV +} + def _line_type_flag_match(flag): - assert flag in _LINE_TYPE_DICT, f'no such flag: "{flag}". Available: {", ".join(_LINE_TYPE_DICT.keys())}' + assert flag in _LINE_TYPE_DICT, 'no such flag: "{}". Available: {}'.format(flag, ", ".join(_LINE_TYPE_DICT.keys())) return _LINE_TYPE_DICT[flag] - def _font_flag_match(flag): - assert flag in _FONTS_DICT, f'no such flag: "{flag}". Available: {", ".join(_FONTS_DICT.keys())}' + assert flag in _FONTS_DICT, 'no such flag: "{}". Available: {}'.format(flag, ", ".join(_FONTS_DICT.keys())) return _FONTS_DICT[flag] +def _threshold_type_flag_match(flag): + assert flag in _THRESHOLD_TYPE_DICT, 'no such flag: "{}". Available: {}'.format(flag, ", ".join(_THRESHOLD_TYPE_DICT.keys())) + return _THRESHOLD_TYPE_DICT[flag] + + + def _handle_poly_pts(img, pts, rel=None): pts = np.array(pts).reshape(-1) pts = _relative_handle(img, *pts, rel=rel) @@ -82,18 +95,22 @@ def _handle_poly_pts(img, pts, rel=None): def _draw_decorator(func): @type_decorator - def wrapper(img, *args, color=None, line_type=cv2.LINE_8, copy=False, **kwargs): + def wrapper(img, *args, color=None, line_type=None, copy=False, **kwargs): if copy: img = img.copy() color = _process_color(color) - if isinstance(line_type, str): + # Handle line_type parameter + if line_type is None: + line_type = opt.LINE_TYPE + elif isinstance(line_type, str): line_type = _line_type_flag_match(line_type) kwargs['t'] = round(kwargs.get('t', opt.THICKNESS)) + kwargs['line_type'] = line_type - return func(img, *args, color=color, line_type=line_type, **kwargs) + return func(img, *args, color=color, **kwargs) return wrapper @@ -269,7 +286,7 @@ def _marker_flag_match(flag): 'triangle_up': cv2.MARKER_TRIANGLE_UP, 'triangle_down': cv2.MARKER_TRIANGLE_DOWN } - assert flag in marker_dict or flag in marker_dict.values(), f'no such flag: "{flag}". Available: {", ".join(marker_dict.keys())}' + assert flag in marker_dict or flag in marker_dict.values(), 'no such flag: "{}". Available: {}'.format(flag, ", ".join(marker_dict.keys())) return marker_dict.get(flag, flag) diff --git a/cv3/_transform.py b/cv3/_transform.py index 7102f44..79ba655 100644 --- a/cv3/_transform.py +++ b/cv3/_transform.py @@ -45,7 +45,7 @@ def _inter_flag_match(flag): Raises: AssertionError: If flag is not one of the valid options. """ - assert flag in _INTER_DICT, f'no such flag: "{flag}". Available: {", ".join(_INTER_DICT.keys())}' + assert flag in _INTER_DICT, 'no such flag: "{}". Available: {}'.format(flag, ", ".join(_INTER_DICT.keys())) return _INTER_DICT[flag] @@ -61,7 +61,7 @@ def _border_flag_match(flag): Raises: AssertionError: If flag is not one of the valid options. """ - assert flag in _BORDER_DICT, f'no such flag: "{flag}". Available: {", ".join(_BORDER_DICT.keys())}' + assert flag in _BORDER_DICT, 'no such flag: "{}". Available: {}'.format(flag, ", ".join(_BORDER_DICT.keys())) return _BORDER_DICT[flag] diff --git a/cv3/_utils.py b/cv3/_utils.py index 7974c8d..59d672c 100644 --- a/cv3/_utils.py +++ b/cv3/_utils.py @@ -74,7 +74,7 @@ def _process_color(color): if color is None: color = opt.COLOR if isinstance(color, str): - assert color in COLORS_RGB_DICT, f'No such color: {color}. Available colors: {list(COLORS_RGB_DICT.keys())}' + assert color in COLORS_RGB_DICT, 'No such color: {}. Available colors: {}'.format(color, list(COLORS_RGB_DICT.keys())) color = COLORS_RGB_DICT[color] if not opt.RGB: color = color[::-1] diff --git a/cv3/draw.py b/cv3/draw.py index ff75383..65fe9ab 100644 --- a/cv3/draw.py +++ b/cv3/draw.py @@ -81,7 +81,7 @@ def rectangle(img, x0, y0, x1, y1, mode='xyxy', rel=None, color=None, t=None, li rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the rectangle (default: opt.COLOR). t: Thickness of the rectangle lines (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). fill (bool, optional): Whether to fill the rectangle. If True, draws a filled rectangle regardless of thickness. If False, draws an outlined rectangle. If None, uses the thickness parameter to determine fill behavior. Defaults to None. @@ -138,7 +138,7 @@ def polylines(img, pts, is_closed=False, rel=None, color=None, t=None, line_type rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the polylines (default: opt.COLOR). t: Thickness of the lines (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). copy (bool): Whether to copy the image before drawing (default: False). Returns: @@ -217,7 +217,7 @@ def circle(img, x0, y0, r, rel=None, color=None, t=None, line_type=None, fill=No rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the circle (default: opt.COLOR). t: Thickness of the circle line. Use -1 or cv2.FILLED for filled circle (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). fill (bool, optional): Whether to fill the circle. If True, draws a filled circle regardless of thickness. If False, draws an outlined circle. If None, uses the thickness parameter to determine fill behavior. Defaults to None. @@ -305,7 +305,7 @@ def line(img, x0, y0, x1, y1, rel=None, color=None, t=None, line_type=None, copy rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the line (default: opt.COLOR). t: Thickness of the line (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). copy (bool): Whether to copy the image before drawing (default: False). Returns: @@ -340,7 +340,7 @@ def hline(img, y, rel=None, color=None, t=None, line_type=None, copy=False): rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the line (default: opt.COLOR). t: Thickness of the line (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). copy (bool): Whether to copy the image before drawing (default: False). Returns: @@ -375,7 +375,7 @@ def vline(img, x, rel=None, color=None, t=None, line_type=None, copy=False): rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the line (default: opt.COLOR). t: Thickness of the line (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). copy (bool): Whether to copy the image before drawing (default: False). Returns: @@ -422,7 +422,7 @@ def text(img, text_str, x=0.5, y=0.5, font=None, scale=None, flip=False, rel=Non rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the text (default: opt.COLOR). t: Thickness of the text strokes (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). copy (bool): Whether to copy the image before drawing (default: False). Returns: @@ -467,7 +467,7 @@ def rectangles(img: np.array, rects: List[List], color=None, t=None, line_type=N of parameters to pass to the rectangle function. color: Color of the rectangles (default: opt.COLOR). t: Thickness of the rectangle lines (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). fill (bool, optional): Whether to fill the rectangles. If True, draws filled rectangles regardless of thickness. If False, draws outlined rectangles. If None, uses the thickness parameter to determine fill behavior. Defaults to None. @@ -572,7 +572,7 @@ def arrow(img, x0, y0, x1, y1, rel=None, color=None, t=None, line_type=None, tip rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the arrow (default: opt.COLOR). t: Thickness of the arrow (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). tip_length (float, optional): The length of the arrow tip in relation to the arrow length. Defaults to 0.1. copy (bool): Whether to copy the image before drawing (default: False). @@ -623,7 +623,7 @@ def ellipse(img, x, y, axes_x, axes_y, angle=0, start_angle=0, end_angle=360, re rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the ellipse (default: opt.COLOR). t: Thickness of the ellipse line. Use -1 or cv2.FILLED for filled ellipse (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). fill (bool, optional): Whether to fill the ellipse. If True, draws a filled ellipse regardless of thickness. If False, draws an outlined ellipse. If None, uses the thickness parameter to determine fill behavior. Defaults to None. @@ -684,7 +684,7 @@ def marker(img, x, y, marker_type=None, marker_size=None, rel=None, color=None, rel (bool, optional): Whether to use relative coordinates. Defaults to None. color: Color of the marker (default: opt.COLOR). t: Thickness of the marker lines (default: opt.THICKNESS). - line_type: Type of line for drawing (default: cv2.LINE_8). + line_type: Type of line for drawing (default: opt.LINE_TYPE). copy (bool): Whether to copy the image before drawing (default: False). Returns: diff --git a/cv3/io.py b/cv3/io.py index c55e7cb..5e95d75 100644 --- a/cv3/io.py +++ b/cv3/io.py @@ -148,7 +148,7 @@ def imread(img_path, flag=cv2.IMREAD_COLOR): if not is_ascii(img_path): img = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), flag) if img is None: - raise OSError(f'File was not read: {img_path}') + raise OSError('File was not read: {}'.format(img_path)) if img.ndim == 2: return img if opt.RGB: @@ -297,7 +297,7 @@ def __init__(self, window_name=None, pos=None, flag=cv2.WINDOW_AUTOSIZE): >>> window = cv3.Window('My Window', pos=(100, 100)) """ if window_name is None: - window_name = f'window{Window.__window_count}' + window_name = 'window{}'.format(Window.__window_count) window_name = str(window_name) cv2.namedWindow(window_name, flag) diff --git a/cv3/opt.py b/cv3/opt.py index 81d1a2b..85350a3 100644 --- a/cv3/opt.py +++ b/cv3/opt.py @@ -11,6 +11,8 @@ THICKNESS (int): Default line thickness for drawing operations. COLOR: Default color for drawing operations. FONT: Default font for text drawing operations. + LINE_TYPE: Default line type for drawing operations. + THRESHOLD_TYPE: Default threshold type for threshold operations. SCALE (float): Default scale factor for drawing operations. PT_RADIUS (int): Default point radius for drawing operations. EXPERIMENTAL (bool): Flag to enable experimental features. @@ -27,6 +29,8 @@ SCALE = 1 PT_RADIUS = 1 EXPERIMENTAL = False +LINE_TYPE = cv2.LINE_8 +THRESHOLD_TYPE = cv2.THRESH_BINARY def set_rgb(): """Set the color format to RGB. @@ -60,12 +64,14 @@ def video(fps=None, fourcc=None): """ global FPS, FOURCC if fps is not None: - assert fps > 0, 'default fps must be more 0' + fps = int(fps) + assert fps > 0, 'default fps must be greater than 0' FPS = fps if fourcc is not None: - # TODO asserts flags - FOURCC = cv2.VideoWriter_fourcc(*fourcc) if isinstance(fourcc, str) else fourcc - + if isinstance(fourcc, str): + assert len(fourcc) == 4, 'if fourcc is str, len(fourcc) must be 4' + fourcc = cv2.VideoWriter_fourcc(*fourcc) + FOURCC = fourcc def draw(thickness=None, color=None, font=None, pt_radius=None, scale=None, line_type=None): """Set default drawing parameters. @@ -90,18 +96,25 @@ def draw(thickness=None, color=None, font=None, pt_radius=None, scale=None, line assert isinstance(color, (str, int, float, np.unsignedinteger, np.floating, np.ndarray, list, tuple)) COLOR = color if font is not None: - # TODO asserts flags + # Import font values from _draw.py + from ._draw import _FONTS_DICT + assert font in _FONTS_DICT.values(), 'invalid font type' FONT = font if pt_radius is not None: - # TODO asserts + assert isinstance(pt_radius, (int, np.unsignedinteger)), 'default pt_radius must be a non-negative integer' + assert pt_radius >= 0, 'default pt_radius must be non-negative' PT_RADIUS = pt_radius if scale is not None: - # TODO asserts + assert isinstance(scale, (int, float, np.floating)), 'default scale must be a positive number' + assert scale > 0, 'default scale must be positive' SCALE = scale if line_type is not None: - # TODO asserts flags + # Import line type values from _draw.py + from ._draw import _LINE_TYPE_DICT + assert line_type in _LINE_TYPE_DICT.values(), 'invalid line type' LINE_TYPE = line_type + def set_exp(exp=True): global EXPERIMENTAL EXPERIMENTAL = exp diff --git a/cv3/processing.py b/cv3/processing.py index 00dc589..9ba5eb9 100644 --- a/cv3/processing.py +++ b/cv3/processing.py @@ -8,23 +8,29 @@ """ import cv2 import numpy as np +from ._utils import type_decorator, _relative_check, _relative_handle +from . import opt +from ._draw import _threshold_type_flag_match -from ._utils import type_decorator __all__ = [ 'threshold' ] -# TODO flags + @type_decorator -def threshold(img: np.ndarray, thr=127, max=255): - """Apply binary threshold to a grayscale image. +def threshold(img: np.ndarray, thr=127, max=None, type=None, rel=None): + """Apply threshold to a grayscale image. Args: img (numpy.ndarray): Input grayscale image. - thr (int, optional): Threshold value. Defaults to 127. - max (int, optional): Maximum value to use with the THRESH_BINARY thresholding. - Defaults to 255. + thr (int or float, optional): Threshold value. Defaults to 127. + max (int or float, optional): Maximum value to use with the thresholding. + If None, defaults to 255. Defaults to None. + type (int or str, optional): Threshold type. Can be an OpenCV threshold flag or string. + Available string options: 'binary', 'binary_inv', 'trunc', 'tozero', 'tozero_inv'. + If None, defaults to opt.THRESHOLD_TYPE. Defaults to None. + rel (bool, optional): Whether to use relative threshold value. Defaults to None. Returns: numpy.ndarray: Thresholded image. @@ -33,9 +39,11 @@ def threshold(img: np.ndarray, thr=127, max=255): AssertionError: If the input image is not a grayscale image. Note: - This function applies binary thresholding using cv2.THRESH_BINARY. - Pixels with values greater than the threshold are set to the maximum value, - and pixels with values less than or equal to the threshold are set to 0. + This function applies thresholding using the specified threshold type. + Pixels are processed according to the chosen thresholding method. + + Relative threshold values are in the range [0, 1] where 0 is the minimum + and 1 is the maximum pixel value in the image. Example: >>> import cv3 @@ -43,10 +51,32 @@ def threshold(img: np.ndarray, thr=127, max=255): >>> # Create a simple grayscale image >>> img = np.zeros((100, 100), dtype=np.uint8) >>> img[25:75, 25:75] = 128 # Gray square - >>> # Apply threshold - >>> thresh = cv3.threshold(img, 100, 255) + >>> # Apply threshold with default type + >>> thresh = cv3.threshold(img, 100) + >>> # Apply threshold with custom type + >>> thresh = cv3.threshold(img, 100, type='binary_inv') + >>> # Apply threshold with relative threshold value + >>> thresh = cv3.threshold(img, 0.5, rel=True) """ assert img.ndim == 2, '`img` must be gray image' - # TODO if img.max() < 1 - _, thresh = cv2.threshold(img, thr, max, cv2.THRESH_BINARY) + + # Handle max value + if max is None: + max = 255 + # Handle threshold type + if type is None: + type = opt.THRESHOLD_TYPE + elif isinstance(type, str): + type = _threshold_type_flag_match(type) + else: + # Validate flag type + from ._draw import _THRESHOLD_TYPE_DICT + assert type in _THRESHOLD_TYPE_DICT.values(), 'invalid threshold type flag: {}'.format(type) + + # Handle relative threshold value + if _relative_check(thr, rel=rel): + thr = thr * img.max() + + _, thresh = cv2.threshold(img, thr, max, type) return thresh + diff --git a/cv3/transform.py b/cv3/transform.py index 600a128..776b9ce 100644 --- a/cv3/transform.py +++ b/cv3/transform.py @@ -103,7 +103,7 @@ def transform(img, angle, scale, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONST 'area', 'cubic', 'lanczos4' or OpenCV flags. Defaults to cv2.INTER_LINEAR. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. Returns: numpy.ndarray: Transformed image. @@ -116,6 +116,8 @@ def transform(img, angle, scale, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONST >>> img[25:75, 25:75] = [255, 255, 255] # White square >>> # Rotate 45 degrees and scale by 1.5 >>> transformed = cv3.transform(img, 45, 1.5) + >>> # Rotate with custom border color + >>> transformed = cv3.transform(img, 45, 1.5, value='red') """ return _transform(img, angle, scale, inter=inter, border=border, value=value) @@ -130,7 +132,7 @@ def rotate(img, angle, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value 'area', 'cubic', 'lanczos4' or OpenCV flags. Defaults to cv2.INTER_LINEAR. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. Returns: numpy.ndarray: Rotated image. @@ -143,6 +145,8 @@ def rotate(img, angle, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value >>> img[25:75, 25:75] = [255, 255, 255] # White square >>> # Rotate 45 degrees >>> rotated = cv3.rotate(img, 45) + >>> # Rotate with custom border color + >>> rotated = cv3.rotate(img, 45, value=[0, 128, 128]) """ return _rotate(img, angle, inter=inter, border=border, value=value) @@ -157,7 +161,7 @@ def scale(img, factor, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value 'area', 'cubic', 'lanczos4' or OpenCV flags. Defaults to cv2.INTER_LINEAR. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. Returns: numpy.ndarray: Scaled image. @@ -170,6 +174,8 @@ def scale(img, factor, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value >>> img[25:75, 25:75] = [255, 255, 255] # White square >>> # Scale by 1.5 >>> scaled = cv3.scale(img, 1.5) + >>> # Scale with custom border color + >>> scaled = cv3.scale(img, 0.5, value='green') """ return _scale(img, factor, inter=inter, border=border, value=value) @@ -183,7 +189,7 @@ def shift(img, x, y, border=cv2.BORDER_CONSTANT, value=None, rel=None): y (int or float): Shift in y direction. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret x and y as relative values. Defaults to None. Returns: @@ -209,7 +215,7 @@ def xshift(img, x, border=cv2.BORDER_CONSTANT, value=None, rel=None): x (int or float): Shift in x direction. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret x as relative value. Defaults to None. Returns: @@ -235,7 +241,7 @@ def yshift(img, y, border=cv2.BORDER_CONSTANT, value=None, rel=None): y (int or float): Shift in y direction. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret y as relative value. Defaults to None. Returns: @@ -311,7 +317,7 @@ def pad(img, y0, y1, x0, x1, border=cv2.BORDER_CONSTANT, value=None, rel=None): y0, y1, x0, x1 (int or float): Padding values for each side. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret padding values as relative. Defaults to None. Returns: @@ -325,6 +331,8 @@ def pad(img, y0, y1, x0, x1, border=cv2.BORDER_CONSTANT, value=None, rel=None): >>> img[25:75, 25:75] = [255, 255, 255] # White square >>> # Pad with 10 pixels on each side >>> padded = cv3.pad(img, 10, 10, 10, 10) + >>> # Pad with custom border color + >>> padded = cv3.pad(img, 10, 10, 10, 10, value=[128, 128, 0]) """ return _pad(img, y0, y1, x0, x1, border=border, value=value, rel=rel) @@ -338,7 +346,7 @@ def translate(img, x, y, border=cv2.BORDER_CONSTANT, value=None, rel=None): y (int or float): Shift in y direction. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret x and y as relative values. Defaults to None. Returns: @@ -364,7 +372,7 @@ def xtranslate(img, x, border=cv2.BORDER_CONSTANT, value=None, rel=None): x (int or float): Shift in x direction. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret x as relative value. Defaults to None. Returns: @@ -390,7 +398,7 @@ def ytranslate(img, y, border=cv2.BORDER_CONSTANT, value=None, rel=None): y (int or float): Shift in y direction. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret y as relative value. Defaults to None. Returns: @@ -417,7 +425,7 @@ def rotate90(img, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value=None 'area', 'cubic', 'lanczos4' or OpenCV flags. Defaults to cv2.INTER_LINEAR. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. Returns: numpy.ndarray: Rotated image. @@ -443,7 +451,7 @@ def rotate180(img, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value=Non 'area', 'cubic', 'lanczos4' or OpenCV flags. Defaults to cv2.INTER_LINEAR. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. Returns: numpy.ndarray: Rotated image. @@ -469,7 +477,7 @@ def rotate270(img, inter=cv2.INTER_LINEAR, border=cv2.BORDER_CONSTANT, value=Non 'area', 'cubic', 'lanczos4' or OpenCV flags. Defaults to cv2.INTER_LINEAR. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. Returns: numpy.ndarray: Rotated image. @@ -494,7 +502,7 @@ def copyMakeBorder(img, y0, y1, x0, x1, border=cv2.BORDER_CONSTANT, value=None, y0, y1, x0, x1 (int or float): Padding values for each side. border (int or str, optional): Border type. Can be one of: 'constant', 'replicate', 'reflect', 'wrap', 'default' or OpenCV flags. Defaults to cv2.BORDER_CONSTANT. - value: Border value for constant border type. Defaults to None. + value: Border color value for constant border type. Defaults to None. rel (bool, optional): Whether to interpret padding values as relative. Defaults to None. Returns: @@ -508,5 +516,7 @@ def copyMakeBorder(img, y0, y1, x0, x1, border=cv2.BORDER_CONSTANT, value=None, >>> img[25:75, 25:75] = [255, 255, 255] # White square >>> # Pad with 10 pixels on each side >>> padded = cv3.copyMakeBorder(img, 10, 10, 10, 10) + >>> # Pad with custom border color + >>> padded = cv3.copyMakeBorder(img, 10, 10, 10, 10, value='green') """ return _copyMakeBorder(img, y0, y1, x0, x1, border=border, value=value, rel=rel) diff --git a/cv3/video.py b/cv3/video.py index 399913c..93371bf 100644 --- a/cv3/video.py +++ b/cv3/video.py @@ -113,6 +113,16 @@ def __init__(self, src: Union[Path, str, int]): IsADirectoryError: If src is a directory. FileNotFoundError: If src is a file path that doesn't exist. OSError: If the video stream cannot be opened. + + Examples: + >>> # Read from a video file + >>> cap = VideoCapture('video.mp4') + >>> + >>> # Read from a camera (index 0) + >>> cap = VideoCapture(0) + >>> + >>> # Read from a camera (by string index) + >>> cap = VideoCapture('0') """ if isinstance(src, str) and src.isdecimal(): src = int(src) @@ -124,7 +134,7 @@ def __init__(self, src: Union[Path, str, int]): src = str(src) self.stream = BaseVideoCapture(src) if not self.is_opened: - raise OSError(f"Video from source {src} didn't open") + raise OSError("Video from source {} didn't open".format(src)) self.frame_cnt = round(self.stream.get(cv2.CAP_PROP_FRAME_COUNT)) self.fps = round(self.stream.get(cv2.CAP_PROP_FPS)) self.width = round(self.stream.get(cv2.CAP_PROP_FRAME_WIDTH)) @@ -152,7 +162,7 @@ def read(self): StopIteration: If the video has finished. """ if not self.is_opened: - raise OSError(f"Video is closed") + raise OSError("Video is closed") _, frame = self.stream.read() if frame is None: raise StopIteration('Video has finished') @@ -236,6 +246,20 @@ def __init__(self, save_path, fps=None, fourcc=None, mkdir=False): fourcc (str or int, optional): FOURCC code for video codec. Defaults to opt.FOURCC. mkdir (bool, optional): If True, create parent directories if they don't exist. Defaults to False. + + Examples: + >>> # Create a video writer with default settings + >>> writer = VideoWriter('output.mp4') + >>> + >>> # Create a video writer with custom FPS + >>> writer = VideoWriter('output.mp4', fps=30) + >>> + >>> # Create a video writer with string FOURCC + >>> writer = VideoWriter('output.mp4', fourcc='mp4v') + >>> + >>> # Create a video writer with integer FOURCC + >>> import cv2 + >>> writer = VideoWriter('output.mp4', fourcc=cv2.VideoWriter_fourcc(*'mp4v')) """ if mkdir: Path(save_path).parent.mkdir(parents=True, exist_ok=True) @@ -287,8 +311,8 @@ def write(self, frame: np.ndarray): self.height, self.width = frame.shape[:2] self.stream = BaseVideoWriter(self.save_path, self.fourcc, self.fps, (self.width, self.height)) if not self.is_opened: - raise OSError(f"Stream is closed") - assert (self.height, self.width) == frame.shape[:2], f'Shape mismatch. Required: {self.shape}' + raise OSError("Stream is closed") + assert (self.height, self.width) == frame.shape[:2], 'Shape mismatch. Required: {}'.format(self.shape) if opt.RGB: frame = rgb(frame) self.stream.write(frame) @@ -308,12 +332,22 @@ def Video(path, mode='r', **kwds): Returns: VideoCapture or VideoWriter: VideoCapture instance if mode='r', - VideoWriter instance if mode='w'. + VideoWriter instance if mode='w'. Raises: TypeError: If keyword arguments are passed in 'r' mode. + + Examples: + >>> # Create a video reading stream + >>> reader = Video('video.mp4', mode='r') + >>> + >>> # Create a video writing stream + >>> writer = Video('output.mp4', mode='w') + >>> + >>> # Create a video writing stream with custom parameters + >>> writer = Video('output.mp4', mode='w', fps=30, fourcc='mp4v') """ - assert mode in 'rw' + assert mode in ('r', 'w'), "Mode must be 'r' or 'w'" if mode == 'r': if kwds: raise TypeError( diff --git a/setup.py b/setup.py index 52c0d17..fb7b0a4 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='cv3', - version='1.3.0', + version='1.3.1', packages=['cv3'], url='https://github.com/gorodion/cv3', license='Apache 2.0', diff --git a/tests/test_other.py b/tests/test_other.py index eebb97c..6118fb2 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -36,3 +36,116 @@ def test_gray2rgb(): # image with shape (height, width, 3) with pytest.raises(ValueError): cv3.gray2rgba(img) + + +def test_opt_video(): + # Test video function with fps parameter + original_fps = cv3.opt.FPS + cv3.opt.video(fps=60) + assert cv3.opt.FPS == 60 + + # Test video function with fourcc parameter as string + original_fourcc = cv3.opt.FOURCC + cv3.opt.video(fourcc='MJPG') + assert isinstance(cv3.opt.FOURCC, int) + + # Test video function with fourcc parameter as int + fourcc_int = cv2.VideoWriter_fourcc(*'XVID') + cv3.opt.video(fourcc=fourcc_int) + assert cv3.opt.FOURCC == fourcc_int + + # Test assertions + with pytest.raises(AssertionError): + cv3.opt.video(fps=0) + + with pytest.raises(AssertionError): + cv3.opt.video(fourcc='INVALID') + + # Restore original values + cv3.opt.FPS = original_fps + cv3.opt.FOURCC = original_fourcc + + +def test_opt_draw(): + # Test draw function with various parameters + original_thickness = cv3.opt.THICKNESS + original_color = cv3.opt.COLOR + original_font = cv3.opt.FONT + original_pt_radius = cv3.opt.PT_RADIUS + original_scale = cv3.opt.SCALE + original_line_type = cv3.opt.LINE_TYPE + + # Test thickness parameter + cv3.opt.draw(thickness=3) + assert cv3.opt.THICKNESS == 3 + + # Test color parameter + cv3.opt.draw(color='red') + assert cv3.opt.COLOR == 'red' + + # Test font parameter + cv3.opt.draw(font=cv2.FONT_HERSHEY_PLAIN) + assert cv3.opt.FONT == cv2.FONT_HERSHEY_PLAIN + + # Test pt_radius parameter + cv3.opt.draw(pt_radius=5) + assert cv3.opt.PT_RADIUS == 5 + + # Test scale parameter + cv3.opt.draw(scale=2.0) + assert cv3.opt.SCALE == 2.0 + + # Test line_type parameter + cv3.opt.draw(line_type=cv2.LINE_8) + assert cv3.opt.LINE_TYPE == cv2.LINE_8 + + # Test assertions + with pytest.raises(AssertionError): + cv3.opt.draw(thickness=1.5) # Not an integer + + with pytest.raises(AssertionError): + cv3.opt.draw(pt_radius=-1) # Negative value + + with pytest.raises(AssertionError): + cv3.opt.draw(scale=0) # Not positive + + with pytest.raises(AssertionError): + cv3.opt.draw(scale=-1) # Negative + + with pytest.raises(AssertionError): + cv3.opt.draw(font=999) # Invalid font + + with pytest.raises(AssertionError): + cv3.opt.draw(line_type=999) # Invalid line type + + # Restore original values + cv3.opt.THICKNESS = original_thickness + cv3.opt.COLOR = original_color + cv3.opt.FONT = original_font + cv3.opt.PT_RADIUS = original_pt_radius + cv3.opt.SCALE = original_scale + cv3.opt.LINE_TYPE = original_line_type + + +def test_opt_rgb_functions(): + # Test set_rgb function + original_rgb = cv3.opt.RGB + cv3.opt.set_bgr() + assert cv3.opt.RGB == False + cv3.opt.set_rgb() + assert cv3.opt.RGB == True + + # Restore original value + cv3.opt.RGB = original_rgb + + +def test_opt_experimental(): + # Test set_exp function + original_exp = cv3.opt.EXPERIMENTAL + cv3.opt.set_exp(True) + assert cv3.opt.EXPERIMENTAL == True + cv3.opt.set_exp(False) + assert cv3.opt.EXPERIMENTAL == False + + # Restore original value + cv3.opt.EXPERIMENTAL = original_exp diff --git a/tests/test_processing.py b/tests/test_processing.py new file mode 100644 index 0000000..0bb0b5c --- /dev/null +++ b/tests/test_processing.py @@ -0,0 +1,141 @@ +import pytest +import numpy as np +import cv3 +import cv2 + +np.random.seed(10) + +def test_threshold_defaults(): + """Test threshold function with default parameters.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + + # Test with default parameters + thresh = cv3.threshold(img, 100) + + # Compare with OpenCV + _, expected = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY) + + assert np.array_equal(thresh, expected) + + +def test_threshold_with_max_value(): + """Test threshold function with custom max value.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + + # Test with custom max value + thresh = cv3.threshold(img, 100, max=128) + + # Compare with OpenCV + _, expected = cv2.threshold(img, 100, 128, cv2.THRESH_BINARY) + + assert np.array_equal(thresh, expected) + + +def test_threshold_with_type_string(): + """Test threshold function with string type parameter.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + + # Test with string type + thresh = cv3.threshold(img, 100, type='binary_inv') + + # Compare with OpenCV + _, expected = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY_INV) + + assert np.array_equal(thresh, expected) + + +def test_threshold_with_type_flag(): + """Test threshold function with flag type parameter.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + + # Test with flag type + thresh = cv3.threshold(img, 100, type=cv2.THRESH_TRUNC) + + # Compare with OpenCV + _, expected = cv2.threshold(img, 100, 255, cv2.THRESH_TRUNC) + + assert np.array_equal(thresh, expected) + +def test_threshold_relative(): + """Test threshold function with relative threshold value.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + max_val = img.max() + + # Test with relative threshold (0.5 * max_val) + thresh = cv3.threshold(img, 0.5, rel=True) + + # Compare with OpenCV (0.5 * max_val is the relative threshold) + _, expected = cv2.threshold(img, 0.5 * max_val, 255, cv2.THRESH_BINARY) + + assert np.array_equal(thresh, expected) + + + +def test_threshold_all_types(): + """Test all available threshold types.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + + types = [ + ('binary', cv2.THRESH_BINARY), + ('binary_inv', cv2.THRESH_BINARY_INV), + ('trunc', cv2.THRESH_TRUNC), + ('tozero', cv2.THRESH_TOZERO), + ('tozero_inv', cv2.THRESH_TOZERO_INV) + ] + + for type_str, type_flag in types: + # Test string type + thresh_str = cv3.threshold(img, 100, type=type_str) + + # Test flag type + thresh_flag = cv3.threshold(img, 100, type=type_flag) + + # Compare with OpenCV + _, expected = cv2.threshold(img, 100, 255, type_flag) + + assert np.array_equal(thresh_str, expected) + assert np.array_equal(thresh_flag, expected) + +def test_threshold_opt_type(): + """Test threshold function using opt.THRESHOLD_TYPE.""" + img = np.random.randint(0, 256, (100, 100), dtype=np.uint8) + + # Save original type + original_type = cv3.opt.THRESHOLD_TYPE + + try: + # Set to a different type + cv3.opt.THRESHOLD_TYPE = cv2.THRESH_BINARY_INV + + # Test with default type (should use opt.THRESHOLD_TYPE) + thresh = cv3.threshold(img, 100) + + # Compare with OpenCV + _, expected = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY_INV) + + assert np.array_equal(thresh, expected) + finally: + # Restore original type + cv3.opt.THRESHOLD_TYPE = original_type + + + +def test_threshold_invalid_type(): + """Test threshold function with invalid type parameter.""" + img = np.zeros((100, 100), dtype=np.uint8) + + # Test with invalid string type + with pytest.raises(AssertionError): + cv3.threshold(img, 100, type='invalid_type') + + # Test with invalid flag type + with pytest.raises(AssertionError): + cv3.threshold(img, 100, type=999999) + +def test_threshold_invalid_image(): + """Test threshold function with invalid image.""" + # Test with color image (3 channels) + img_color = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + + with pytest.raises(AssertionError): + cv3.threshold(img_color, 100) diff --git a/tests/test_video.py b/tests/test_video.py index 63c511d..a292137 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -75,7 +75,7 @@ def imgseq_fixture(): imgseq_dir = Path(IMGSEQ_DIR) imgseq_dir.mkdir(parents=True, exist_ok=True) for i, frame in enumerate(cv3.Video(TEST_VID)): - cv3.imwrite(imgseq_dir / f'img{i//30:02d}.png', frame) + cv3.imwrite(imgseq_dir / 'img{:02d}.png'.format(i//30), frame) yield shutil.rmtree(IMGSEQ_DIR, ignore_errors=True) diff --git a/tests/test_window.py b/tests/test_window.py index 0924bd0..5a368ee 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -78,7 +78,7 @@ def test_imshow_window_pos(): def test_window_move(): secs = 2 - with cv3.Window(f'Move in {secs} secs', pos=(0,0)) as w: + with cv3.Window('Move in {} secs'.format(secs), pos=(0,0)) as w: w.imshow(test_img) w.wait_key(secs * 1000) w.move(100, 100) From 34ac440e9bd8f1c4cee956b7951189939c472119 Mon Sep 17 00:00:00 2001 From: Rodion Date: Sun, 7 Sep 2025 02:28:26 +0300 Subject: [PATCH 2/2] 1.3.1 --- CHANGELOG.md | 1 + tests/test_io.py | 8 ++++---- tests/test_video.py | 8 ++++---- tests/test_window.py | 14 ++++++-------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3eb960..8e5e978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * got rid of f-strings for compatibility with early python versions * got rid of TODOs * small docs corrections +* replaced Path(...).unlink(missing_ok=True) with if Path(...).exists(): Path(...).unlink() for compatibility ### Deprecated diff --git a/tests/test_io.py b/tests/test_io.py index ee79e40..20f06ee 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -161,7 +161,7 @@ def test_imread_utf8_invalid(): def test_imwrite_str(): "Testing imwrite with string path" - Path(OUT_PATH_IMG).unlink(missing_ok=True) + if Path(OUT_PATH_IMG).exists(): Path(OUT_PATH_IMG).unlink() cv3.imwrite(OUT_PATH_IMG, test_img_bgr) assert os.path.isfile(OUT_PATH_IMG) @@ -171,7 +171,7 @@ def test_imwrite_str(): def test_imwrite_path(): "Testing imwrite with pathlib.Path" out_path = Path(OUT_PATH_IMG) - out_path.unlink(missing_ok=True) + if out_path.exists(): out_path.unlink() cv3.imwrite(out_path, test_img_bgr) assert out_path.is_file() @@ -228,9 +228,9 @@ def test_imwrite_invalid_extension(): @pytest.fixture() def write_utf8_fixture(): - Path(UTF8_PATH).unlink(missing_ok=True) + if Path(UTF8_PATH).exists(): Path(UTF8_PATH).unlink() yield - Path(UTF8_PATH).unlink(missing_ok=True) + if Path(UTF8_PATH).exists(): Path(UTF8_PATH).unlink() @pytest.mark.usefixtures('write_utf8_fixture') diff --git a/tests/test_video.py b/tests/test_video.py index a292137..d241869 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -164,11 +164,11 @@ def test_video_extra_kw(): @pytest.fixture() def out_path_fixture(): - Path(OUT_PATH_VID).unlink(missing_ok=True) - Path(OUT_PATH_VID_AVI).unlink(missing_ok=True) + if Path(OUT_PATH_VID).exists(): Path(OUT_PATH_VID).unlink() + if Path(OUT_PATH_VID_AVI).exists(): Path(OUT_PATH_VID_AVI).unlink() yield - Path(OUT_PATH_VID).unlink(missing_ok=True) - Path(OUT_PATH_VID_AVI).unlink(missing_ok=True) + if Path(OUT_PATH_VID).exists(): Path(OUT_PATH_VID).unlink() + if Path(OUT_PATH_VID_AVI).exists(): Path(OUT_PATH_VID_AVI).unlink() class TestWriterOpenWrite: diff --git a/tests/test_window.py b/tests/test_window.py index 5a368ee..1ad944e 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -66,14 +66,12 @@ def test_imshow_pil(): def test_imshow_window_pos(): - with ( - cv3.Window('pos 0,0', pos=(0,0)) as w1, - cv3.Window('pos 960,540', pos=(480,270)) as w2, - cv3.Window('pos 0,700', pos=(0,540)) as w3, - ): - for w in w1, w2, w3: - w.imshow(test_img) - cv3.wait_key(2000) + with cv3.Window('pos 0,0', pos=(0,0)) as w1: + with cv3.Window('pos 960,540', pos=(480,270)) as w2: + with cv3.Window('pos 0,700', pos=(0,540)) as w3: + for w in (w1, w2, w3): + w.imshow(test_img) + cv3.wait_key(2000) def test_window_move():