Skip to content

Commit c407f3e

Browse files
shagrenlpasselin
andauthored
Refactor handles and add tests (#59)
* Simple test * More functions supported * Fixes, Example * Add code quality tool Reformat code Add type hintings Fix code errors * Add .mypy_cache to .gitignore * Make line length 120 chars * Initial playback support * Seek support * Add tests * Format tests too * Remove commented code * Smaller asset. Refactor tests. Patch module for tests. * Fix build * Remove debug * Validate if open() method called twice * Remove unused file placeholder * Better typing * Merge develop Refactor tests * CI changes * CI changes * CI changes * CI changes * CI changes * CI changes * Some changes in tests definitions * Fix Maklefile Remove unused total_capsules * Fix * Update playback_seek_timestamp function description * Typofix * Update readme with small details. * rename _thread_safe to thread_safe * reformat line for readability * force ubuntu-18.04. Version supported by SDK * WIP * WIP * Fix build * Rebase mkv-support-3 * Refactor tests Add unit tests * WIP: color controls * More color-module tests CI * Refactor device_get_color_control_capabilities() * WIP: IMU support * CI fix * start/stop cameras support * device_get_capture support * Remove debug lines * support of get_capture, get_imu_sample * WIP: Calibration * WIP: Capture * Support device_get_raw_calibration * Support creating calibration from json file * convert_3d_to_3d support * calibration_2d_to_3d support * WIP: Transformations support * Transformation functions * Refactor transformation * Refactor examples * Add benchmark example * Fix tests * Better playback example get_previouse_capture support * Fix playback example * Rollback some text changes * rename capsule_xxxx_name to CAPSULE_XXXX_NAME * Text fix * Typo fix * Refactor examples * CR changes * CR changes * CR changes * CR changes * CR changes * add py.typed to distribution * remove not required _start_imu() call * fix PytestAssertRewriteWarning Fix will start workign with pytest 6.1 see pytest-dev/pytest#7700 Co-authored-by: Louis-Philippe Asselin <[email protected]> Co-authored-by: Louis-Philippe Asselin <[email protected]>
1 parent 8e20b2a commit c407f3e

39 files changed

+2538
-476
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ jobs:
6565
pip install -e .
6666
- name: Run tests
6767
run: |
68-
make test
68+
make test-no-hardware

Makefile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
SOURCES=pyk4a example tests
2-
2+
TESTS=tests
33
.PHONY: setup fmt lint test help build
44
.SILENT: help
55
help:
@@ -9,7 +9,10 @@ help:
99
"- build: Build and install pyk4a package\n" \
1010
"- fmt: Format all code\n" \
1111
"- lint: Lint code syntax and formatting\n" \
12-
"- test: Run tests"
12+
"- test: Run tests\n"\
13+
"- test-hardware: Run tests related from connected kinect"
14+
"- test-no-hardware: Run tests without connected kinect"
15+
1316

1417
setup:
1518
pip install -r requirements-dev.txt
@@ -27,4 +30,10 @@ lint:
2730
mypy $(SOURCES)
2831

2932
test:
30-
pytest --cov=pyk4a
33+
pytest --cov=pyk4a --verbose $(TESTS)
34+
35+
test-hardware:
36+
pytest --cov=pyk4a -m "device" --verbose $(TESTS)
37+
38+
test-no-hardware:
39+
pytest --cov=pyk4a -m "not device" --verbose $(TESTS)

example/benchmark.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from argparse import Action, ArgumentParser, Namespace
2+
from enum import Enum
3+
from time import monotonic
4+
5+
from pyk4a import FPS, ColorResolution, Config, DepthMode, ImageFormat, PyK4A, WiredSyncMode
6+
7+
8+
class EnumAction(Action):
9+
"""
10+
Argparse action for handling Enums
11+
"""
12+
13+
def __init__(self, **kwargs):
14+
# Pop off the type value
15+
enum = kwargs.pop("type", None)
16+
17+
# Ensure an Enum subclass is provided
18+
if enum is None:
19+
raise ValueError("type must be assigned an Enum when using EnumAction")
20+
if not issubclass(enum, Enum):
21+
raise TypeError("type must be an Enum when using EnumAction")
22+
23+
# Generate choices from the Enum
24+
kwargs.setdefault("choices", tuple(e.name for e in enum))
25+
26+
super(EnumAction, self).__init__(**kwargs)
27+
28+
self._enum = enum
29+
30+
def __call__(self, parser, namespace, values, option_string=None):
31+
# Convert value back into an Enum
32+
setattr(namespace, self.dest, self._enum(values))
33+
34+
35+
class EnumActionTuned(Action):
36+
"""
37+
Argparse action for handling Enums
38+
"""
39+
40+
def __init__(self, **kwargs):
41+
# Pop off the type value
42+
enum = kwargs.pop("type", None)
43+
44+
# Ensure an Enum subclass is provided
45+
if enum is None:
46+
raise ValueError("type must be assigned an Enum when using EnumAction")
47+
if not issubclass(enum, Enum):
48+
raise TypeError("type must be an Enum when using EnumAction")
49+
50+
# Generate choices from the Enum
51+
kwargs.setdefault("choices", tuple(e.name.split("_")[-1] for e in enum))
52+
53+
super(EnumActionTuned, self).__init__(**kwargs)
54+
55+
self._enum = enum
56+
57+
def __call__(self, parser, namespace, values, option_string=None):
58+
# Convert value back into an Enum
59+
items = {item.name.split("_")[-1]: item.value for item in self._enum}
60+
setattr(namespace, self.dest, self._enum(items[values]))
61+
62+
63+
def parse_args() -> Namespace:
64+
parser = ArgumentParser(
65+
description="Camera captures transfer speed benchmark. \n"
66+
"You can check if you USB controller/cable has enough performance."
67+
)
68+
parser.add_argument("--device-id", type=int, default=0, help="Device ID, from zero. Default: 0")
69+
parser.add_argument(
70+
"--color-resolution",
71+
type=ColorResolution,
72+
action=EnumActionTuned,
73+
default=ColorResolution.RES_720P,
74+
help="Color sensor resoultion. Default: 720P",
75+
)
76+
parser.add_argument(
77+
"--color-format",
78+
type=ImageFormat,
79+
action=EnumActionTuned,
80+
default=ImageFormat.COLOR_BGRA32,
81+
help="Color color_image color_format. Default: BGRA32",
82+
)
83+
parser.add_argument(
84+
"--depth-mode",
85+
type=DepthMode,
86+
action=EnumAction,
87+
default=DepthMode.NFOV_UNBINNED,
88+
help="Depth sensor mode. Default: NFOV_UNBINNED",
89+
)
90+
parser.add_argument(
91+
"--camera-fps", type=FPS, action=EnumActionTuned, default=FPS.FPS_30, help="Camera FPS. Default: 30"
92+
)
93+
parser.add_argument(
94+
"--synchronized-images-only",
95+
action="store_true",
96+
dest="synchronized_images_only",
97+
help="Only synchronized color and depth images, default",
98+
)
99+
parser.add_argument(
100+
"--no-synchronized-images",
101+
action="store_false",
102+
dest="synchronized_images_only",
103+
help="Color and Depth images can be non synced.",
104+
)
105+
parser.set_defaults(synchronized_images_only=True)
106+
parser.add_argument(
107+
"--wired-sync-mode",
108+
type=WiredSyncMode,
109+
action=EnumActionTuned,
110+
default=WiredSyncMode.STANDALONE,
111+
help="Wired sync mode. Default: STANDALONE",
112+
)
113+
return parser.parse_args()
114+
115+
116+
def bench(config: Config, device_id: int):
117+
device = PyK4A(config=config, device_id=device_id)
118+
device.start()
119+
depth = color = depth_period = color_period = 0
120+
print("Press CTRL-C top stop benchmark")
121+
started_at = started_at_period = monotonic()
122+
while True:
123+
try:
124+
capture = device.get_capture()
125+
if capture.color is not None:
126+
color += 1
127+
color_period += 1
128+
if capture.depth is not None:
129+
depth += 1
130+
depth_period += 1
131+
elapsed_period = monotonic() - started_at_period
132+
if elapsed_period >= 2:
133+
print(
134+
f"Color: {color_period / elapsed_period:0.2f} FPS, Depth: {depth_period / elapsed_period: 0.2f} FPS"
135+
)
136+
color_period = depth_period = 0
137+
started_at_period = monotonic()
138+
except KeyboardInterrupt:
139+
break
140+
elapsed = monotonic() - started_at
141+
device.stop()
142+
print()
143+
print(f"Result: Color: {color / elapsed:0.2f} FPS, Depth: {depth / elapsed: 0.2f} FPS")
144+
145+
146+
def main():
147+
args = parse_args()
148+
config = Config(
149+
color_resolution=args.color_resolution,
150+
color_format=args.color_format,
151+
depth_mode=args.depth_mode,
152+
synchronized_images_only=args.synchronized_images_only,
153+
wired_sync_mode=args.wired_sync_mode,
154+
)
155+
bench(config, args.device_id)
156+
157+
158+
if __name__ == "__main__":
159+
main()

example/color_formats.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,43 @@
22
import numpy as np
33

44
import pyk4a
5+
from helpers import convert_to_bgra_if_required
56
from pyk4a import Config, PyK4A
67

78

89
def get_color_image_size(config, imshow=True):
910
if imshow:
1011
cv2.namedWindow("k4a")
1112
k4a = PyK4A(config)
12-
k4a.connect()
13+
k4a.start()
1314
count = 0
1415
while count < 60:
1516
capture = k4a.get_capture()
1617
if np.any(capture.color):
1718
count += 1
1819
if imshow:
19-
cv2.imshow("k4a", convert_to_bgra_if_required(k4a, capture.color))
20+
cv2.imshow("k4a", convert_to_bgra_if_required(config.color_format, capture.color))
2021
cv2.waitKey(10)
2122
cv2.destroyAllWindows()
22-
k4a.disconnect()
23+
k4a.stop()
2324
return capture.color.nbytes
2425

2526

26-
def convert_to_bgra_if_required(k4a, img_color):
27-
# examples for all possible pyk4a.ColorFormats
28-
if k4a._config.color_format == pyk4a.ColorFormat.MJPG:
29-
img_color = cv2.imdecode(img_color, cv2.IMREAD_COLOR)
30-
elif k4a._config.color_format == pyk4a.ColorFormat.NV12:
31-
img_color = cv2.cvtColor(img_color, cv2.COLOR_YUV2BGRA_NV12)
32-
# this also works and it explains how the NV12 color format is stored in memory
33-
# h, w = img_color.shape[0:2]
34-
# h = h // 3 * 2
35-
# luminance = img_color[:h]
36-
# chroma = img_color[h:, :w//2]
37-
# img_color = cv2.cvtColorTwoPlane(luminance, chroma, cv2.COLOR_YUV2BGRA_NV12)
38-
elif k4a._config.color_format == pyk4a.ColorFormat.YUY2:
39-
img_color = cv2.cvtColor(img_color, cv2.COLOR_YUV2BGRA_YUY2)
40-
return img_color
41-
42-
4327
if __name__ == "__main__":
4428
imshow = True
45-
config_BGRA32 = Config(color_format=pyk4a.ColorFormat.BGRA32)
46-
config_MJPG = Config(color_format=pyk4a.ColorFormat.MJPG)
47-
config_NV12 = Config(color_format=pyk4a.ColorFormat.NV12)
48-
config_YUY2 = Config(color_format=pyk4a.ColorFormat.YUY2)
29+
config_BGRA32 = Config(color_format=pyk4a.ImageFormat.COLOR_BGRA32)
30+
config_MJPG = Config(color_format=pyk4a.ImageFormat.COLOR_MJPG)
31+
config_NV12 = Config(color_format=pyk4a.ImageFormat.COLOR_NV12)
32+
config_YUY2 = Config(color_format=pyk4a.ImageFormat.COLOR_YUY2)
4933

5034
nbytes_BGRA32 = get_color_image_size(config_BGRA32, imshow=imshow)
5135
nbytes_MJPG = get_color_image_size(config_MJPG, imshow=imshow)
5236
nbytes_NV12 = get_color_image_size(config_NV12, imshow=imshow)
5337
nbytes_YUY2 = get_color_image_size(config_YUY2, imshow=imshow)
5438

55-
print(f"{nbytes_BGRA32} {nbytes_MJPG}")
56-
print(f"BGRA32 is {nbytes_BGRA32/nbytes_MJPG} larger")
39+
print(f"BGRA32: {nbytes_BGRA32}, MJPG: {nbytes_MJPG}, NV12: {nbytes_NV12}, YUY2: {nbytes_YUY2}")
40+
print(f"BGRA32 is {nbytes_BGRA32/nbytes_MJPG:0.2f} larger than MJPG")
5741

5842
# output:
5943
# nbytes_BGRA32=3686400 nbytes_MJPG=229693
60-
# BGRA32 is 16.04924834452944 larger
44+
# COLOR_BGRA32 is 16.04924834452944 larger

example/helpers.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import Optional, Tuple
2+
3+
import cv2
4+
import numpy as np
5+
6+
from pyk4a import ImageFormat
7+
8+
9+
def convert_to_bgra_if_required(color_format: ImageFormat, color_image):
10+
# examples for all possible pyk4a.ColorFormats
11+
if color_format == ImageFormat.COLOR_MJPG:
12+
color_image = cv2.imdecode(color_image, cv2.IMREAD_COLOR)
13+
elif color_format == ImageFormat.COLOR_NV12:
14+
color_image = cv2.cvtColor(color_image, cv2.COLOR_YUV2BGRA_NV12)
15+
# this also works and it explains how the COLOR_NV12 color color_format is stored in memory
16+
# h, w = color_image.shape[0:2]
17+
# h = h // 3 * 2
18+
# luminance = color_image[:h]
19+
# chroma = color_image[h:, :w//2]
20+
# color_image = cv2.cvtColorTwoPlane(luminance, chroma, cv2.COLOR_YUV2BGRA_NV12)
21+
elif color_format == ImageFormat.COLOR_YUY2:
22+
color_image = cv2.cvtColor(color_image, cv2.COLOR_YUV2BGRA_YUY2)
23+
return color_image
24+
25+
26+
def colorize(
27+
image: np.ndarray,
28+
clipping_range: Tuple[Optional[int], Optional[int]] = (None, None),
29+
colormap: int = cv2.COLORMAP_HSV,
30+
) -> np.ndarray:
31+
if clipping_range[0] or clipping_range[1]:
32+
img = image.clip(clipping_range[0], clipping_range[1])
33+
else:
34+
img = image.copy()
35+
img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
36+
img = cv2.applyColorMap(img, colormap)
37+
return img

example/imu.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ def main():
2525
synchronized_images_only=True,
2626
)
2727
)
28-
k4a.connect()
29-
k4a._start_imu()
28+
k4a.start()
3029

3130
plt.ion()
3231
fig, axes = plt.subplots(3, sharex=False)
@@ -75,7 +74,7 @@ def main():
7574
fig.canvas.flush_events()
7675

7776
k4a._stop_imu()
78-
k4a.disconnect()
77+
k4a.stop()
7978

8079

8180
if __name__ == "__main__":

example/playback.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
from argparse import ArgumentParser
2-
from json import dumps, loads
32

3+
import cv2
4+
5+
from helpers import colorize, convert_to_bgra_if_required
46
from pyk4a import PyK4APlayback
57

68

79
def info(playback: PyK4APlayback):
810
print(f"Record length: {playback.length / 1000000: 0.2f} sec")
911

10-
calibration_str = playback.calibration_json
11-
calibration_formatted = dumps(loads(calibration_str), indent=2)
12-
print("=== Calibration ===")
13-
print(calibration_formatted)
12+
13+
def play(playback: PyK4APlayback):
14+
while True:
15+
try:
16+
capture = playback.get_next_capture()
17+
if capture.color is not None:
18+
cv2.imshow("Color", convert_to_bgra_if_required(playback.configuration["color_format"], capture.color))
19+
if capture.depth is not None:
20+
cv2.imshow("Depth", colorize(capture.depth, (None, 5000)))
21+
key = cv2.waitKey(10)
22+
if key != -1:
23+
break
24+
except EOFError:
25+
break
26+
cv2.destroyAllWindows()
1427

1528

1629
def main() -> None:
@@ -29,6 +42,7 @@ def main() -> None:
2942

3043
if offset != 0.0:
3144
playback.seek(int(offset * 1000000))
45+
play(playback)
3246

3347
playback.close()
3448

example/threads.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ def __init__(self, device_id=0, thread_safe: bool = True):
3636
def run(self) -> None:
3737
print("Start run")
3838
camera = PyK4A(device_id=self._device_id, thread_safe=self.thread_safe)
39-
camera.connect()
39+
camera.start()
4040
while not self._halt:
4141
capture = camera.get_capture()
4242
assert capture.depth is not None
4343
self._count += 1
4444
sleep(0.1)
45-
camera.disconnect()
45+
camera.stop()
4646
del camera
4747
print("Stop run")
4848

0 commit comments

Comments
 (0)