Skip to content

Commit ab59dbb

Browse files
committed
conv/opencv: Add stride support
Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
1 parent 332825e commit ab59dbb

3 files changed

Lines changed: 91 additions & 54 deletions

File tree

pixutils/conv/conv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def to_bgr888(
7070
if backend == 'opencv':
7171
from .opencv import opencv_to_bgr888
7272

73-
result = opencv_to_bgr888(fmt, width, height, arr, options)
73+
result = opencv_to_bgr888(fmt, width, height, bytesperline, arr, options)
7474
if result is not None:
7575
return result
7676
# opencv couldn't handle this format/options, try next backend

pixutils/conv/opencv.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ def _can_use_opencv_rgb(fmt: PixelFormat) -> bool:
6161

6262

6363
def opencv_to_bgr888(
64-
fmt: PixelFormat, width: int, height: int, arr: npt.NDArray[np.uint8], options: dict | None
64+
fmt: PixelFormat,
65+
width: int,
66+
height: int,
67+
bytesperline: int,
68+
arr: npt.NDArray[np.uint8],
69+
options: dict | None,
6570
) -> npt.NDArray[np.uint8] | None:
6671
if fmt.color == PixelColorEncoding.YUV:
6772
if not _can_use_opencv_yuv(fmt, options):
@@ -78,4 +83,4 @@ def opencv_to_bgr888(
7883
# Import and call implementation only if format is supported
7984
from .opencv_impl import opencv_convert
8085

81-
return opencv_convert(fmt, width, height, arr)
86+
return opencv_convert(fmt, width, height, bytesperline, arr)

pixutils/conv/opencv_impl.py

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
from __future__ import annotations
55

6-
from typing import Callable, cast
6+
from typing import cast
77

88
import cv2 # type: ignore[import-not-found]
99
import numpy as np
1010
import numpy.typing as npt
11+
from numpy.lib.stride_tricks import as_strided
1112

1213
from pixutils.formats import PixelFormat, PixelColorEncoding
1314

@@ -20,95 +21,126 @@
2021
'GBRG': cv2.COLOR_BAYER_GB2BGR,
2122
}
2223

23-
# Tuple is (cv2 color code or None, reshape function)
24-
RGB_FORMAT_MAP: dict[str, tuple[int | None, Callable]] = {
24+
RGB_FORMAT_MAP: dict[str, int | None] = {
2525
# 32-bit BGRA formats
26-
'XRGB8888': (cv2.COLOR_BGRA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
27-
'BGRX8888': (cv2.COLOR_BGRA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
28-
'ARGB8888': (cv2.COLOR_BGRA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
29-
'BGRA8888': (cv2.COLOR_BGRA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
26+
'XRGB8888': cv2.COLOR_BGRA2BGR,
27+
'BGRX8888': cv2.COLOR_BGRA2BGR,
28+
'ARGB8888': cv2.COLOR_BGRA2BGR,
29+
'BGRA8888': cv2.COLOR_BGRA2BGR,
3030
# 32-bit RGBA formats
31-
'XBGR8888': (cv2.COLOR_RGBA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
32-
'RGBX8888': (cv2.COLOR_RGBA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
33-
'ABGR8888': (cv2.COLOR_RGBA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
34-
'RGBA8888': (cv2.COLOR_RGBA2BGR, lambda b, w, h: b.reshape(h, w, 4)),
31+
'XBGR8888': cv2.COLOR_RGBA2BGR,
32+
'RGBX8888': cv2.COLOR_RGBA2BGR,
33+
'ABGR8888': cv2.COLOR_RGBA2BGR,
34+
'RGBA8888': cv2.COLOR_RGBA2BGR,
3535
# 24-bit formats
36-
'RGB888': (cv2.COLOR_RGB2BGR, lambda b, w, h: b.reshape(h, w, 3)),
37-
'BGR888': (None, lambda b, w, h: b.reshape(h, w, 3)),
36+
'RGB888': cv2.COLOR_RGB2BGR,
37+
'BGR888': None, # Already BGR
3838
}
3939

40-
# Tuple is (cv2 color code or None, reshape function)
41-
YUV_FORMAT_MAP: dict[str, tuple[int, Callable]] = {
42-
'YUYV': (cv2.COLOR_YUV2BGR_YUY2, lambda b, w, h: b.reshape(h, w, 2)),
43-
'UYVY': (cv2.COLOR_YUV2BGR_UYVY, lambda b, w, h: b.reshape(h, w, 2)),
44-
'YVYU': (cv2.COLOR_YUV2BGR_YVYU, lambda b, w, h: b.reshape(h, w, 2)),
45-
'NV12': (cv2.COLOR_YUV2BGR_NV12, lambda b, w, h: b.reshape(h * 3 // 2, w)),
46-
'NV21': (cv2.COLOR_YUV2BGR_NV21, lambda b, w, h: b.reshape(h * 3 // 2, w)),
40+
YUV_FORMAT_MAP: dict[str, int] = {
41+
'YUYV': cv2.COLOR_YUV2BGR_YUY2,
42+
'UYVY': cv2.COLOR_YUV2BGR_UYVY,
43+
'YVYU': cv2.COLOR_YUV2BGR_YVYU,
44+
'NV12': cv2.COLOR_YUV2BGR_NV12,
45+
'NV21': cv2.COLOR_YUV2BGR_NV21,
4746
}
4847

4948

5049
def _convert_yuv(
51-
fmt: PixelFormat, width: int, height: int, arr: npt.NDArray[np.uint8]
50+
fmt: PixelFormat, width: int, height: int, stride: int, arr: npt.NDArray[np.uint8]
5251
) -> npt.NDArray[np.uint8]:
53-
cv_code, reshape_func = YUV_FORMAT_MAP[fmt.name]
54-
reshaped = reshape_func(arr, width, height)
52+
cv_code = YUV_FORMAT_MAP[fmt.name]
53+
54+
if len(fmt.planes) == 1:
55+
# Packed formats (YUYV, UYVY, YVYU)
56+
plane = fmt.planes[0]
57+
bytes_per_pixel = plane.bytes_per_block // plane.pixels_per_block
58+
59+
# OpenCV requires 3D array with channel dimension
60+
reshaped = as_strided(
61+
arr,
62+
shape=(height, width, bytes_per_pixel),
63+
strides=(stride, bytes_per_pixel, 1),
64+
writeable=False,
65+
)
66+
else:
67+
# Multi-plane formats (NV12, NV21)
68+
# OpenCV expects concatenated layout: (h * 3/2, w)
69+
reshaped = arr.reshape(height * 3 // 2, width)
70+
5571
return cv2.cvtColor(reshaped, cv_code)
5672

5773

5874
def _convert_rgb(
59-
fmt: PixelFormat, width: int, height: int, arr: npt.NDArray[np.uint8]
75+
fmt: PixelFormat, width: int, height: int, stride: int, arr: npt.NDArray[np.uint8]
6076
) -> npt.NDArray[np.uint8]:
61-
cv_code, reshape_func = RGB_FORMAT_MAP[fmt.name]
62-
reshaped = reshape_func(arr, width, height)
77+
cv_code = RGB_FORMAT_MAP[fmt.name]
78+
79+
# Generic bytes_per_pixel from plane info
80+
plane = fmt.planes[0]
81+
bytes_per_pixel = plane.bytes_per_block // plane.pixels_per_block
82+
83+
# OpenCV requires 3D array with channel dimension
84+
reshaped = as_strided(
85+
arr,
86+
shape=(height, width, bytes_per_pixel),
87+
strides=(stride, bytes_per_pixel, 1),
88+
writeable=False,
89+
)
6390

6491
if cv_code is None:
65-
# Already BGR, just return a copy
6692
return reshaped.copy()
6793

6894
return cv2.cvtColor(reshaped, cv_code)
6995

7096

7197
def _convert_raw(
72-
fmt: PixelFormat, width: int, height: int, arr: npt.NDArray[np.uint8]
98+
fmt: PixelFormat, width: int, height: int, stride: int, arr: npt.NDArray[np.uint8]
7399
) -> npt.NDArray[np.uint8] | None:
74100
pattern = fmt.bayer_pattern
75101
assert pattern is not None
76102
cv_code = BAYER_PATTERN_MAP[pattern]
77103

78104
name = fmt.name
105+
plane = fmt.planes[0]
106+
107+
# Determine element size from plane info
108+
bytes_per_pixel = plane.bytes_per_block // plane.pixels_per_block
79109

80-
# Determine bit depth from format name
81-
if '8' in name:
82-
# 8-bit: reshape to (h, w) uint8
83-
bayer = arr.reshape(height, width)
110+
if bytes_per_pixel == 1:
111+
# 8-bit formats
112+
bayer = as_strided(arr, shape=(height, width), strides=(stride, 1), writeable=False)
84113
return cast(npt.NDArray[np.uint8], cv2.cvtColor(bayer, cv_code))
85-
elif '16' in name:
86-
# 16-bit: reshape to (h, w) uint16, convert, then scale to 8-bit
87-
bayer = arr.view(np.uint16).reshape(height, width)
88-
bgr16 = cast(npt.NDArray[np.uint16], cv2.cvtColor(bayer, cv_code))
89-
return (bgr16 >> 8).astype(np.uint8)
90-
elif '10' in name or '12' in name:
91-
# 10/12-bit unpacked (stored in 16-bit): shift up to use full 16-bit range
92-
bits = 10 if '10' in name else 12
93-
bayer = arr.view(np.uint16).reshape(height, width)
94-
bayer = bayer << (16 - bits)
95-
bgr16 = cast(npt.NDArray[np.uint16], cv2.cvtColor(bayer, cv_code))
96-
return (bgr16 >> 8).astype(np.uint8)
97-
else:
98-
# Unknown bit depth
99-
return None
114+
elif bytes_per_pixel == 2:
115+
# 16-bit formats (could be 10, 12, or 16 bit stored in 16)
116+
arr16 = arr.view(np.uint16)
117+
bayer = as_strided(arr16, shape=(height, width), strides=(stride, 2), writeable=False)
118+
119+
# Detect actual bit depth from format name for scaling
120+
if '16' in name:
121+
bgr16 = cast(npt.NDArray[np.uint16], cv2.cvtColor(bayer, cv_code))
122+
return (bgr16 >> 8).astype(np.uint8)
123+
elif '10' in name or '12' in name:
124+
bits = 10 if '10' in name else 12
125+
bayer = bayer << (16 - bits)
126+
bgr16 = cast(npt.NDArray[np.uint16], cv2.cvtColor(bayer, cv_code))
127+
return (bgr16 >> 8).astype(np.uint8)
128+
129+
return None
100130

101131

102132
def opencv_convert(
103-
fmt: PixelFormat, width: int, height: int, arr: npt.NDArray[np.uint8]
133+
fmt: PixelFormat, width: int, height: int, bytesperline: int, arr: npt.NDArray[np.uint8]
104134
) -> npt.NDArray[np.uint8] | None:
135+
stride = bytesperline if bytesperline > 0 else fmt.stride(width, 0)
136+
105137
if fmt.color == PixelColorEncoding.YUV:
106-
return _convert_yuv(fmt, width, height, arr)
138+
return _convert_yuv(fmt, width, height, stride, arr)
107139

108140
if fmt.color == PixelColorEncoding.RAW:
109-
return _convert_raw(fmt, width, height, arr)
141+
return _convert_raw(fmt, width, height, stride, arr)
110142

111143
if fmt.color == PixelColorEncoding.RGB:
112-
return _convert_rgb(fmt, width, height, arr)
144+
return _convert_rgb(fmt, width, height, stride, arr)
113145

114146
return None

0 commit comments

Comments
 (0)