├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── example ├── benchmark.py ├── color_formats.py ├── devices.py ├── helpers.py ├── imu.py ├── playback.py ├── record.py ├── threads.py ├── viewer.py ├── viewer_depth.py ├── viewer_point_cloud.py └── viewer_transformation.py ├── figs └── pyk4a_logo.png ├── pyk4a ├── __init__.py ├── calibration.py ├── capture.py ├── config.py ├── errors.py ├── module.py ├── playback.py ├── py.typed ├── pyk4a.cpp ├── pyk4a.py ├── record.py ├── results.py ├── transformation.py └── win32_utils.py ├── pyproject.toml ├── requirements-dev.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── assets ├── calibration.json └── recording.mkv ├── conftest.py ├── functional ├── __init__.py ├── conftest.py ├── test_calibration.py ├── test_capture.py ├── test_device.py ├── test_playback.py └── test_record.py ├── plugins ├── calibration.py ├── capture.py ├── device.py └── playback.py └── unit ├── __init__.py ├── test_calibration.py ├── test_device.py ├── test_playback.py └── test_record.py /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveBitFields: false 9 | AlignConsecutiveDeclarations: false 10 | AlignEscapedNewlines: Right 11 | AlignOperands: Align 12 | AlignTrailingComments: true 13 | AllowAllArgumentsOnNextLine: true 14 | AllowAllConstructorInitializersOnNextLine: true 15 | AllowAllParametersOfDeclarationOnNextLine: true 16 | AllowShortEnumsOnASingleLine: true 17 | AllowShortBlocksOnASingleLine: Never 18 | AllowShortCaseLabelsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: All 20 | AllowShortLambdasOnASingleLine: All 21 | AllowShortIfStatementsOnASingleLine: Never 22 | AllowShortLoopsOnASingleLine: false 23 | AlwaysBreakAfterDefinitionReturnType: None 24 | AlwaysBreakAfterReturnType: None 25 | AlwaysBreakBeforeMultilineStrings: false 26 | AlwaysBreakTemplateDeclarations: MultiLine 27 | BinPackArguments: true 28 | BinPackParameters: true 29 | BraceWrapping: 30 | AfterCaseLabel: false 31 | AfterClass: false 32 | AfterControlStatement: Never 33 | AfterEnum: false 34 | AfterFunction: false 35 | AfterNamespace: false 36 | AfterObjCDeclaration: false 37 | AfterStruct: false 38 | AfterUnion: false 39 | AfterExternBlock: false 40 | BeforeCatch: false 41 | BeforeElse: false 42 | BeforeLambdaBody: false 43 | BeforeWhile: false 44 | IndentBraces: false 45 | SplitEmptyFunction: true 46 | SplitEmptyRecord: true 47 | SplitEmptyNamespace: true 48 | BreakBeforeBinaryOperators: None 49 | BreakBeforeBraces: Attach 50 | BreakBeforeInheritanceComma: false 51 | BreakInheritanceList: BeforeColon 52 | BreakBeforeTernaryOperators: true 53 | BreakConstructorInitializersBeforeComma: false 54 | BreakConstructorInitializers: BeforeColon 55 | BreakAfterJavaFieldAnnotations: false 56 | BreakStringLiterals: true 57 | ColumnLimit: 120 58 | CommentPragmas: '^ IWYU pragma:' 59 | CompactNamespaces: false 60 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 61 | ConstructorInitializerIndentWidth: 4 62 | ContinuationIndentWidth: 4 63 | Cpp11BracedListStyle: true 64 | DeriveLineEnding: true 65 | DerivePointerAlignment: false 66 | DisableFormat: false 67 | ExperimentalAutoDetectBinPacking: false 68 | FixNamespaceComments: true 69 | ForEachMacros: 70 | - foreach 71 | - Q_FOREACH 72 | - BOOST_FOREACH 73 | IncludeBlocks: Preserve 74 | IncludeCategories: 75 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 76 | Priority: 2 77 | SortPriority: 0 78 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 79 | Priority: 3 80 | SortPriority: 0 81 | - Regex: '.*' 82 | Priority: 1 83 | SortPriority: 0 84 | IncludeIsMainRegex: '(Test)?$' 85 | IncludeIsMainSourceRegex: '' 86 | IndentCaseLabels: false 87 | IndentCaseBlocks: false 88 | IndentGotoLabels: true 89 | IndentPPDirectives: None 90 | IndentExternBlock: AfterExternBlock 91 | IndentWidth: 2 92 | IndentWrappedFunctionNames: false 93 | InsertTrailingCommas: None 94 | JavaScriptQuotes: Leave 95 | JavaScriptWrapImports: true 96 | KeepEmptyLinesAtTheStartOfBlocks: true 97 | MacroBlockBegin: '' 98 | MacroBlockEnd: '' 99 | MaxEmptyLinesToKeep: 1 100 | NamespaceIndentation: None 101 | ObjCBinPackProtocolList: Auto 102 | ObjCBlockIndentWidth: 2 103 | ObjCBreakBeforeNestedBlockParam: true 104 | ObjCSpaceAfterProperty: false 105 | ObjCSpaceBeforeProtocolList: true 106 | PenaltyBreakAssignment: 2 107 | PenaltyBreakBeforeFirstCallParameter: 19 108 | PenaltyBreakComment: 300 109 | PenaltyBreakFirstLessLess: 120 110 | PenaltyBreakString: 1000 111 | PenaltyBreakTemplateDeclaration: 10 112 | PenaltyExcessCharacter: 1000000 113 | PenaltyReturnTypeOnItsOwnLine: 60 114 | PointerAlignment: Right 115 | ReflowComments: true 116 | SortIncludes: true 117 | SortUsingDeclarations: true 118 | SpaceAfterCStyleCast: false 119 | SpaceAfterLogicalNot: false 120 | SpaceAfterTemplateKeyword: true 121 | SpaceBeforeAssignmentOperators: true 122 | SpaceBeforeCpp11BracedList: false 123 | SpaceBeforeCtorInitializerColon: true 124 | SpaceBeforeInheritanceColon: true 125 | SpaceBeforeParens: ControlStatements 126 | SpaceBeforeRangeBasedForLoopColon: true 127 | SpaceInEmptyBlock: false 128 | SpaceInEmptyParentheses: false 129 | SpacesBeforeTrailingComments: 1 130 | SpacesInAngles: false 131 | SpacesInConditionalStatement: false 132 | SpacesInContainerLiterals: true 133 | SpacesInCStyleCastParentheses: false 134 | SpacesInParentheses: false 135 | SpacesInSquareBrackets: false 136 | SpaceBeforeSquareBrackets: false 137 | Standard: Latest 138 | StatementMacros: 139 | - Q_UNUSED 140 | - QT_REQUIRE_VERSION 141 | TabWidth: 8 142 | UseCRLF: false 143 | UseTab: Never 144 | WhitespaceSensitiveMacros: 145 | - STRINGIZE 146 | - PP_STRINGIZE 147 | - BOOST_PP_STRINGIZE 148 | ... 149 | 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - master 9 | - develop 10 | tags: 11 | - "[0-9]+.[0-9]+" 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-18.04 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Setup Python 3.9 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 3.9 24 | - name: Cache PyPI 25 | uses: actions/cache@v2 26 | with: 27 | key: pip-cache-lint-${{ hashFiles('requirements-dev.txt') }} 28 | path: ~/.cache/pip 29 | restore-keys: | 30 | pip-cache-lint 31 | - name: Install dependencies 32 | uses: py-actions/py-dependency-install@v2 33 | with: 34 | path: requirements-dev.txt 35 | - name: Add clang repository 36 | uses: myci-actions/add-deb-repo@4 37 | with: 38 | repo: deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-11 main 39 | repo-name: llvm 40 | keys: 15CF4D18AF4F7421 41 | key-server: keyserver.ubuntu.com 42 | - name: Install clang-format 43 | run: | 44 | sudo apt install -yq clang-format-11 45 | sudo ln -s /usr/bin/clang-format-11 /usr/local/bin/clang-format 46 | - name: Run linters 47 | run: | 48 | make lint 49 | test: 50 | strategy: 51 | matrix: 52 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 53 | fail-fast: false 54 | name: Test 55 | runs-on: ubuntu-18.04 56 | steps: 57 | - name: Prepare Environment 58 | run: | 59 | sudo apt install -q -y curl software-properties-common build-essential 60 | curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 61 | sudo apt-add-repository https://packages.microsoft.com/ubuntu/18.04/prod 62 | env DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y sudo -E apt install -q -y libk4a1.4-dev 63 | - name: Checkout 64 | uses: actions/checkout@v2 65 | - name: Setup Python ${{ matrix.python-version }} 66 | uses: actions/setup-python@v2 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | - name: Cache PyPI 70 | uses: actions/cache@v2 71 | with: 72 | key: pip-cache-test-${{ matrix.python-version }}-${{ hashFiles('requirements-dev.txt') }}-${{ hashFiles('setup.py') }} 73 | path: ~/.cache/pip 74 | restore-keys: | 75 | pip-cache-test-${{ matrix.python-version }} 76 | - name: Install dependencies 77 | uses: py-actions/py-dependency-install@v2 78 | with: 79 | path: requirements-dev.txt 80 | - name: Install module 81 | run: 82 | pip install -e . 83 | - name: Run tests 84 | run: | 85 | make test-ci 86 | - name: Coverage 87 | uses: codecov/codecov-action@v1 88 | with: 89 | file: ./coverage.xml # optional 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .eggs/ 10 | .Python 11 | env/ 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | .pytest_cache 36 | coverage.xml 37 | 38 | # Test files 39 | test_images 40 | 41 | # Translations 42 | *.mo 43 | 44 | # Mr Developer 45 | .mr.developer.cfg 46 | .project 47 | .pydevproject 48 | 49 | # Rope 50 | .ropeproject 51 | 52 | # Django stuff: 53 | *.log 54 | *.pot 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # viewdoc output 60 | .long-description.html 61 | 62 | # Vim cruft 63 | .*.swp 64 | 65 | #emacs 66 | *~ 67 | \#*# 68 | .#* 69 | 70 | #VS Code 71 | .vscode 72 | 73 | #Komodo 74 | *.komodoproject 75 | 76 | #OS 77 | .DS_Store 78 | 79 | # JetBrains 80 | .idea 81 | 82 | # mypy cache 83 | .mypy_cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 etiennedub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES=pyk4a example tests 2 | CPP_SOURCES=pyk4a/pyk4a.cpp 3 | TESTS=tests 4 | .PHONY: setup fmt lint test help build 5 | .SILENT: help 6 | help: 7 | echo \ 8 | "Available targets: \n" \ 9 | "- setup: Install required for development packages\n" \ 10 | "- build: Build and install pyk4a package\n" \ 11 | "- fmt: Format all code\n" \ 12 | "- lint: Lint code syntax and formatting\n" \ 13 | "- test: Run tests\n"\ 14 | "- test-hardware: Run tests related from connected kinect" 15 | "- test-no-hardware: Run tests without connected kinect" 16 | 17 | 18 | setup: 19 | pip install -r requirements-dev.txt 20 | 21 | build: 22 | pip install -e . 23 | 24 | fmt: 25 | isort $(SOURCES) 26 | black $(SOURCES) 27 | clang-format -i $(CPP_SOURCES) 28 | 29 | lint: 30 | black --check $(SOURCES) 31 | flake8 $(SOURCES) 32 | mypy $(SOURCES) 33 | clang-format --Werror --dry-run $(CPP_SOURCES) 34 | 35 | test: 36 | pytest $(TESTS) 37 | 38 | test-hardware: 39 | pytest -m "device" $(TESTS) 40 | 41 | test-no-hardware: 42 | pytest -m "not device" $(TESTS) 43 | 44 | test-ci: 45 | pytest -m "not device and not opengl" $(TESTS) 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyk4a 2 | 3 | ![CI](https://github.com/etiennedub/pyk4a/workflows/CI/badge.svg) 4 | [![codecov](https://codecov.io/gh/etiennedub/pyk4a/branch/master/graph/badge.svg)](https://codecov.io/gh/etiennedub/pyk4a) 5 | 6 | ![pyk4a](https://github.com/etiennedub/pyk4a/raw/master/figs/pyk4a_logo.png) 7 | 8 | 9 | This library is a simple and pythonic wrapper in Python 3 for the Azure-Kinect-Sensor-SDK. 10 | 11 | Images are returned as numpy arrays and behave like python objects. 12 | 13 | This approach incurs almost no overhead in terms of CPU, memory or other resources. 14 | It also simplifies usage. Kinect C api image buffers are directly reused and image releases are performed automatically by the python garbage collector. 15 | 16 | Homepage: https://github.com/etiennedub/pyk4a/ 17 | 18 | ## Prerequisites 19 | The [Azure-Kinect-Sensor-SDK](https://github.com/microsoft/Azure-Kinect-Sensor-SDK) is required to build this library. 20 | To use the SDK, refer to the installation instructions [here](https://github.com/microsoft/Azure-Kinect-Sensor-SDK/blob/develop/docs/usage.md). 21 | 22 | 23 | ## Install 24 | 25 | ### Linux 26 | 27 | Linux specific installation instructions [here](https://docs.microsoft.com/en-us/azure/kinect-dk/sensor-sdk-download#linux-installation-instructions) 28 | 29 | Install both packages `libk4a.` and `libk4a.-dev`. The latter contains the headers and CMake files to build pyk4a. 30 | 31 | Make sure your `LD_LIBRARY_PATH` contains the directory of k4a.lib 32 | 33 | ```shell 34 | pip install pyk4a 35 | ``` 36 | 37 | ### Windows 38 | 39 | In most cases `pip install pyk4a` is enough to install this package. 40 | 41 | When using an anaconda environment, you need to set the environment variable `CONDA_DLL_SEARCH_MODIFICATION_ENABLE=1` https://github.com/conda/conda/issues/10897 42 | 43 | Because of the numerous issues received from Windows users, the installer ([setup.py](setup.py)) automatically detects the kinect SDK path. 44 | 45 | When the installer is not able to find the path, the following snippet can help. 46 | Make sure you replace the paths in these instructions with your own kinect SDK path. It is important to replace 1.4.1 with your installed version of the SDK. 47 | ```shell 48 | pip install pyk4a --no-use-pep517 --global-option=build_ext --global-option="-IC:\Program Files\Azure Kinect SDK v1.4.1\sdk\include" --global-option="-LC:\Program Files\Azure Kinect SDK v1.4.1\sdk\windows-desktop\amd64\release\lib" 49 | ``` 50 | 51 | During execution, `k4a.dll` is required. The automatic detection should be able to find this file. 52 | It is also possible to specify the DLL's directory with the environment variable `K4A_DLL_DIR`. 53 | If `K4A_DLL_DIR` is used, the automatic DLL search is not performed. 54 | 55 | ## Example 56 | 57 | For a basic example displaying the first frame, you can run this code: 58 | 59 | ``` 60 | from pyk4a import PyK4A 61 | 62 | # Load camera with the default config 63 | k4a = PyK4A() 64 | k4a.start() 65 | 66 | # Get the next capture (blocking function) 67 | capture = k4a.get_capture() 68 | img_color = capture.color 69 | 70 | # Display with pyplot 71 | from matplotlib import pyplot as plt 72 | plt.imshow(img_color[:, :, 2::-1]) # BGRA to RGB 73 | plt.show() 74 | ``` 75 | 76 | Otherwise, a more avanced example is available in the [example](https://github.com/etiennedub/pyk4a/tree/master/example) folder. 77 | To execute it [opencv-python](https://github.com/skvark/opencv-python) is required. 78 | ``` 79 | git clone https://github.com/etiennedub/pyk4a.git 80 | cd pyk4a/example 81 | python viewer.py 82 | ``` 83 | 84 | ## Documentation 85 | 86 | No documentation is available but all functinos are properly [type hinted](https://docs.python.org/3/library/typing.html). 87 | The code of the main class is a good place to start[PyK4A](https://github.com/etiennedub/pyk4a/blob/master/pyk4a/pyk4a.py). 88 | 89 | You can also follow the various [example folder](example) scripts as reference. 90 | 91 | 92 | ## Bug Reports 93 | Submit an issue and please include as much details as possible. 94 | 95 | Make sure to use the search function on closed issues, especially if your problem is related to installing on [windows](https://github.com/etiennedub/pyk4a/issues?q=windows+). 96 | 97 | 98 | ## Module Development 99 | 100 | 1) Install required packages: `make setup` 101 | 102 | 2) Install local pyk4a version (compiles pyk4a.cpp): `make build` 103 | 104 | ## Contribution 105 | 106 | Feel free to send pull requests. The develop branch should be used. 107 | 108 | Please rebuild, format, check code quality and run tests before submitting a pull request: 109 | ```shell script 110 | make build 111 | make fmt lint 112 | make test 113 | ``` 114 | 115 | Note: you need `clang-format` tool(v 11.0+) for formatting CPP code. 116 | -------------------------------------------------------------------------------- /example/benchmark.py: -------------------------------------------------------------------------------- 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=lambda i: ColorResolution(int(i)), 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=lambda i: ImageFormat(int(i)), 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=lambda i: DepthMode(int(i)), 86 | action=EnumAction, 87 | default=DepthMode.NFOV_UNBINNED, 88 | help="Depth sensor mode. Default: NFOV_UNBINNED", 89 | ) 90 | parser.add_argument( 91 | "--camera-fps", 92 | type=lambda i: FPS(int(i)), 93 | action=EnumActionTuned, 94 | default=FPS.FPS_30, 95 | help="Camera FPS. Default: 30", 96 | ) 97 | parser.add_argument( 98 | "--synchronized-images-only", 99 | action="store_true", 100 | dest="synchronized_images_only", 101 | help="Only synchronized color and depth images, default", 102 | ) 103 | parser.add_argument( 104 | "--no-synchronized-images", 105 | action="store_false", 106 | dest="synchronized_images_only", 107 | help="Color and Depth images can be non synced.", 108 | ) 109 | parser.set_defaults(synchronized_images_only=True) 110 | parser.add_argument( 111 | "--wired-sync-mode", 112 | type=lambda i: WiredSyncMode(int(i)), 113 | action=EnumActionTuned, 114 | default=WiredSyncMode.STANDALONE, 115 | help="Wired sync mode. Default: STANDALONE", 116 | ) 117 | return parser.parse_args() 118 | 119 | 120 | def bench(config: Config, device_id: int): 121 | device = PyK4A(config=config, device_id=device_id) 122 | device.start() 123 | depth = color = depth_period = color_period = 0 124 | print("Press CTRL-C top stop benchmark") 125 | started_at = started_at_period = monotonic() 126 | while True: 127 | try: 128 | capture = device.get_capture() 129 | if capture.color is not None: 130 | color += 1 131 | color_period += 1 132 | if capture.depth is not None: 133 | depth += 1 134 | depth_period += 1 135 | elapsed_period = monotonic() - started_at_period 136 | if elapsed_period >= 2: 137 | print( 138 | f"Color: {color_period / elapsed_period:0.2f} FPS, Depth: {depth_period / elapsed_period: 0.2f} FPS" 139 | ) 140 | color_period = depth_period = 0 141 | started_at_period = monotonic() 142 | except KeyboardInterrupt: 143 | break 144 | elapsed = monotonic() - started_at 145 | device.stop() 146 | print() 147 | print(f"Result: Color: {color / elapsed:0.2f} FPS, Depth: {depth / elapsed: 0.2f} FPS") 148 | 149 | 150 | def main(): 151 | args = parse_args() 152 | config = Config( 153 | color_resolution=args.color_resolution, 154 | color_format=args.color_format, 155 | depth_mode=args.depth_mode, 156 | synchronized_images_only=args.synchronized_images_only, 157 | wired_sync_mode=args.wired_sync_mode, 158 | ) 159 | bench(config, args.device_id) 160 | 161 | 162 | if __name__ == "__main__": 163 | main() 164 | -------------------------------------------------------------------------------- /example/color_formats.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | import pyk4a 5 | from helpers import convert_to_bgra_if_required 6 | from pyk4a import Config, PyK4A 7 | 8 | 9 | def get_color_image_size(config, imshow=True): 10 | if imshow: 11 | cv2.namedWindow("k4a") 12 | k4a = PyK4A(config) 13 | k4a.start() 14 | count = 0 15 | while count < 60: 16 | capture = k4a.get_capture() 17 | if np.any(capture.color): 18 | count += 1 19 | if imshow: 20 | cv2.imshow("k4a", convert_to_bgra_if_required(config.color_format, capture.color)) 21 | cv2.waitKey(10) 22 | cv2.destroyAllWindows() 23 | k4a.stop() 24 | return capture.color.nbytes 25 | 26 | 27 | if __name__ == "__main__": 28 | imshow = True 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) 33 | 34 | nbytes_BGRA32 = get_color_image_size(config_BGRA32, imshow=imshow) 35 | nbytes_MJPG = get_color_image_size(config_MJPG, imshow=imshow) 36 | nbytes_NV12 = get_color_image_size(config_NV12, imshow=imshow) 37 | nbytes_YUY2 = get_color_image_size(config_YUY2, imshow=imshow) 38 | 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") 41 | 42 | # output: 43 | # nbytes_BGRA32=3686400 nbytes_MJPG=229693 44 | # COLOR_BGRA32 is 16.04924834452944 larger 45 | -------------------------------------------------------------------------------- /example/devices.py: -------------------------------------------------------------------------------- 1 | from pyk4a import PyK4A, connected_device_count 2 | 3 | 4 | cnt = connected_device_count() 5 | if not cnt: 6 | print("No devices available") 7 | exit() 8 | print(f"Available devices: {cnt}") 9 | for device_id in range(cnt): 10 | device = PyK4A(device_id=device_id) 11 | device.open() 12 | print(f"{device_id}: {device.serial}") 13 | device.close() 14 | -------------------------------------------------------------------------------- /example/helpers.py: -------------------------------------------------------------------------------- 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]) # type: ignore 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 38 | -------------------------------------------------------------------------------- /example/imu.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import pyplot as plt 3 | 4 | import pyk4a 5 | from pyk4a import Config, PyK4A 6 | 7 | 8 | MAX_SAMPLES = 1000 9 | 10 | 11 | def set_default_data(data): 12 | data["acc_x"] = MAX_SAMPLES * [data["acc_x"][-1]] 13 | data["acc_y"] = MAX_SAMPLES * [data["acc_y"][-1]] 14 | data["acc_z"] = MAX_SAMPLES * [data["acc_z"][-1]] 15 | data["gyro_x"] = MAX_SAMPLES * [data["acc_x"][-1]] 16 | data["gyro_y"] = MAX_SAMPLES * [data["acc_y"][-1]] 17 | data["gyro_z"] = MAX_SAMPLES * [data["acc_z"][-1]] 18 | 19 | 20 | def main(): 21 | k4a = PyK4A( 22 | Config( 23 | color_resolution=pyk4a.ColorResolution.RES_720P, 24 | depth_mode=pyk4a.DepthMode.NFOV_UNBINNED, 25 | synchronized_images_only=True, 26 | ) 27 | ) 28 | k4a.start() 29 | 30 | plt.ion() 31 | fig, axes = plt.subplots(3, sharex=False) 32 | 33 | data = { 34 | "temperature": [0] * MAX_SAMPLES, 35 | "acc_x": [0] * MAX_SAMPLES, 36 | "acc_y": [0] * MAX_SAMPLES, 37 | "acc_z": [0] * MAX_SAMPLES, 38 | "acc_timestamp": [0] * MAX_SAMPLES, 39 | "gyro_x": [0] * MAX_SAMPLES, 40 | "gyro_y": [0] * MAX_SAMPLES, 41 | "gyro_z": [0] * MAX_SAMPLES, 42 | "gyro_timestamp": [0] * MAX_SAMPLES, 43 | } 44 | y = np.zeros(MAX_SAMPLES) 45 | lines = { 46 | "temperature": axes[0].plot(y, label="temperature")[0], 47 | "acc_x": axes[1].plot(y, label="acc_x")[0], 48 | "acc_y": axes[1].plot(y, label="acc_y")[0], 49 | "acc_z": axes[1].plot(y, label="acc_z")[0], 50 | "gyro_x": axes[2].plot(y, label="gyro_x")[0], 51 | "gyro_y": axes[2].plot(y, label="gyro_y")[0], 52 | "gyro_z": axes[2].plot(y, label="gyro_z")[0], 53 | } 54 | 55 | for i in range(MAX_SAMPLES): 56 | sample = k4a.get_imu_sample() 57 | sample["acc_x"], sample["acc_y"], sample["acc_z"] = sample.pop("acc_sample") 58 | sample["gyro_x"], sample["gyro_y"], sample["gyro_z"] = sample.pop("gyro_sample") 59 | for k, v in sample.items(): 60 | data[k][i] = v 61 | if i == 0: 62 | set_default_data(data) 63 | 64 | for k in ("temperature", "acc_x", "acc_y", "acc_z", "gyro_x", "gyro_y", "gyro_z"): 65 | lines[k].set_data(range(MAX_SAMPLES), data[k]) 66 | 67 | acc_y = data["acc_x"] + data["acc_y"] + data["acc_z"] 68 | gyro_y = data["gyro_x"] + data["gyro_y"] + data["gyro_z"] 69 | lines["acc_x"].axes.set_ylim(min(acc_y), max(acc_y)) 70 | lines["gyro_x"].axes.set_ylim(min(gyro_y), max(gyro_y)) 71 | lines["temperature"].axes.set_ylim(min(data["temperature"][0 : i + 1]), max(data["temperature"][0 : i + 1])) 72 | 73 | fig.canvas.draw() 74 | fig.canvas.flush_events() 75 | 76 | k4a._stop_imu() 77 | k4a.stop() 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /example/playback.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | import cv2 4 | 5 | from helpers import colorize, convert_to_bgra_if_required 6 | from pyk4a import PyK4APlayback 7 | 8 | 9 | def info(playback: PyK4APlayback): 10 | print(f"Record length: {playback.length / 1000000: 0.2f} sec") 11 | 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() 27 | 28 | 29 | def main() -> None: 30 | parser = ArgumentParser(description="pyk4a player") 31 | parser.add_argument("--seek", type=float, help="Seek file to specified offset in seconds", default=0.0) 32 | parser.add_argument("FILE", type=str, help="Path to MKV file written by k4arecorder") 33 | 34 | args = parser.parse_args() 35 | filename: str = args.FILE 36 | offset: float = args.seek 37 | 38 | playback = PyK4APlayback(filename) 39 | playback.open() 40 | 41 | info(playback) 42 | 43 | if offset != 0.0: 44 | playback.seek(int(offset * 1000000)) 45 | play(playback) 46 | 47 | playback.close() 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /example/record.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | from pyk4a import Config, ImageFormat, PyK4A, PyK4ARecord 4 | 5 | 6 | parser = ArgumentParser(description="pyk4a recorder") 7 | parser.add_argument("--device", type=int, help="Device ID", default=0) 8 | parser.add_argument("FILE", type=str, help="Path to MKV file") 9 | args = parser.parse_args() 10 | 11 | print(f"Starting device #{args.device}") 12 | config = Config(color_format=ImageFormat.COLOR_MJPG) 13 | device = PyK4A(config=config, device_id=args.device) 14 | device.start() 15 | 16 | print(f"Open record file {args.FILE}") 17 | record = PyK4ARecord(device=device, config=config, path=args.FILE) 18 | record.create() 19 | try: 20 | print("Recording... Press CTRL-C to stop recording.") 21 | while True: 22 | capture = device.get_capture() 23 | record.write_capture(capture) 24 | except KeyboardInterrupt: 25 | print("CTRL-C pressed. Exiting.") 26 | 27 | record.flush() 28 | record.close() 29 | print(f"{record.captures_count} frames written.") 30 | -------------------------------------------------------------------------------- /example/threads.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from math import sin 3 | from time import sleep 4 | from typing import Dict, List 5 | 6 | from pyk4a import PyK4A 7 | 8 | 9 | class Worker(threading.Thread): 10 | def __init__(self): 11 | self._halt = False 12 | self._count = 0 13 | super().__init__() 14 | 15 | def halt(self): 16 | self._halt = True 17 | 18 | @property 19 | def count(self): 20 | return self._count 21 | 22 | 23 | class CpuWorker(Worker): 24 | def run(self) -> None: 25 | while not self._halt: 26 | sin(self._count) 27 | self._count += 1 28 | 29 | 30 | class CameraWorker(Worker): 31 | def __init__(self, device_id=0, thread_safe: bool = True): 32 | self._device_id = device_id 33 | self.thread_safe = thread_safe 34 | super().__init__() 35 | 36 | def run(self) -> None: 37 | print("Start run") 38 | camera = PyK4A(device_id=self._device_id, thread_safe=self.thread_safe) 39 | camera.start() 40 | while not self._halt: 41 | capture = camera.get_capture() 42 | assert capture.depth is not None 43 | self._count += 1 44 | sleep(0.1) 45 | camera.stop() 46 | del camera 47 | print("Stop run") 48 | 49 | 50 | def bench(camera_workers: List[CameraWorker], cpu_workers: List[CpuWorker], duration: float) -> int: 51 | # start cameras 52 | for camera_worker in camera_workers: 53 | camera_worker.start() 54 | while not camera_worker.count: 55 | sleep(0.1) 56 | if not camera_worker.is_alive(): 57 | print("Cannot start camera") 58 | exit(1) 59 | # start cpu-bound workers 60 | for cpu_worker in cpu_workers: 61 | cpu_worker.start() 62 | 63 | sleep(duration) 64 | 65 | for cpu_worker in cpu_workers: 66 | cpu_worker.halt() 67 | for camera_worker in camera_workers: 68 | camera_worker.halt() 69 | 70 | # wait while all workers stop 71 | workers: List[Worker] = [*camera_workers, *cpu_workers] 72 | while True: 73 | for worker in workers: 74 | if worker.is_alive(): 75 | sleep(0.05) 76 | break 77 | else: 78 | break 79 | total = 0 80 | for cpu_worker in cpu_workers: 81 | total += cpu_worker.count 82 | return total 83 | 84 | 85 | def draw(results: Dict[int, Dict[bool, int]]): 86 | try: 87 | from matplotlib import pyplot as plt 88 | except ImportError: 89 | return 90 | plt.figure() 91 | plt.subplot(2, 1, 1) 92 | plt.title("Threading performance") 93 | plt.ylabel("Operations Count") 94 | plt.xlabel("CPU Workers count") 95 | plt.plot( 96 | results.keys(), 97 | [result[True] for result in results.values()], 98 | "r", 99 | label="Thread safe", 100 | ) 101 | plt.plot( 102 | results.keys(), 103 | [result[False] for result in results.values()], 104 | "g", 105 | label="Non thread safe", 106 | ) 107 | plt.legend() 108 | 109 | plt.subplot(2, 1, 2) 110 | plt.title("Difference") 111 | plt.ylabel("Difference, %") 112 | plt.xlabel("CPU Workers count") 113 | plt.plot( 114 | results.keys(), 115 | [float(result[False] - result[True]) / result[True] * 100 for result in results.values()], 116 | ) 117 | xmin, xmax, ymin, ymax = plt.axis() 118 | if ymin > 0: 119 | ymin = 0 120 | plt.axis([xmin, xmax, ymin, ymax]) 121 | 122 | plt.show() 123 | 124 | 125 | MAX_CPU_WORKERS_COUNT = 5 126 | DURATION = 10 127 | results: Dict[int, Dict[bool, int]] = {} 128 | for cpu_workers_count in range(1, MAX_CPU_WORKERS_COUNT + 1): 129 | result: Dict[bool, int] = {} 130 | for thread_safe in (True, False): 131 | camera_workers = [CameraWorker(thread_safe=thread_safe)] 132 | cpu_workers = [CpuWorker() for i in range(cpu_workers_count)] 133 | operations = bench(camera_workers=camera_workers, cpu_workers=cpu_workers, duration=DURATION) 134 | print(f"Bench result: cpu_workers={cpu_workers_count}, " f"thread_safe={thread_safe}, operations={operations}") 135 | result[thread_safe] = operations 136 | 137 | percent = float(result[False] - result[True]) / result[True] * 100 138 | print(f"Difference: {percent: 0.2f} %") 139 | results[cpu_workers_count] = result 140 | 141 | draw(results) 142 | -------------------------------------------------------------------------------- /example/viewer.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | import pyk4a 5 | from pyk4a import Config, PyK4A 6 | 7 | 8 | def main(): 9 | k4a = PyK4A( 10 | Config( 11 | color_resolution=pyk4a.ColorResolution.RES_720P, 12 | depth_mode=pyk4a.DepthMode.NFOV_UNBINNED, 13 | synchronized_images_only=True, 14 | ) 15 | ) 16 | k4a.start() 17 | 18 | # getters and setters directly get and set on device 19 | k4a.whitebalance = 4500 20 | assert k4a.whitebalance == 4500 21 | k4a.whitebalance = 4510 22 | assert k4a.whitebalance == 4510 23 | 24 | while 1: 25 | capture = k4a.get_capture() 26 | if np.any(capture.color): 27 | cv2.imshow("k4a", capture.color[:, :, :3]) 28 | key = cv2.waitKey(10) 29 | if key != -1: 30 | cv2.destroyAllWindows() 31 | break 32 | k4a.stop() 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /example/viewer_depth.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | import pyk4a 5 | from helpers import colorize 6 | from pyk4a import Config, PyK4A 7 | 8 | 9 | def main(): 10 | k4a = PyK4A( 11 | Config( 12 | color_resolution=pyk4a.ColorResolution.OFF, 13 | depth_mode=pyk4a.DepthMode.NFOV_UNBINNED, 14 | synchronized_images_only=False, 15 | ) 16 | ) 17 | k4a.start() 18 | 19 | # getters and setters directly get and set on device 20 | k4a.whitebalance = 4500 21 | assert k4a.whitebalance == 4500 22 | k4a.whitebalance = 4510 23 | assert k4a.whitebalance == 4510 24 | 25 | while True: 26 | capture = k4a.get_capture() 27 | if np.any(capture.depth): 28 | cv2.imshow("k4a", colorize(capture.depth, (None, 5000), cv2.COLORMAP_HSV)) 29 | key = cv2.waitKey(10) 30 | if key != -1: 31 | cv2.destroyAllWindows() 32 | break 33 | k4a.stop() 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /example/viewer_point_cloud.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import pyplot as plt 3 | from mpl_toolkits import mplot3d # noqa: F401 4 | 5 | import pyk4a 6 | from pyk4a import Config, PyK4A 7 | 8 | 9 | def main(): 10 | k4a = PyK4A( 11 | Config( 12 | color_resolution=pyk4a.ColorResolution.RES_720P, 13 | camera_fps=pyk4a.FPS.FPS_5, 14 | depth_mode=pyk4a.DepthMode.WFOV_2X2BINNED, 15 | synchronized_images_only=True, 16 | ) 17 | ) 18 | k4a.start() 19 | 20 | # getters and setters directly get and set on device 21 | k4a.whitebalance = 4500 22 | assert k4a.whitebalance == 4500 23 | k4a.whitebalance = 4510 24 | assert k4a.whitebalance == 4510 25 | while True: 26 | capture = k4a.get_capture() 27 | if np.any(capture.depth) and np.any(capture.color): 28 | break 29 | while True: 30 | capture = k4a.get_capture() 31 | if np.any(capture.depth) and np.any(capture.color): 32 | break 33 | points = capture.depth_point_cloud.reshape((-1, 3)) 34 | colors = capture.transformed_color[..., (2, 1, 0)].reshape((-1, 3)) 35 | 36 | fig = plt.figure() 37 | ax = fig.add_subplot(111, projection="3d") 38 | ax.scatter( 39 | points[:, 0], 40 | points[:, 1], 41 | points[:, 2], 42 | s=1, 43 | c=colors / 255, 44 | ) 45 | ax.set_xlabel("x") 46 | ax.set_ylabel("y") 47 | ax.set_zlabel("z") 48 | ax.set_xlim(-2000, 2000) 49 | ax.set_ylim(-2000, 2000) 50 | ax.set_zlim(0, 4000) 51 | ax.view_init(elev=-90, azim=-90) 52 | plt.show() 53 | 54 | k4a.stop() 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /example/viewer_transformation.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | import pyk4a 4 | from helpers import colorize 5 | from pyk4a import Config, PyK4A 6 | 7 | 8 | def main(): 9 | k4a = PyK4A( 10 | Config( 11 | color_resolution=pyk4a.ColorResolution.RES_720P, 12 | depth_mode=pyk4a.DepthMode.NFOV_UNBINNED, 13 | ) 14 | ) 15 | k4a.start() 16 | 17 | while True: 18 | capture = k4a.get_capture() 19 | if capture.depth is not None: 20 | cv2.imshow("Depth", colorize(capture.depth, (None, 5000))) 21 | if capture.ir is not None: 22 | cv2.imshow("IR", colorize(capture.ir, (None, 500), colormap=cv2.COLORMAP_JET)) 23 | if capture.color is not None: 24 | cv2.imshow("Color", capture.color) 25 | if capture.transformed_depth is not None: 26 | cv2.imshow("Transformed Depth", colorize(capture.transformed_depth, (None, 5000))) 27 | if capture.transformed_color is not None: 28 | cv2.imshow("Transformed Color", capture.transformed_color) 29 | if capture.transformed_ir is not None: 30 | cv2.imshow("Transformed IR", colorize(capture.transformed_ir, (None, 500), colormap=cv2.COLORMAP_JET)) 31 | 32 | key = cv2.waitKey(10) 33 | if key != -1: 34 | cv2.destroyAllWindows() 35 | break 36 | 37 | k4a.stop() 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /figs/pyk4a_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennedub/pyk4a/8c153164b484037bd7afc02db0c2be89e7bb8b38/figs/pyk4a_logo.png -------------------------------------------------------------------------------- /pyk4a/__init__.py: -------------------------------------------------------------------------------- 1 | from .calibration import Calibration, CalibrationType 2 | from .capture import PyK4ACapture 3 | from .config import ( 4 | FPS, 5 | ColorControlCommand, 6 | ColorControlMode, 7 | ColorResolution, 8 | Config, 9 | DepthMode, 10 | ImageFormat, 11 | WiredSyncMode, 12 | ) 13 | from .errors import K4AException, K4ATimeoutException 14 | from .module import k4a_module 15 | from .playback import PyK4APlayback, SeekOrigin 16 | from .pyk4a import ColorControlCapabilities, PyK4A 17 | from .record import PyK4ARecord 18 | from .transformation import ( 19 | color_image_to_depth_camera, 20 | depth_image_to_color_camera, 21 | depth_image_to_color_camera_custom, 22 | depth_image_to_point_cloud, 23 | ) 24 | 25 | 26 | def connected_device_count() -> int: 27 | return k4a_module.device_get_installed_count() 28 | 29 | 30 | __all__ = ( 31 | "Calibration", 32 | "CalibrationType", 33 | "FPS", 34 | "ColorControlCommand", 35 | "ColorControlMode", 36 | "ImageFormat", 37 | "ColorResolution", 38 | "Config", 39 | "DepthMode", 40 | "WiredSyncMode", 41 | "K4AException", 42 | "K4ATimeoutException", 43 | "PyK4A", 44 | "PyK4ACapture", 45 | "PyK4APlayback", 46 | "SeekOrigin", 47 | "ColorControlCapabilities", 48 | "color_image_to_depth_camera", 49 | "depth_image_to_point_cloud", 50 | "depth_image_to_color_camera", 51 | "depth_image_to_color_camera_custom", 52 | "PyK4ARecord", 53 | "connected_device_count", 54 | ) 55 | -------------------------------------------------------------------------------- /pyk4a/calibration.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import Optional, Tuple 3 | 4 | import numpy as np 5 | 6 | from .config import ColorResolution, DepthMode 7 | from .errors import K4AException, _verify_error 8 | from .module import k4a_module 9 | 10 | 11 | class CalibrationType(IntEnum): 12 | UNKNOWN = -1 # Unknown 13 | DEPTH = 0 # Depth Camera 14 | COLOR = 1 # Color Sensor 15 | GYRO = 2 # Gyroscope 16 | ACCEL = 3 # Accelerometer 17 | NUM = 4 # Number of types excluding unknown type 18 | 19 | 20 | class Calibration: 21 | def __init__( 22 | self, handle: object, depth_mode: DepthMode, color_resolution: ColorResolution, thread_safe: bool = True 23 | ): 24 | self._calibration_handle = handle 25 | self._transformation_handle: Optional[object] = None 26 | self.thread_safe = thread_safe 27 | self._depth_mode = depth_mode 28 | self._color_resolution = color_resolution 29 | self._raw: Optional[str] = None 30 | 31 | @classmethod 32 | def from_raw( 33 | cls, value: str, depth_mode: DepthMode, color_resolution: ColorResolution, thread_safe: bool = True 34 | ) -> "Calibration": 35 | res, handle = k4a_module.calibration_get_from_raw(thread_safe, value, depth_mode, color_resolution) 36 | _verify_error(res) 37 | return Calibration( 38 | handle=handle, depth_mode=depth_mode, color_resolution=color_resolution, thread_safe=thread_safe 39 | ) 40 | 41 | @property 42 | def depth_mode(self) -> DepthMode: 43 | return self._depth_mode 44 | 45 | @property 46 | def color_resolution(self) -> ColorResolution: 47 | return self._color_resolution 48 | 49 | def _convert_3d_to_3d( 50 | self, 51 | source_point_3d: Tuple[float, float, float], 52 | source_camera: CalibrationType, 53 | target_camera: CalibrationType, 54 | ) -> Tuple[float, float, float]: 55 | """ 56 | Transform a 3d point of a source coordinate system into a 3d 57 | point of the target coordinate system. 58 | :param source_point_3d The 3D coordinates in millimeters representing a point in source_camera. 59 | :param source_camera The current camera. 60 | :param target_camera The target camera. 61 | :return The 3D coordinates in millimeters representing a point in target camera. 62 | """ 63 | res, target_point_3d = k4a_module.calibration_3d_to_3d( 64 | self._calibration_handle, 65 | self.thread_safe, 66 | source_point_3d, 67 | source_camera, 68 | target_camera, 69 | ) 70 | 71 | _verify_error(res) 72 | return target_point_3d 73 | 74 | def depth_to_color_3d(self, point_3d: Tuple[float, float, float]) -> Tuple[float, float, float]: 75 | return self._convert_3d_to_3d(point_3d, CalibrationType.DEPTH, CalibrationType.COLOR) 76 | 77 | def color_to_depth_3d(self, point_3d: Tuple[float, float, float]) -> Tuple[float, float, float]: 78 | return self._convert_3d_to_3d(point_3d, CalibrationType.COLOR, CalibrationType.DEPTH) 79 | 80 | def _convert_2d_to_3d( 81 | self, 82 | source_pixel_2d: Tuple[float, float], 83 | source_depth: float, 84 | source_camera: CalibrationType, 85 | target_camera: CalibrationType, 86 | ) -> Tuple[float, float, float]: 87 | """ 88 | Transform a 3d point of a source coordinate system into a 3d 89 | point of the target coordinate system. 90 | :param source_pixel_2d The 2D coordinates in px of source_camera color_image. 91 | :param source_depth Depth in mm 92 | :param source_camera The current camera. 93 | :param target_camera The target camera. 94 | :return The 3D coordinates in mm representing a point in target camera. 95 | """ 96 | res, valid, target_point_3d = k4a_module.calibration_2d_to_3d( 97 | self._calibration_handle, 98 | self.thread_safe, 99 | source_pixel_2d, 100 | source_depth, 101 | source_camera, 102 | target_camera, 103 | ) 104 | 105 | _verify_error(res) 106 | if valid == 0: 107 | raise ValueError(f"Coordinates {source_pixel_2d} are not valid in the calibration model") 108 | 109 | return target_point_3d 110 | 111 | def convert_2d_to_3d( 112 | self, 113 | coordinates: Tuple[float, float], 114 | depth: float, 115 | source_camera: CalibrationType, 116 | target_camera: Optional[CalibrationType] = None, 117 | ): 118 | """ 119 | Transform a 2d pixel to a 3d point of the target coordinate system. 120 | """ 121 | if target_camera is None: 122 | target_camera = source_camera 123 | return self._convert_2d_to_3d(coordinates, depth, source_camera, target_camera) 124 | 125 | def _convert_3d_to_2d( 126 | self, 127 | source_point_3d: Tuple[float, float, float], 128 | source_camera: CalibrationType, 129 | target_camera: CalibrationType, 130 | ) -> Tuple[float, float]: 131 | """ 132 | Transform a 3d point of a source coordinate system into a 3d 133 | point of the target coordinate system. 134 | :param source_point_3d The 3D coordinates in mm of source_camera. 135 | :param source_camera The current camera. 136 | :param target_camera The target camera. 137 | :return The 3D coordinates in mm representing a point in target camera. 138 | """ 139 | res, valid, target_px_2d = k4a_module.calibration_3d_to_2d( 140 | self._calibration_handle, 141 | self.thread_safe, 142 | source_point_3d, 143 | source_camera, 144 | target_camera, 145 | ) 146 | 147 | _verify_error(res) 148 | if valid == 0: 149 | raise ValueError(f"Coordinates {source_point_3d} are not valid in the calibration model") 150 | 151 | return target_px_2d 152 | 153 | def convert_3d_to_2d( 154 | self, 155 | coordinates: Tuple[float, float, float], 156 | source_camera: CalibrationType, 157 | target_camera: Optional[CalibrationType] = None, 158 | ): 159 | """ 160 | Transform a 3d point to a 2d pixel of the target coordinate system. 161 | """ 162 | if target_camera is None: 163 | target_camera = source_camera 164 | return self._convert_3d_to_2d(coordinates, source_camera, target_camera) 165 | 166 | @property 167 | def transformation_handle(self) -> object: 168 | if not self._transformation_handle: 169 | handle = k4a_module.transformation_create(self._calibration_handle, self.thread_safe) 170 | if not handle: 171 | raise K4AException("Cannot create transformation handle") 172 | self._transformation_handle = handle 173 | return self._transformation_handle 174 | 175 | def get_camera_matrix(self, camera: CalibrationType) -> np.ndarray: 176 | """ 177 | Get the camera matrix (in OpenCV compatible format) for the color or depth camera 178 | """ 179 | if camera not in [CalibrationType.COLOR, CalibrationType.DEPTH]: 180 | raise ValueError("Camera matrix only available for color and depth cameras.") 181 | params = k4a_module.calibration_get_intrinsics(self._calibration_handle, self.thread_safe, camera) 182 | if len(params) != 14: 183 | raise ValueError("Unknown camera calibration type") 184 | 185 | cx, cy, fx, fy = params[:4] 186 | return np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) 187 | 188 | def get_distortion_coefficients(self, camera: CalibrationType) -> np.ndarray: 189 | """ 190 | Get the distortion coefficients (in OpenCV compatible format) for the color or depth camera 191 | """ 192 | if camera not in [CalibrationType.COLOR, CalibrationType.DEPTH]: 193 | raise ValueError("Distortion coefficients only available for color and depth cameras.") 194 | params = k4a_module.calibration_get_intrinsics(self._calibration_handle, self.thread_safe, camera) 195 | if len(params) != 14: 196 | raise ValueError("Unknown camera calibration type") 197 | 198 | return np.array([params[4], params[5], params[13], params[12], *params[6:10]]) 199 | 200 | def get_extrinsic_parameters( 201 | self, source_camera: CalibrationType, target_camera: CalibrationType 202 | ) -> Tuple[np.ndarray, np.ndarray]: 203 | """ 204 | The extrinsic parameters allow 3D coordinate conversions between depth camera, color camera, the IMU's gyroscope and accelerometer. 205 | """ 206 | params = k4a_module.calibration_get_extrinsics( 207 | self._calibration_handle, self.thread_safe, source_camera, target_camera 208 | ) 209 | 210 | rotation = np.reshape(np.array(params[0]), [3, 3]) 211 | translation = np.reshape(np.array(params[1]), [1, 3]) / 1000 # Millimeter to meter conversion 212 | 213 | return rotation, translation 214 | -------------------------------------------------------------------------------- /pyk4a/capture.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import numpy as np 4 | 5 | from .calibration import Calibration 6 | from .config import ImageFormat 7 | from .errors import K4AException 8 | from .module import k4a_module 9 | from .transformation import ( 10 | color_image_to_depth_camera, 11 | depth_image_to_color_camera, 12 | depth_image_to_color_camera_custom, 13 | depth_image_to_point_cloud, 14 | ) 15 | 16 | 17 | class PyK4ACapture: 18 | def __init__( 19 | self, calibration: Calibration, capture_handle: object, color_format: ImageFormat, thread_safe: bool = True 20 | ): 21 | self._calibration: Calibration = calibration 22 | self._capture_handle: object = capture_handle # built-in PyCapsule 23 | self.thread_safe = thread_safe 24 | self._color_format = color_format 25 | 26 | self._color: Optional[np.ndarray] = None 27 | self._color_timestamp_usec: int = 0 28 | self._color_system_timestamp_nsec: int = 0 29 | self._color_exposure_usec: Optional[int] = None 30 | self._color_iso_speed: Optional[int] = None 31 | self._color_white_balance: Optional[int] = None 32 | self._depth: Optional[np.ndarray] = None 33 | self._depth_timestamp_usec: int = 0 34 | self._depth_system_timestamp_nsec: int = 0 35 | self._ir: Optional[np.ndarray] = None 36 | self._ir_timestamp_usec: int = 0 37 | self._ir_system_timestamp_nsec: int = 0 38 | self._depth_point_cloud: Optional[np.ndarray] = None 39 | self._transformed_depth: Optional[np.ndarray] = None 40 | self._transformed_depth_point_cloud: Optional[np.ndarray] = None 41 | self._transformed_color: Optional[np.ndarray] = None 42 | self._transformed_ir: Optional[np.ndarray] = None 43 | 44 | @property 45 | def color(self) -> Optional[np.ndarray]: 46 | if self._color is None: 47 | ( 48 | self._color, 49 | self._color_timestamp_usec, 50 | self._color_system_timestamp_nsec, 51 | ) = k4a_module.capture_get_color_image(self._capture_handle, self.thread_safe) 52 | return self._color 53 | 54 | @property 55 | def color_timestamp_usec(self) -> int: 56 | """Device timestamp for color image. Not equal host machine timestamp!""" 57 | if self._color is None: 58 | self.color 59 | return self._color_timestamp_usec 60 | 61 | @property 62 | def color_system_timestamp_nsec(self) -> int: 63 | """System timestamp for color image in nanoseconds. Corresponds to Python's time.perf_counter_ns().""" 64 | if self._color is None: 65 | self.color 66 | return self._color_system_timestamp_nsec 67 | 68 | @property 69 | def color_exposure_usec(self) -> int: 70 | if self._color_exposure_usec is None: 71 | value = k4a_module.color_image_get_exposure_usec(self._capture_handle) 72 | if value == 0: 73 | raise K4AException("Cannot read exposure from color image") 74 | self._color_exposure_usec = value 75 | return self._color_exposure_usec 76 | 77 | @property 78 | def color_iso_speed(self) -> int: 79 | if self._color_iso_speed is None: 80 | value = k4a_module.color_image_get_iso_speed(self._capture_handle) 81 | if value == 0: 82 | raise K4AException("Cannot read iso from color_image") 83 | self._color_iso_speed = value 84 | return self._color_iso_speed 85 | 86 | @property 87 | def color_white_balance(self) -> int: 88 | if self._color_white_balance is None: 89 | value = k4a_module.color_image_get_white_balance(self._capture_handle) 90 | if value == 0: 91 | raise K4AException("Cannot read white balance from color image") 92 | self._color_white_balance = value 93 | return self._color_white_balance 94 | 95 | @property 96 | def depth(self) -> Optional[np.ndarray]: 97 | if self._depth is None: 98 | ( 99 | self._depth, 100 | self._depth_timestamp_usec, 101 | self._depth_system_timestamp_nsec, 102 | ) = k4a_module.capture_get_depth_image(self._capture_handle, self.thread_safe) 103 | return self._depth 104 | 105 | @property 106 | def depth_timestamp_usec(self) -> int: 107 | """Device timestamp for depth image. Not equal host machine timestamp!. Like as equal IR image timestamp""" 108 | if self._depth is None: 109 | self.depth 110 | return self._depth_timestamp_usec 111 | 112 | @property 113 | def depth_system_timestamp_nsec(self) -> int: 114 | """System timestamp for depth image in nanoseconds. Corresponds to Python's time.perf_counter_ns().""" 115 | if self._depth is None: 116 | self.depth 117 | return self._depth_system_timestamp_nsec 118 | 119 | @property 120 | def ir(self) -> Optional[np.ndarray]: 121 | """Device timestamp for IR image. Not equal host machine timestamp!. Like as equal depth image timestamp""" 122 | if self._ir is None: 123 | self._ir, self._ir_timestamp_usec, self._ir_system_timestamp_nsec = k4a_module.capture_get_ir_image( 124 | self._capture_handle, self.thread_safe 125 | ) 126 | return self._ir 127 | 128 | @property 129 | def ir_timestamp_usec(self) -> int: 130 | if self._ir is None: 131 | self.ir 132 | return self._ir_timestamp_usec 133 | 134 | @property 135 | def ir_system_timestamp_nsec(self) -> int: 136 | """System timestamp for IR image in nanoseconds. Corresponds to Python's time.perf_counter_ns().""" 137 | if self._ir is None: 138 | self.ir 139 | return self._ir_system_timestamp_nsec 140 | 141 | @property 142 | def transformed_depth(self) -> Optional[np.ndarray]: 143 | if self._transformed_depth is None and self.depth is not None: 144 | self._transformed_depth = depth_image_to_color_camera( 145 | self._depth, # type: ignore 146 | self._calibration, 147 | self.thread_safe, 148 | ) 149 | return self._transformed_depth 150 | 151 | @property 152 | def depth_point_cloud(self) -> Optional[np.ndarray]: 153 | if self._depth_point_cloud is None and self.depth is not None: 154 | self._depth_point_cloud = depth_image_to_point_cloud( 155 | self._depth, # type: ignore 156 | self._calibration, 157 | self.thread_safe, 158 | calibration_type_depth=True, 159 | ) 160 | return self._depth_point_cloud 161 | 162 | @property 163 | def transformed_depth_point_cloud(self) -> Optional[np.ndarray]: 164 | if self._transformed_depth_point_cloud is None and self.transformed_depth is not None: 165 | self._transformed_depth_point_cloud = depth_image_to_point_cloud( 166 | self.transformed_depth, 167 | self._calibration, 168 | self.thread_safe, 169 | calibration_type_depth=False, 170 | ) 171 | return self._transformed_depth_point_cloud 172 | 173 | @property 174 | def transformed_color(self) -> Optional[np.ndarray]: 175 | if self._transformed_color is None and self.depth is not None and self.color is not None: 176 | if self._color_format != ImageFormat.COLOR_BGRA32: 177 | raise RuntimeError( 178 | "color color_image must be of color_format K4A_IMAGE_FORMAT_COLOR_BGRA32 for " 179 | "transformation_color_image_to_depth_camera" 180 | ) 181 | self._transformed_color = color_image_to_depth_camera( 182 | self.color, self.depth, self._calibration, self.thread_safe 183 | ) 184 | return self._transformed_color 185 | 186 | @property 187 | def transformed_ir(self) -> Optional[np.ndarray]: 188 | if self._transformed_ir is None and self.ir is not None and self.depth is not None: 189 | result = depth_image_to_color_camera_custom(self.depth, self.ir, self._calibration, self.thread_safe) 190 | if result is None: 191 | return None 192 | else: 193 | self._transformed_ir, self._transformed_depth = result 194 | return self._transformed_ir 195 | -------------------------------------------------------------------------------- /pyk4a/config.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import Tuple 3 | 4 | 5 | # k4a_fps_t 6 | class FPS(IntEnum): 7 | FPS_5 = 0 8 | FPS_15 = 1 9 | FPS_30 = 2 10 | 11 | 12 | # k4a_image_format_t 13 | class ImageFormat(IntEnum): 14 | COLOR_MJPG = 0 15 | COLOR_NV12 = 1 16 | COLOR_YUY2 = 2 17 | COLOR_BGRA32 = 3 18 | DEPTH16 = 4 19 | IR16 = 5 20 | CUSTOM8 = 6 21 | CUSTOM16 = 7 22 | CUSTOM = 8 23 | 24 | 25 | # k4a_depth_mode_t 26 | class DepthMode(IntEnum): 27 | OFF = 0 28 | NFOV_2X2BINNED = 1 29 | NFOV_UNBINNED = 2 30 | WFOV_2X2BINNED = 3 31 | WFOV_UNBINNED = 4 32 | PASSIVE_IR = 5 33 | 34 | 35 | # k4a_color_resolution_t 36 | class ColorResolution(IntEnum): 37 | OFF = 0 38 | RES_720P = 1 39 | RES_1080P = 2 40 | RES_1440P = 3 41 | RES_1536P = 4 42 | RES_2160P = 5 43 | RES_3072P = 6 44 | 45 | 46 | # k4a_wired_sync_mode_t 47 | class WiredSyncMode(IntEnum): 48 | STANDALONE = 0 49 | MASTER = 1 50 | SUBORDINATE = 2 51 | 52 | 53 | # k4a_color_control_command_t 54 | class ColorControlCommand(IntEnum): 55 | EXPOSURE_TIME_ABSOLUTE = 0 56 | AUTO_EXPOSURE_PRIORITY = 1 # deprecated 57 | BRIGHTNESS = 2 58 | CONTRAST = 3 59 | SATURATION = 4 60 | SHARPNESS = 5 61 | WHITEBALANCE = 6 62 | BACKLIGHT_COMPENSATION = 7 63 | GAIN = 8 64 | POWERLINE_FREQUENCY = 9 65 | 66 | 67 | # k4a_color_control_mode_t 68 | class ColorControlMode(IntEnum): 69 | AUTO = 0 70 | MANUAL = 1 71 | 72 | 73 | class Config: 74 | def __init__( 75 | self, 76 | color_resolution: ColorResolution = ColorResolution.RES_720P, 77 | color_format: ImageFormat = ImageFormat.COLOR_BGRA32, 78 | depth_mode: DepthMode = DepthMode.NFOV_UNBINNED, 79 | camera_fps: FPS = FPS.FPS_30, 80 | synchronized_images_only: bool = True, 81 | depth_delay_off_color_usec: int = 0, 82 | wired_sync_mode: WiredSyncMode = WiredSyncMode.STANDALONE, 83 | subordinate_delay_off_master_usec: int = 0, 84 | disable_streaming_indicator: bool = False, 85 | ): 86 | self.color_resolution = color_resolution 87 | self.color_format = color_format 88 | self.depth_mode = depth_mode 89 | self.camera_fps = camera_fps 90 | self.synchronized_images_only = synchronized_images_only 91 | self.depth_delay_off_color_usec = depth_delay_off_color_usec 92 | self.wired_sync_mode = wired_sync_mode 93 | self.subordinate_delay_off_master_usec = subordinate_delay_off_master_usec 94 | self.disable_streaming_indicator = disable_streaming_indicator 95 | assert self.subordinate_delay_off_master_usec >= 0 96 | 97 | def unpack(self) -> Tuple[ImageFormat, ColorResolution, DepthMode, FPS, bool, int, WiredSyncMode, int, bool]: 98 | return ( 99 | self.color_format, 100 | self.color_resolution, 101 | self.depth_mode, 102 | self.camera_fps, 103 | self.synchronized_images_only, 104 | self.depth_delay_off_color_usec, 105 | self.wired_sync_mode, 106 | self.subordinate_delay_off_master_usec, 107 | self.disable_streaming_indicator, 108 | ) 109 | -------------------------------------------------------------------------------- /pyk4a/errors.py: -------------------------------------------------------------------------------- 1 | from .results import Result 2 | 3 | 4 | class K4AException(Exception): 5 | pass 6 | 7 | 8 | class K4ATimeoutException(K4AException): 9 | pass 10 | 11 | 12 | def _verify_error(res: int): 13 | """ 14 | Validate k4a_module result 15 | """ 16 | res = Result(res) 17 | if res == Result.Failed: 18 | raise K4AException() 19 | elif res == Result.Timeout: 20 | raise K4ATimeoutException() 21 | -------------------------------------------------------------------------------- /pyk4a/module.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | try: 5 | import k4a_module # type: ignore # noqa: F401 6 | except BaseException as e: 7 | if sys.platform == "win32": 8 | from .win32_utils import prepare_import_k4a_module 9 | 10 | added_dll_dir = prepare_import_k4a_module() 11 | try: 12 | import k4a_module # type: ignore # noqa: F401 13 | except BaseException: 14 | raise ImportError( 15 | ( 16 | "Cannot import k4a_module. " 17 | f"DLL directory added was {added_dll_dir}. " 18 | "You can provide a different path containing `k4a.dll`" 19 | "using the environment variable `K4A_DLL_DIR`. " 20 | "Also make sure pyk4a was properly built." 21 | ) 22 | ) from e 23 | else: 24 | raise ImportError( 25 | ( 26 | "Cannot import k4a_module. " 27 | "Make sure `libk4a.so` can be found. " 28 | "Add the directory to your `LD_LIBRARY_PATH` if required. " 29 | "Also make sure pyk4a is properly built." 30 | ) 31 | ) from e 32 | -------------------------------------------------------------------------------- /pyk4a/playback.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import warnings 3 | from enum import IntEnum 4 | from pathlib import Path 5 | from typing import Any, Optional, Tuple, Union 6 | 7 | 8 | if sys.version_info < (3, 8): 9 | from typing_extensions import TypedDict 10 | else: 11 | from typing import TypedDict 12 | 13 | from .calibration import Calibration 14 | from .capture import PyK4ACapture 15 | from .config import FPS, ColorResolution, DepthMode, ImageFormat, WiredSyncMode 16 | from .errors import K4AException, _verify_error 17 | from .module import k4a_module 18 | from .pyk4a import ImuSample 19 | from .results import Result, StreamResult 20 | 21 | 22 | class SeekOrigin(IntEnum): 23 | BEGIN = 0 24 | END = 1 25 | DEVICE_TIME = 2 26 | 27 | 28 | class Configuration(TypedDict): 29 | color_format: ImageFormat 30 | color_resolution: ColorResolution 31 | depth_mode: DepthMode 32 | camera_fps: FPS 33 | color_track_enabled: bool 34 | depth_track_enabled: bool 35 | ir_track_enabled: bool 36 | imu_track_enabled: bool 37 | depth_delay_off_color_usec: int 38 | wired_sync_mode: WiredSyncMode 39 | subordinate_delay_off_master_usec: int 40 | start_timestamp_offset_usec: int 41 | 42 | 43 | class PyK4APlayback: 44 | def __init__(self, path: Union[str, Path], thread_safe: bool = True): 45 | self._path: Path = Path(path) 46 | self.thread_safe = thread_safe 47 | self._handle: Optional[object] = None 48 | self._length: Optional[int] = None 49 | self._calibration_json: Optional[str] = None 50 | self._calibration: Optional[Calibration] = None 51 | self._configuration: Optional[Configuration] = None 52 | 53 | def __del__(self): 54 | if self._handle: 55 | self.close() 56 | 57 | def __enter__(self): 58 | self.open() 59 | return self 60 | 61 | def __exit__(self, exc_type, exc_value, traceback): 62 | self.close() 63 | 64 | @property 65 | def path(self) -> Path: 66 | """ 67 | Record file path 68 | """ 69 | return self._path 70 | 71 | @property 72 | def configuration(self) -> Configuration: 73 | self._validate_is_open() 74 | if self._configuration is None: 75 | res: int 76 | conf: Tuple[Any, ...] 77 | res, conf = k4a_module.playback_get_record_configuration(self._handle, self.thread_safe) 78 | _verify_error(res) 79 | self._configuration = Configuration( 80 | color_format=ImageFormat(conf[0]), 81 | color_resolution=ColorResolution(conf[1]), 82 | depth_mode=DepthMode(conf[2]), 83 | camera_fps=FPS(conf[3]), 84 | color_track_enabled=bool(conf[4]), 85 | depth_track_enabled=bool(conf[5]), 86 | ir_track_enabled=bool(conf[6]), 87 | imu_track_enabled=bool(conf[7]), 88 | depth_delay_off_color_usec=conf[8], 89 | wired_sync_mode=WiredSyncMode(conf[9]), 90 | subordinate_delay_off_master_usec=conf[10], 91 | start_timestamp_offset_usec=conf[11], 92 | ) 93 | return self._configuration 94 | 95 | @property 96 | def length(self) -> int: 97 | """ 98 | Record length in usec 99 | """ 100 | if self._length is None: 101 | self._validate_is_open() 102 | self._length = k4a_module.playback_get_recording_length_usec(self._handle, self.thread_safe) 103 | return self._length 104 | 105 | @property 106 | def calibration_raw(self) -> str: 107 | self._validate_is_open() 108 | res, raw = k4a_module.playback_get_raw_calibration(self._handle, self.thread_safe) 109 | _verify_error(res) 110 | return raw 111 | 112 | @calibration_raw.setter 113 | def calibration_raw(self, value: str): 114 | self._validate_is_open() 115 | self._calibration = Calibration.from_raw( 116 | value, self.calibration.depth_mode, self.calibration.color_resolution, self.thread_safe 117 | ) 118 | 119 | @property 120 | def calibration(self) -> Calibration: 121 | self._validate_is_open() 122 | if self._calibration is None: 123 | res, handle = k4a_module.playback_get_calibration(self._handle, self.thread_safe) 124 | _verify_error(res) 125 | self._calibration = Calibration( 126 | handle=handle, 127 | depth_mode=self.configuration["depth_mode"], 128 | color_resolution=self.configuration["color_resolution"], 129 | thread_safe=self.thread_safe, 130 | ) 131 | return self._calibration 132 | 133 | def open(self) -> None: 134 | """ 135 | Open record file 136 | """ 137 | if self._handle: 138 | raise K4AException("Playback already opened") 139 | result, handle = k4a_module.playback_open(str(self._path), self.thread_safe) 140 | if Result(result) != Result.Success: 141 | raise K4AException(f"Cannot open file {self._path}") 142 | 143 | self._handle = handle 144 | 145 | def close(self): 146 | """ 147 | Close record file 148 | """ 149 | self._validate_is_open() 150 | k4a_module.playback_close(self._handle, self.thread_safe) 151 | self._handle = None 152 | 153 | def seek(self, offset: int, origin: SeekOrigin = SeekOrigin.BEGIN) -> None: 154 | """ 155 | Seek playback pointer to specified offset 156 | """ 157 | self._validate_is_open() 158 | result = k4a_module.playback_seek_timestamp(self._handle, self.thread_safe, offset, int(origin)) 159 | self._verify_stream_error(result) 160 | 161 | def get_next_capture(self): 162 | self._validate_is_open() 163 | result, capture_handle = k4a_module.playback_get_next_capture(self._handle, self.thread_safe) 164 | self._verify_stream_error(result) 165 | return PyK4ACapture( 166 | calibration=self.calibration, 167 | capture_handle=capture_handle, 168 | color_format=self.configuration["color_format"], 169 | thread_safe=self.thread_safe, 170 | ) 171 | 172 | def get_previouse_capture(self): 173 | warnings.warn( 174 | "get_previouse_capture() deprecated, please use get_previous_capture() instead", DeprecationWarning 175 | ) 176 | return self.get_previous_capture() 177 | 178 | def get_previous_capture(self): 179 | self._validate_is_open() 180 | result, capture_handle = k4a_module.playback_get_previous_capture(self._handle, self.thread_safe) 181 | self._verify_stream_error(result) 182 | return PyK4ACapture( 183 | calibration=self.calibration, 184 | capture_handle=capture_handle, 185 | color_format=self.configuration["color_format"], 186 | thread_safe=self.thread_safe, 187 | ) 188 | 189 | def get_next_imu_sample(self) -> Optional["ImuSample"]: 190 | self._validate_is_open() 191 | result, imu_sample = k4a_module.playback_get_next_imu_sample(self._handle, self.thread_safe) 192 | self._verify_stream_error(result) 193 | return imu_sample 194 | 195 | def _validate_is_open(self): 196 | if not self._handle: 197 | raise K4AException("Playback not opened.") 198 | 199 | @staticmethod 200 | def _verify_stream_error(res: int): 201 | """ 202 | Validate k4a_module result(k4a_stream_result_t) 203 | """ 204 | result = StreamResult(res) 205 | if result == StreamResult.Failed: 206 | raise K4AException() 207 | elif result == StreamResult.EOF: 208 | raise EOFError() 209 | -------------------------------------------------------------------------------- /pyk4a/py.typed: -------------------------------------------------------------------------------- 1 | # marker -------------------------------------------------------------------------------- /pyk4a/pyk4a.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any, Optional, Tuple 3 | 4 | from .calibration import Calibration 5 | from .capture import PyK4ACapture 6 | from .config import ColorControlCommand, ColorControlMode, Config 7 | from .errors import K4AException, _verify_error 8 | from .module import k4a_module 9 | 10 | 11 | if sys.version_info < (3, 8): 12 | from typing_extensions import TypedDict 13 | else: 14 | from typing import TypedDict 15 | 16 | 17 | class PyK4A: 18 | TIMEOUT_WAIT_INFINITE = -1 19 | 20 | def __init__(self, config: Optional[Config] = None, device_id: int = 0, thread_safe: bool = True): 21 | self._device_id = device_id 22 | self._config: Config = config if (config is not None) else Config() 23 | self.thread_safe = thread_safe 24 | self._device_handle: Optional[object] = None 25 | self._calibration: Optional[Calibration] = None 26 | self.is_running = False 27 | 28 | def start(self): 29 | """ 30 | Open device if device not opened, then start cameras and IMU 31 | All-in-one function 32 | :return: 33 | """ 34 | if not self.opened: 35 | self.open() 36 | self._start_cameras() 37 | self._start_imu() 38 | self.is_running = True 39 | 40 | def stop(self): 41 | """ 42 | Stop cameras, IMU, ... and close device 43 | :return: 44 | """ 45 | self._stop_imu() 46 | self._stop_cameras() 47 | self._device_close() 48 | self.is_running = False 49 | 50 | def __del__(self): 51 | if self.is_running: 52 | self.stop() 53 | elif self.opened: 54 | self.close() 55 | 56 | @property 57 | def opened(self) -> bool: 58 | return self._device_handle is not None 59 | 60 | def open(self): 61 | """ 62 | Open device 63 | You must open device before querying any information 64 | """ 65 | if self.opened: 66 | raise K4AException("Device already opened") 67 | self._device_open() 68 | 69 | def close(self): 70 | self._validate_is_opened() 71 | self._device_close() 72 | 73 | def save_calibration_json(self, path: Any): 74 | with open(path, "w") as f: 75 | f.write(self.calibration_raw) 76 | 77 | def load_calibration_json(self, path: Any): 78 | with open(path, "r") as f: 79 | calibration = f.read() 80 | self.calibration_raw = calibration 81 | 82 | def _device_open(self): 83 | res, handle = k4a_module.device_open(self._device_id, self.thread_safe) 84 | _verify_error(res) 85 | self._device_handle = handle 86 | 87 | def _device_close(self): 88 | res = k4a_module.device_close(self._device_handle, self.thread_safe) 89 | _verify_error(res) 90 | self._device_handle = None 91 | 92 | def _start_cameras(self): 93 | res = k4a_module.device_start_cameras(self._device_handle, self.thread_safe, *self._config.unpack()) 94 | _verify_error(res) 95 | 96 | def _start_imu(self): 97 | res = k4a_module.device_start_imu(self._device_handle, self.thread_safe) 98 | _verify_error(res) 99 | 100 | def _stop_cameras(self): 101 | res = k4a_module.device_stop_cameras(self._device_handle, self.thread_safe) 102 | _verify_error(res) 103 | 104 | def _stop_imu(self): 105 | res = k4a_module.device_stop_imu(self._device_handle, self.thread_safe) 106 | _verify_error(res) 107 | 108 | def get_capture( 109 | self, 110 | timeout=TIMEOUT_WAIT_INFINITE, 111 | ) -> "PyK4ACapture": 112 | """ 113 | Fetch a capture from the device and return a PyK4ACapture object. Images are 114 | lazily fetched. 115 | 116 | Arguments: 117 | :param timeout: Timeout in ms. Default is infinite. 118 | 119 | Returns: 120 | :return capture containing requested images and infos if they are available 121 | in the current capture. There are no guarantees that the returned 122 | object will contain all the requested images. 123 | 124 | If using any ImageFormat other than ImageFormat.COLOR_BGRA32, the color color_image must be 125 | decoded. See example/color_formats.py 126 | """ 127 | self._validate_is_opened() 128 | res, capture_capsule = k4a_module.device_get_capture(self._device_handle, self.thread_safe, timeout) 129 | _verify_error(res) 130 | 131 | capture = PyK4ACapture( 132 | calibration=self.calibration, 133 | capture_handle=capture_capsule, 134 | color_format=self._config.color_format, 135 | thread_safe=self.thread_safe, 136 | ) 137 | return capture 138 | 139 | def get_imu_sample(self, timeout: int = TIMEOUT_WAIT_INFINITE) -> Optional["ImuSample"]: 140 | self._validate_is_opened() 141 | res, imu_sample = k4a_module.device_get_imu_sample(self._device_handle, self.thread_safe, timeout) 142 | _verify_error(res) 143 | return imu_sample 144 | 145 | @property 146 | def serial(self) -> str: 147 | self._validate_is_opened() 148 | ret = k4a_module.device_get_serialnum(self._device_handle, self.thread_safe) 149 | if ret == "": 150 | raise K4AException("Cannot read serial") 151 | return ret 152 | 153 | @property 154 | def calibration_raw(self) -> str: 155 | self._validate_is_opened() 156 | raw = k4a_module.device_get_raw_calibration(self._device_handle, self.thread_safe) 157 | return raw 158 | 159 | @calibration_raw.setter 160 | def calibration_raw(self, value: str): 161 | self._validate_is_opened() 162 | self._calibration = Calibration.from_raw( 163 | value, self._config.depth_mode, self._config.color_resolution, self.thread_safe 164 | ) 165 | 166 | @property 167 | def sync_jack_status(self) -> Tuple[bool, bool]: 168 | self._validate_is_opened() 169 | res, jack_in, jack_out = k4a_module.device_get_sync_jack(self._device_handle, self.thread_safe) 170 | _verify_error(res) 171 | return jack_in == 1, jack_out == 1 172 | 173 | def _get_color_control(self, cmd: ColorControlCommand) -> Tuple[int, ColorControlMode]: 174 | self._validate_is_opened() 175 | res, mode, value = k4a_module.device_get_color_control(self._device_handle, self.thread_safe, cmd) 176 | _verify_error(res) 177 | return value, ColorControlMode(mode) 178 | 179 | def _set_color_control(self, cmd: ColorControlCommand, value: int, mode=ColorControlMode.MANUAL): 180 | self._validate_is_opened() 181 | res = k4a_module.device_set_color_control(self._device_handle, self.thread_safe, cmd, mode, value) 182 | _verify_error(res) 183 | 184 | @property 185 | def brightness(self) -> int: 186 | return self._get_color_control(ColorControlCommand.BRIGHTNESS)[0] 187 | 188 | @brightness.setter 189 | def brightness(self, value: int): 190 | self._set_color_control(ColorControlCommand.BRIGHTNESS, value) 191 | 192 | @property 193 | def contrast(self) -> int: 194 | return self._get_color_control(ColorControlCommand.CONTRAST)[0] 195 | 196 | @contrast.setter 197 | def contrast(self, value: int): 198 | self._set_color_control(ColorControlCommand.CONTRAST, value) 199 | 200 | @property 201 | def saturation(self) -> int: 202 | return self._get_color_control(ColorControlCommand.SATURATION)[0] 203 | 204 | @saturation.setter 205 | def saturation(self, value: int): 206 | self._set_color_control(ColorControlCommand.SATURATION, value) 207 | 208 | @property 209 | def sharpness(self) -> int: 210 | return self._get_color_control(ColorControlCommand.SHARPNESS)[0] 211 | 212 | @sharpness.setter 213 | def sharpness(self, value: int): 214 | self._set_color_control(ColorControlCommand.SHARPNESS, value) 215 | 216 | @property 217 | def backlight_compensation(self) -> int: 218 | return self._get_color_control(ColorControlCommand.BACKLIGHT_COMPENSATION)[0] 219 | 220 | @backlight_compensation.setter 221 | def backlight_compensation(self, value: int): 222 | self._set_color_control(ColorControlCommand.BACKLIGHT_COMPENSATION, value) 223 | 224 | @property 225 | def gain(self) -> int: 226 | return self._get_color_control(ColorControlCommand.GAIN)[0] 227 | 228 | @gain.setter 229 | def gain(self, value: int): 230 | self._set_color_control(ColorControlCommand.GAIN, value) 231 | 232 | @property 233 | def powerline_frequency(self) -> int: 234 | return self._get_color_control(ColorControlCommand.POWERLINE_FREQUENCY)[0] 235 | 236 | @powerline_frequency.setter 237 | def powerline_frequency(self, value: int): 238 | self._set_color_control(ColorControlCommand.POWERLINE_FREQUENCY, value) 239 | 240 | @property 241 | def exposure(self) -> int: 242 | # sets mode to manual 243 | return self._get_color_control(ColorControlCommand.EXPOSURE_TIME_ABSOLUTE)[0] 244 | 245 | @exposure.setter 246 | def exposure(self, value: int): 247 | self._set_color_control(ColorControlCommand.EXPOSURE_TIME_ABSOLUTE, value) 248 | 249 | @property 250 | def exposure_mode_auto(self) -> bool: 251 | return self._get_color_control(ColorControlCommand.EXPOSURE_TIME_ABSOLUTE)[1] == ColorControlMode.AUTO 252 | 253 | @exposure_mode_auto.setter 254 | def exposure_mode_auto(self, mode_auto: bool, value: int = 2500): 255 | mode = ColorControlMode.AUTO if mode_auto else ColorControlMode.MANUAL 256 | self._set_color_control(ColorControlCommand.EXPOSURE_TIME_ABSOLUTE, value=value, mode=mode) 257 | 258 | @property 259 | def whitebalance(self) -> int: 260 | # sets mode to manual 261 | return self._get_color_control(ColorControlCommand.WHITEBALANCE)[0] 262 | 263 | @whitebalance.setter 264 | def whitebalance(self, value: int): 265 | self._set_color_control(ColorControlCommand.WHITEBALANCE, value) 266 | 267 | @property 268 | def whitebalance_mode_auto(self) -> bool: 269 | return self._get_color_control(ColorControlCommand.WHITEBALANCE)[1] == ColorControlMode.AUTO 270 | 271 | @whitebalance_mode_auto.setter 272 | def whitebalance_mode_auto(self, mode_auto: bool, value: int = 2500): 273 | mode = ColorControlMode.AUTO if mode_auto else ColorControlMode.MANUAL 274 | self._set_color_control(ColorControlCommand.WHITEBALANCE, value=value, mode=mode) 275 | 276 | def _get_color_control_capabilities(self, cmd: ColorControlCommand) -> Optional["ColorControlCapabilities"]: 277 | self._validate_is_opened() 278 | res, capabilities = k4a_module.device_get_color_control_capabilities(self._device_handle, self.thread_safe, cmd) 279 | _verify_error(res) 280 | return capabilities 281 | 282 | def reset_color_control_to_default(self): 283 | for cmd in ColorControlCommand: 284 | capability = self._get_color_control_capabilities(cmd) 285 | self._set_color_control(cmd, capability["default_value"], capability["default_mode"]) 286 | 287 | @property 288 | def calibration(self) -> Calibration: 289 | self._validate_is_opened() 290 | if not self._calibration: 291 | res, calibration_handle = k4a_module.device_get_calibration( 292 | self._device_handle, self.thread_safe, self._config.depth_mode, self._config.color_resolution 293 | ) 294 | _verify_error(res) 295 | self._calibration = Calibration( 296 | handle=calibration_handle, 297 | depth_mode=self._config.depth_mode, 298 | color_resolution=self._config.color_resolution, 299 | thread_safe=self.thread_safe, 300 | ) 301 | return self._calibration 302 | 303 | def _validate_is_opened(self): 304 | if not self.opened: 305 | raise K4AException("Device is not opened") 306 | 307 | 308 | class ImuSample(TypedDict): 309 | temperature: float 310 | acc_sample: Tuple[float, float, float] 311 | acc_timestamp: int 312 | gyro_sample: Tuple[float, float, float] 313 | gyro_timestamp: int 314 | 315 | 316 | class ColorControlCapabilities(TypedDict): 317 | color_control_command: ColorControlCommand 318 | supports_auto: bool 319 | min_value: int 320 | max_value: int 321 | step_value: int 322 | default_value: int 323 | default_mode: ColorControlMode 324 | -------------------------------------------------------------------------------- /pyk4a/record.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional, Union 3 | 4 | from .capture import PyK4ACapture 5 | from .config import Config 6 | from .errors import K4AException 7 | from .module import k4a_module 8 | from .pyk4a import PyK4A 9 | from .results import Result 10 | 11 | 12 | class PyK4ARecord: 13 | def __init__( 14 | self, path: Union[str, Path], config: Config, device: Optional[PyK4A] = None, thread_safe: bool = True 15 | ): 16 | self._path: Path = Path(path) 17 | self.thread_safe = thread_safe 18 | self._device: Optional[PyK4A] = device 19 | self._config: Config = config 20 | self._handle: Optional[object] = None 21 | self._header_written: bool = False 22 | self._captures_count: int = 0 23 | 24 | def __del__(self): 25 | if self.created: 26 | self.close() 27 | 28 | def create(self) -> None: 29 | """Create record file""" 30 | if self.created: 31 | raise K4AException(f"Record already created {self._path}") 32 | device_handle = self._device._device_handle if self._device else None 33 | result, handle = k4a_module.record_create( 34 | device_handle, str(self._path), self.thread_safe, *self._config.unpack() 35 | ) 36 | if result != Result.Success: 37 | raise K4AException(f"Cannot create record {self._path}") 38 | self._handle = handle 39 | 40 | def close(self): 41 | """Close record""" 42 | self._validate_is_created() 43 | k4a_module.record_close(self._handle, self.thread_safe) 44 | self._handle = None 45 | 46 | def write_header(self): 47 | """Write MKV header""" 48 | self._validate_is_created() 49 | if self.header_written: 50 | raise K4AException(f"Header already written {self._path}") 51 | result: Result = k4a_module.record_write_header(self._handle, self.thread_safe) 52 | if result != Result.Success: 53 | raise K4AException(f"Cannot write record header {self._path}") 54 | self._header_written = True 55 | 56 | def write_capture(self, capture: PyK4ACapture): 57 | """Write capture to file (send to queue)""" 58 | self._validate_is_created() 59 | if not self.header_written: 60 | self.write_header() 61 | result: Result = k4a_module.record_write_capture(self._handle, capture._capture_handle, self.thread_safe) 62 | if result != Result.Success: 63 | raise K4AException(f"Cannot write capture {self._path}") 64 | self._captures_count += 1 65 | 66 | def flush(self): 67 | """Flush queue""" 68 | self._validate_is_created() 69 | result: Result = k4a_module.record_flush(self._handle, self.thread_safe) 70 | if result != Result.Success: 71 | raise K4AException(f"Cannot flush data {self._path}") 72 | 73 | @property 74 | def created(self) -> bool: 75 | return self._handle is not None 76 | 77 | @property 78 | def header_written(self) -> bool: 79 | return self._header_written 80 | 81 | @property 82 | def captures_count(self) -> int: 83 | return self._captures_count 84 | 85 | @property 86 | def path(self) -> Path: 87 | return self._path 88 | 89 | def _validate_is_created(self): 90 | if not self.created: 91 | raise K4AException("Record not created.") 92 | -------------------------------------------------------------------------------- /pyk4a/results.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | # k4a_wait_result_t 5 | class Result(IntEnum): 6 | Success = 0 7 | Failed = 1 8 | Timeout = 2 9 | 10 | 11 | # k4a_buffer_result_t 12 | class BufferResult(IntEnum): 13 | Success = 0 14 | Failed = 1 15 | TooSmall = 2 16 | 17 | 18 | # k4a_stream_result_t 19 | class StreamResult(IntEnum): 20 | Success = 0 21 | Failed = 1 22 | EOF = 2 23 | -------------------------------------------------------------------------------- /pyk4a/transformation.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import numpy as np 4 | 5 | from .calibration import Calibration 6 | from .module import k4a_module 7 | 8 | 9 | def depth_image_to_color_camera(depth: np.ndarray, calibration: Calibration, thread_safe: bool) -> Optional[np.ndarray]: 10 | """ 11 | Transform depth color_image to color color_image space 12 | Return empty result if transformation failed 13 | """ 14 | return k4a_module.transformation_depth_image_to_color_camera( 15 | calibration.transformation_handle, 16 | thread_safe, 17 | depth, 18 | calibration.color_resolution, 19 | ) 20 | 21 | 22 | def depth_image_to_color_camera_custom( 23 | depth: np.ndarray, 24 | custom: np.ndarray, 25 | calibration: Calibration, 26 | thread_safe: bool, 27 | interp_nearest: bool = True, 28 | ) -> Optional[np.ndarray]: 29 | """ 30 | Transforms depth image and custom image to color_image space 31 | Return empty result if transformation failed 32 | """ 33 | return k4a_module.transformation_depth_image_to_color_camera_custom( 34 | calibration.transformation_handle, 35 | thread_safe, 36 | depth, 37 | custom, 38 | calibration.color_resolution, 39 | interp_nearest, 40 | ) 41 | 42 | 43 | def depth_image_to_point_cloud( 44 | depth: np.ndarray, calibration: Calibration, thread_safe: bool, calibration_type_depth=True 45 | ) -> Optional[np.ndarray]: 46 | """ 47 | Transform depth color_image to point cloud 48 | Return empty result if transformation failed 49 | """ 50 | return k4a_module.transformation_depth_image_to_point_cloud( 51 | calibration.transformation_handle, 52 | thread_safe, 53 | depth, 54 | calibration_type_depth, 55 | ) 56 | 57 | 58 | def color_image_to_depth_camera( 59 | color: np.ndarray, depth: np.ndarray, calibration: Calibration, thread_safe: bool 60 | ) -> Optional[np.ndarray]: 61 | """ 62 | Transform color color_image to depth color_image space 63 | Return empty result if transformation failed 64 | """ 65 | return k4a_module.transformation_color_image_to_depth_camera( 66 | calibration.transformation_handle, thread_safe, depth, color 67 | ) 68 | -------------------------------------------------------------------------------- /pyk4a/win32_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | 7 | if sys.platform != "win32": 8 | raise ImportError(f"This file should only be used on a win32 OS: {__file__}") 9 | 10 | 11 | def add_dll_directory(path: Path): 12 | if hasattr(os, "add_dll_directory"): 13 | # only available for python 3.8+ on win32 14 | os.add_dll_directory(str(path)) # type: ignore 15 | else: 16 | from ctypes import c_wchar_p, windll # type: ignore 17 | from ctypes.wintypes import DWORD 18 | 19 | AddDllDirectory = windll.kernel32.AddDllDirectory 20 | AddDllDirectory.restype = DWORD 21 | AddDllDirectory.argtypes = [c_wchar_p] 22 | AddDllDirectory(str(path)) 23 | 24 | 25 | def find_k4a_dll_dir() -> Optional[Path]: 26 | # get program_files 27 | for k in ("ProgramFiles", "PROGRAMFILES"): 28 | if k in os.environ: 29 | program_files = Path(os.environ[k]) 30 | break 31 | else: 32 | program_files = Path("C:\\Program Files\\") 33 | # search through program_files 34 | arch = os.getenv("PROCESSOR_ARCHITECTURE", "amd64") 35 | for dir in sorted(program_files.glob("Azure Kinect SDK v*"), reverse=True): 36 | candidate = dir / "sdk" / "windows-desktop" / arch / "release" / "bin" 37 | dll = candidate / "k4a.dll" 38 | if dll.exists(): 39 | dll_dir = candidate 40 | return dll_dir 41 | 42 | 43 | def prepare_import_k4a_module() -> Optional[Path]: 44 | dll_dir: Optional[Path] 45 | if "K4A_DLL_DIR" in os.environ: 46 | dll_dir = Path(os.environ["K4A_DLL_DIR"]) 47 | else: 48 | dll_dir = find_k4a_dll_dir() 49 | 50 | if dll_dir is not None: 51 | add_dll_directory(dll_dir) 52 | return dll_dir 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "numpy"] 3 | 4 | [tool.black] 5 | line-length = 120 6 | 7 | [tool.pytest.ini_options] 8 | markers = [ 9 | "device: Tests require connected real device", 10 | "opengl: Tests require opengl for GPU accelerated depth engine software" 11 | ] 12 | addopts = "--cov=pyk4a --cov-report=xml --verbose" 13 | 14 | [[tool.mypy.overrides]] 15 | module = [ 16 | "cv2", 17 | "matplotlib", 18 | "mpl_toolkits", 19 | "k4a_module" 20 | ] 21 | ignore_missing_imports = true -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black~=23.1.0 2 | flake8~=5.0.4 3 | isort~=5.11.5 4 | flake8-isort~=6.0.0 5 | mypy==0.991 6 | mypy-extensions~=0.4.3 7 | pytest~=7.2.1 8 | pytest-cov~=4.0.0 9 | dataclasses==0.6; python_version<"3.7" 10 | opencv-python-headless~=4.7.0.68 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyk4a 3 | version = 1.5.0 4 | description-file = README.md 5 | description = Python wrapper over Azure Kinect SDK 6 | long_description = file: README.md 7 | long_description_content_type = text/markdown 8 | url = https://github.com/etiennedub/pyk4a/ 9 | classifiers = 10 | Operating System :: OS Independent 11 | License :: OSI Approved :: MIT License 12 | Development Status :: 4 - Beta 13 | Topic :: Multimedia :: Video :: Capture 14 | Programming Language :: Python :: 3 15 | Programming Language :: C 16 | 17 | [options] 18 | packages = pyk4a 19 | python_requires = >= 3.4 20 | install_requires = 21 | numpy 22 | 23 | [options.package_data] 24 | pyk4a = py.typed 25 | 26 | 27 | [flake8] 28 | max-line-length = 120 29 | extend-ignore = 30 | # See https://github.com/PyCQA/pycodestyle/issues/373 31 | E203, 32 | E501, 33 | 34 | [isort] 35 | line_length=120 36 | include_trailing_comma=True 37 | multi_line_output=3 38 | force_grid_wrap=0 39 | combine_as_imports=True 40 | lines_after_imports=2 41 | known_first_party=pyk4a,k4a_module,helpers 42 | 43 | [mypy-numpy.*] 44 | ignore_missing_imports = True 45 | 46 | [mypy-cv2.*] 47 | ignore_missing_imports = True 48 | 49 | [mypy-matplotlib.*] 50 | ignore_missing_imports = True 51 | 52 | [mypy-mpl_toolkits] 53 | ignore_missing_imports = True 54 | 55 | [mypy-k4a_module] 56 | ignore_missing_imports = True 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, Extension 4 | from pathlib import Path 5 | import sys 6 | from setuptools.command.build_ext import build_ext 7 | from typing import Tuple, Optional 8 | 9 | if sys.version_info[0] == 2: 10 | sys.exit("Python 2 is not supported.") 11 | 12 | # Enables --editable install with --user 13 | # https://github.com/pypa/pip/issues/7953 14 | import site 15 | 16 | site.ENABLE_USER_SITE = "--user" in sys.argv[1:] 17 | 18 | 19 | # Bypass import numpy before running install_requires 20 | # https://stackoverflow.com/questions/54117786/add-numpy-get-include-argument-to-setuptools-without-preinstalled-numpy 21 | class get_numpy_include: 22 | def __str__(self): 23 | import numpy 24 | 25 | return numpy.get_include() 26 | 27 | 28 | def detect_win32_sdk_include_and_library_dirs() -> Optional[Tuple[str, str]]: 29 | # get program_files path 30 | for k in ("ProgramFiles", "PROGRAMFILES"): 31 | if k in os.environ: 32 | program_files = Path(os.environ[k]) 33 | break 34 | else: 35 | program_files = Path("C:\\Program Files\\") 36 | # search through program_files 37 | arch = os.getenv("PROCESSOR_ARCHITECTURE", "amd64") 38 | for dir in sorted(program_files.glob("Azure Kinect SDK v*"), reverse=True): 39 | include = dir / "sdk" / "include" 40 | lib = dir / "sdk" / "windows-desktop" / arch / "release" / "lib" 41 | if include.exists() and lib.exists(): 42 | return str(include), str(lib) 43 | return None 44 | 45 | 46 | def detect_and_insert_sdk_include_and_library_dirs(include_dirs, library_dirs) -> None: 47 | if sys.platform == "win32": 48 | r = detect_win32_sdk_include_and_library_dirs() 49 | else: 50 | # Only implemented for windows 51 | r = None 52 | 53 | if r is None: 54 | print("Automatic kinect SDK detection did not yield any results.") 55 | else: 56 | include_dir, library_dir = r 57 | print(f"Automatically detected kinect SDK. Adding include dir: {include_dir} and library dir {library_dir}.") 58 | include_dirs.insert(0, include_dir) 59 | library_dirs.insert(0, library_dir) 60 | 61 | 62 | include_dirs = [get_numpy_include()] 63 | library_dirs = [] 64 | detect_and_insert_sdk_include_and_library_dirs(include_dirs, library_dirs) 65 | module = Extension( 66 | "k4a_module", 67 | sources=["pyk4a/pyk4a.cpp"], 68 | libraries=["k4a", "k4arecord"], 69 | include_dirs=include_dirs, 70 | library_dirs=library_dirs, 71 | ) 72 | 73 | setup( 74 | ext_modules=[module], 75 | ) 76 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennedub/pyk4a/8c153164b484037bd7afc02db0c2be89e7bb8b38/tests/__init__.py -------------------------------------------------------------------------------- /tests/assets/calibration.json: -------------------------------------------------------------------------------- 1 | { 2 | "CalibrationInformation": { 3 | "Cameras": [ 4 | { 5 | "Intrinsics": { 6 | "ModelParameterCount": 14, 7 | "ModelParameters": [ 8 | 0.51296979188919067, 9 | 0.51692354679107666, 10 | 0.4927808940410614, 11 | 0.49290284514427185, 12 | 0.54793977737426758, 13 | -0.020971370860934258, 14 | -0.0026522316038608551, 15 | 0.88967907428741455, 16 | 0.086130209267139435, 17 | -0.013910384848713875, 18 | 0, 19 | 0, 20 | -7.2104157879948616E-5, 21 | 2.0557534298859537E-5 22 | ], 23 | "ModelType": "CALIBRATION_LensDistortionModelBrownConrady" 24 | }, 25 | "Location": "CALIBRATION_CameraLocationD0", 26 | "Purpose": "CALIBRATION_CameraPurposeDepth", 27 | "MetricRadius": 1.7399997711181641, 28 | "Rt": { 29 | "Rotation": [ 30 | 1, 31 | 0, 32 | 0, 33 | 0, 34 | 1, 35 | 0, 36 | 0, 37 | 0, 38 | 1 39 | ], 40 | "Translation": [ 41 | 0, 42 | 0, 43 | 0 44 | ] 45 | }, 46 | "SensorHeight": 1024, 47 | "SensorWidth": 1024, 48 | "Shutter": "CALIBRATION_ShutterTypeUndefined", 49 | "ThermalAdjustmentParams": { 50 | "Params": [ 51 | 0, 52 | 0, 53 | 0, 54 | 0, 55 | 0, 56 | 0, 57 | 0, 58 | 0, 59 | 0, 60 | 0, 61 | 0, 62 | 0 63 | ] 64 | } 65 | }, 66 | { 67 | "Intrinsics": { 68 | "ModelParameterCount": 14, 69 | "ModelParameters": [ 70 | 0.49984714388847351, 71 | 0.50661760568618774, 72 | 0.47796511650085449, 73 | 0.63742393255233765, 74 | 0.33012130856513977, 75 | -2.4893965721130371, 76 | 1.453670859336853, 77 | 0.20891207456588745, 78 | -2.302915096282959, 79 | 1.3751275539398193, 80 | 0, 81 | 0, 82 | -0.00012944525224156678, 83 | -0.00016178244550246745 84 | ], 85 | "ModelType": "CALIBRATION_LensDistortionModelBrownConrady" 86 | }, 87 | "Location": "CALIBRATION_CameraLocationPV0", 88 | "Purpose": "CALIBRATION_CameraPurposePhotoVideo", 89 | "MetricRadius": 0, 90 | "Rt": { 91 | "Rotation": [ 92 | 0.99999922513961792, 93 | 0.0011817576596513391, 94 | -0.00044814043212682009, 95 | -0.0011387512786313891, 96 | 0.99628061056137085, 97 | 0.0861603245139122, 98 | 0.0005482942215166986, 99 | -0.086159750819206238, 100 | 0.9962812066078186 101 | ], 102 | "Translation": [ 103 | -0.032083429396152496, 104 | -0.0022053730208426714, 105 | 0.0038836926687508821 106 | ] 107 | }, 108 | "SensorHeight": 3072, 109 | "SensorWidth": 4096, 110 | "Shutter": "CALIBRATION_ShutterTypeUndefined", 111 | "ThermalAdjustmentParams": { 112 | "Params": [ 113 | 0, 114 | 0, 115 | 0, 116 | 0, 117 | 0, 118 | 0, 119 | 0, 120 | 0, 121 | 0, 122 | 0, 123 | 0, 124 | 0 125 | ] 126 | } 127 | } 128 | ], 129 | "InertialSensors": [ 130 | { 131 | "BiasTemperatureModel": [ 132 | -0.00038840249180793762, 133 | 0, 134 | 0, 135 | 0, 136 | 0.0066333282738924026, 137 | 0, 138 | 0, 139 | 0, 140 | 0.0011737601598724723, 141 | 0, 142 | 0, 143 | 0 144 | ], 145 | "BiasUncertainty": [ 146 | 9.9999997473787516E-5, 147 | 9.9999997473787516E-5, 148 | 9.9999997473787516E-5 149 | ], 150 | "Id": "CALIBRATION_InertialSensorId_LSM6DSM", 151 | "MixingMatrixTemperatureModel": [ 152 | 0.99519604444503784, 153 | 0, 154 | 0, 155 | 0, 156 | 0.0019702909048646688, 157 | 0, 158 | 0, 159 | 0, 160 | -0.00029000110225751996, 161 | 0, 162 | 0, 163 | 0, 164 | 0.0019510588608682156, 165 | 0, 166 | 0, 167 | 0, 168 | 1.00501549243927, 169 | 0, 170 | 0, 171 | 0, 172 | 0.0030893723014742136, 173 | 0, 174 | 0, 175 | 0, 176 | -0.00028924690559506416, 177 | 0, 178 | 0, 179 | 0, 180 | 0.0031117112375795841, 181 | 0, 182 | 0, 183 | 0, 184 | 0.99779671430587769, 185 | 0, 186 | 0, 187 | 0 188 | ], 189 | "ModelTypeMask": 16, 190 | "Noise": [ 191 | 0.00095000001601874828, 192 | 0.00095000001601874828, 193 | 0.00095000001601874828, 194 | 0, 195 | 0, 196 | 0 197 | ], 198 | "Rt": { 199 | "Rotation": [ 200 | 0.0018387800082564354, 201 | 0.11238107085227966, 202 | -0.993663489818573, 203 | -0.99999493360519409, 204 | -0.002381625585258007, 205 | -0.0021198526956140995, 206 | -0.0026047658175230026, 207 | 0.99366235733032227, 208 | 0.11237611621618271 209 | ], 210 | "Translation": [ 211 | 0, 212 | 0, 213 | 0 214 | ] 215 | }, 216 | "SecondOrderScaling": [ 217 | 0, 218 | 0, 219 | 0, 220 | 0, 221 | 0, 222 | 0, 223 | 0, 224 | 0, 225 | 0 226 | ], 227 | "SensorType": "CALIBRATION_InertialSensorType_Gyro", 228 | "TemperatureBounds": [ 229 | 5, 230 | 60 231 | ], 232 | "TemperatureC": 0 233 | }, 234 | { 235 | "BiasTemperatureModel": [ 236 | 0.1411108672618866, 237 | 0, 238 | 0, 239 | 0, 240 | 0.0053673363290727139, 241 | 0, 242 | 0, 243 | 0, 244 | -0.42933177947998047, 245 | 0, 246 | 0, 247 | 0 248 | ], 249 | "BiasUncertainty": [ 250 | 0.0099999997764825821, 251 | 0.0099999997764825821, 252 | 0.0099999997764825821 253 | ], 254 | "Id": "CALIBRATION_InertialSensorId_LSM6DSM", 255 | "MixingMatrixTemperatureModel": [ 256 | 0.99946749210357666, 257 | 0, 258 | 0, 259 | 0, 260 | 3.0261040592449717E-5, 261 | 0, 262 | 0, 263 | 0, 264 | -0.00310571794398129, 265 | 0, 266 | 0, 267 | 0, 268 | 3.0574858101317659E-5, 269 | 0, 270 | 0, 271 | 0, 272 | 0.98919963836669922, 273 | 0, 274 | 0, 275 | 0, 276 | 0.00054294260917231441, 277 | 0, 278 | 0, 279 | 0, 280 | -0.0031195830088108778, 281 | 0, 282 | 0, 283 | 0, 284 | 0.00053976889466866851, 285 | 0, 286 | 0, 287 | 0, 288 | 0.995025634765625, 289 | 0, 290 | 0, 291 | 0 292 | ], 293 | "ModelTypeMask": 56, 294 | "Noise": [ 295 | 0.010700000450015068, 296 | 0.010700000450015068, 297 | 0.010700000450015068, 298 | 0, 299 | 0, 300 | 0 301 | ], 302 | "Rt": { 303 | "Rotation": [ 304 | 0.0019369830843061209, 305 | 0.1084766760468483, 306 | -0.994097113609314, 307 | -0.99999403953552246, 308 | -0.0026388836558908224, 309 | -0.0022364303003996611, 310 | -0.0028659072704613209, 311 | 0.994095504283905, 312 | 0.10847091674804688 313 | ], 314 | "Translation": [ 315 | -0.0509832426905632, 316 | 0.0034723200369626284, 317 | 0.0013277813559398055 318 | ] 319 | }, 320 | "SecondOrderScaling": [ 321 | 0, 322 | 0, 323 | 0, 324 | 0, 325 | 0, 326 | 0, 327 | 0, 328 | 0, 329 | 0 330 | ], 331 | "SensorType": "CALIBRATION_InertialSensorType_Accelerometer", 332 | "TemperatureBounds": [ 333 | 5, 334 | 60 335 | ], 336 | "TemperatureC": 0 337 | } 338 | ], 339 | "Metadata": { 340 | "SerialId": "001514394512", 341 | "FactoryCalDate": "11/9/2019 10:32:12 AM GMT", 342 | "Version": { 343 | "Major": 1, 344 | "Minor": 2 345 | }, 346 | "DeviceName": "AzureKinect-PV", 347 | "Notes": "PV0_max_radius_invalid" 348 | } 349 | } 350 | } -------------------------------------------------------------------------------- /tests/assets/recording.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennedub/pyk4a/8c153164b484037bd7afc02db0c2be89e7bb8b38/tests/assets/recording.mkv -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from pyk4a import PyK4APlayback 6 | 7 | 8 | pytest_plugins = [ 9 | "tests.plugins.calibration", 10 | "tests.plugins.capture", 11 | "tests.plugins.device", 12 | "tests.plugins.playback", 13 | ] 14 | 15 | 16 | @pytest.fixture() 17 | def recording_path() -> Path: 18 | return Path(__file__).parent / "assets" / "recording.mkv" 19 | 20 | 21 | @pytest.fixture() 22 | def playback(recording_path: Path) -> PyK4APlayback: 23 | return PyK4APlayback(path=recording_path) 24 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennedub/pyk4a/8c153164b484037bd7afc02db0c2be89e7bb8b38/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/functional/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Iterator 2 | 3 | import pytest 4 | 5 | from pyk4a import K4AException, PyK4A 6 | 7 | 8 | DEVICE_ID = 0 9 | DEVICE_ID_NOT_EXISTS = 99 10 | 11 | 12 | @pytest.fixture() 13 | def device_id() -> int: 14 | return DEVICE_ID 15 | 16 | 17 | @pytest.fixture() 18 | def device_id_not_exists() -> int: 19 | return DEVICE_ID_NOT_EXISTS 20 | 21 | 22 | @pytest.fixture() 23 | def device(device_id: int) -> Iterator[PyK4A]: 24 | device = PyK4A(device_id=device_id) 25 | yield device 26 | 27 | if device._device_handle: 28 | # close all 29 | try: 30 | device._stop_imu() 31 | except K4AException: 32 | pass 33 | try: 34 | device._stop_cameras() 35 | except K4AException: 36 | pass 37 | try: 38 | device.close() 39 | except K4AException: 40 | pass 41 | 42 | 43 | @pytest.fixture() 44 | def device_not_exists(device_id_not_exists: int) -> Iterator[PyK4A]: 45 | device = PyK4A(device_id=device_id_not_exists) 46 | yield device 47 | -------------------------------------------------------------------------------- /tests/functional/test_calibration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyk4a import Calibration, ColorResolution, DepthMode, K4AException 4 | 5 | 6 | @pytest.fixture() 7 | def calibration(calibration_raw) -> Calibration: 8 | return Calibration.from_raw( 9 | calibration_raw, depth_mode=DepthMode.NFOV_UNBINNED, color_resolution=ColorResolution.RES_720P 10 | ) 11 | 12 | 13 | class TestCalibration: 14 | @staticmethod 15 | def test_from_raw_incorrect_data(calibration_raw): 16 | with pytest.raises(K4AException): 17 | Calibration.from_raw( 18 | "none-calibration-json-string", depth_mode=DepthMode.NFOV_UNBINNED, color_resolution=ColorResolution.OFF 19 | ) 20 | 21 | @staticmethod 22 | def test_from_raw(calibration_raw): 23 | calibration = Calibration.from_raw( 24 | calibration_raw, depth_mode=DepthMode.NFOV_UNBINNED, color_resolution=ColorResolution.OFF 25 | ) 26 | assert calibration 27 | 28 | @staticmethod 29 | @pytest.mark.opengl 30 | def test_creating_transfromation_handle(calibration: Calibration): 31 | transformation = calibration.transformation_handle 32 | assert transformation is not None 33 | -------------------------------------------------------------------------------- /tests/functional/test_capture.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyk4a import PyK4A 5 | 6 | 7 | class TestCapture: 8 | @staticmethod 9 | @pytest.mark.device 10 | @pytest.mark.opengl 11 | def test_device_capture_images(device: PyK4A): 12 | device.open() 13 | device._start_cameras() 14 | capture = device.get_capture() 15 | color = capture.color 16 | assert color is not None 17 | depth = capture.depth 18 | assert depth is not None 19 | ir = capture.ir 20 | assert ir is not None 21 | assert np.array_equal(color, depth) is False 22 | assert np.array_equal(depth, ir) is False 23 | assert np.array_equal(ir, color) is False 24 | assert capture.color_white_balance is not None 25 | assert capture.color_exposure_usec is not None 26 | assert capture.color_iso_speed is not None 27 | -------------------------------------------------------------------------------- /tests/functional/test_device.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyk4a import K4AException, PyK4A, connected_device_count 4 | 5 | 6 | class TestOpenClose: 7 | @staticmethod 8 | def test_open_none_existing_device(device_not_exists: PyK4A): 9 | with pytest.raises(K4AException): 10 | device_not_exists.open() 11 | 12 | @staticmethod 13 | @pytest.mark.device 14 | def test_open_close_existing_device(device: PyK4A): 15 | device.open() 16 | device.close() 17 | 18 | 19 | class TestProperties: 20 | @staticmethod 21 | @pytest.mark.device 22 | def test_sync_jack_status(device: PyK4A): 23 | device.open() 24 | jack_in, jack_out = device.sync_jack_status 25 | assert isinstance(jack_in, bool) 26 | assert isinstance(jack_out, bool) 27 | 28 | @staticmethod 29 | @pytest.mark.device 30 | def test_get_color_control(device: PyK4A): 31 | device.open() 32 | brightness = device.brightness 33 | assert isinstance(brightness, int) 34 | 35 | @staticmethod 36 | @pytest.mark.device 37 | def test_set_color_control(device: PyK4A): 38 | device.open() 39 | device.brightness = 123 40 | 41 | @staticmethod 42 | @pytest.mark.device 43 | def test_reset_color_control_to_default(device: PyK4A): 44 | device.open() 45 | device.reset_color_control_to_default() 46 | assert device.brightness == 128 # default value 128 47 | device.brightness = 123 48 | assert device.brightness == 123 49 | device.reset_color_control_to_default() 50 | assert device.brightness == 128 51 | 52 | @staticmethod 53 | @pytest.mark.device 54 | def test_get_calibration(device: PyK4A): 55 | device.open() 56 | calibration = device.calibration 57 | assert calibration._calibration_handle 58 | 59 | @staticmethod 60 | @pytest.mark.device 61 | def test_serial(device: PyK4A): 62 | device.open() 63 | serial = device.serial 64 | assert len(serial) > 5 65 | 66 | 67 | class TestCameras: 68 | @staticmethod 69 | @pytest.mark.device 70 | @pytest.mark.opengl 71 | def test_start_stop_cameras(device: PyK4A): 72 | device.open() 73 | device._start_cameras() 74 | device._stop_cameras() 75 | 76 | @staticmethod 77 | @pytest.mark.device 78 | @pytest.mark.opengl 79 | def test_capture(device: PyK4A): 80 | device.open() 81 | device._start_cameras() 82 | capture = device.get_capture() 83 | assert capture._capture_handle is not None 84 | 85 | 86 | class TestIMU: 87 | @staticmethod 88 | @pytest.mark.device 89 | @pytest.mark.opengl 90 | def test_start_stop_imu(device: PyK4A): 91 | device.open() 92 | device._start_cameras() # imu will not work without cameras 93 | device._start_imu() 94 | device._stop_imu() 95 | device._stop_cameras() 96 | 97 | @staticmethod 98 | @pytest.mark.device 99 | @pytest.mark.opengl 100 | def test_get_imu_sample(device: PyK4A): 101 | device.open() 102 | device._start_cameras() 103 | device._start_imu() 104 | sample = device.get_imu_sample() 105 | assert sample is not None 106 | 107 | 108 | class TestCalibrationRaw: 109 | @staticmethod 110 | @pytest.mark.device 111 | def test_calibration_raw(device: PyK4A): 112 | device.open() 113 | raw = device.calibration_raw 114 | import sys 115 | 116 | print(raw, file=sys.stderr) 117 | assert raw 118 | 119 | 120 | class TestInstalledCount: 121 | @staticmethod 122 | def test_count(): 123 | count = connected_device_count() 124 | assert count >= 0 125 | -------------------------------------------------------------------------------- /tests/functional/test_playback.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyk4a import K4AException, PyK4APlayback, SeekOrigin 4 | from pyk4a.config import FPS, ColorResolution, DepthMode, ImageFormat, WiredSyncMode 5 | from pyk4a.playback import Configuration 6 | 7 | 8 | RECORD_LENGTH = 463945 9 | RECORD_CALIBRATION_JSON = r'{"CalibrationInformation":{"Cameras":[{"Intrinsics":{"ModelParameterCount":14,"ModelParameters":[0.51296979188919067,0.51692354679107666,0.4927808940410614,0.49290284514427185,0.54793977737426758,-0.020971370860934258,-0.0026522316038608551,0.88967907428741455,0.086130209267139435,-0.013910384848713875,0,0,-7.2104157879948616E-5,2.0557534298859537E-5],"ModelType":"CALIBRATION_LensDistortionModelBrownConrady"},"Location":"CALIBRATION_CameraLocationD0","Purpose":"CALIBRATION_CameraPurposeDepth","MetricRadius":1.7399997711181641,"Rt":{"Rotation":[1,0,0,0,1,0,0,0,1],"Translation":[0,0,0]},"SensorHeight":1024,"SensorWidth":1024,"Shutter":"CALIBRATION_ShutterTypeUndefined","ThermalAdjustmentParams":{"Params":[0,0,0,0,0,0,0,0,0,0,0,0]}},{"Intrinsics":{"ModelParameterCount":14,"ModelParameters":[0.49984714388847351,0.50661760568618774,0.47796511650085449,0.63742393255233765,0.33012130856513977,-2.4893965721130371,1.453670859336853,0.20891207456588745,-2.302915096282959,1.3751275539398193,0,0,-0.00012944525224156678,-0.00016178244550246745],"ModelType":"CALIBRATION_LensDistortionModelBrownConrady"},"Location":"CALIBRATION_CameraLocationPV0","Purpose":"CALIBRATION_CameraPurposePhotoVideo","MetricRadius":0,"Rt":{"Rotation":[0.99999922513961792,0.0011817576596513391,-0.00044814043212682009,-0.0011387512786313891,0.99628061056137085,0.0861603245139122,0.0005482942215166986,-0.086159750819206238,0.9962812066078186],"Translation":[-0.032083429396152496,-0.0022053730208426714,0.0038836926687508821]},"SensorHeight":3072,"SensorWidth":4096,"Shutter":"CALIBRATION_ShutterTypeUndefined","ThermalAdjustmentParams":{"Params":[0,0,0,0,0,0,0,0,0,0,0,0]}}],"InertialSensors":[{"BiasTemperatureModel":[-0.00038840249180793762,0,0,0,0.0066333282738924026,0,0,0,0.0011737601598724723,0,0,0],"BiasUncertainty":[9.9999997473787516E-5,9.9999997473787516E-5,9.9999997473787516E-5],"Id":"CALIBRATION_InertialSensorId_LSM6DSM","MixingMatrixTemperatureModel":[0.99519604444503784,0,0,0,0.0019702909048646688,0,0,0,-0.00029000110225751996,0,0,0,0.0019510588608682156,0,0,0,1.00501549243927,0,0,0,0.0030893723014742136,0,0,0,-0.00028924690559506416,0,0,0,0.0031117112375795841,0,0,0,0.99779671430587769,0,0,0],"ModelTypeMask":16,"Noise":[0.00095000001601874828,0.00095000001601874828,0.00095000001601874828,0,0,0],"Rt":{"Rotation":[0.0018387800082564354,0.11238107085227966,-0.993663489818573,-0.99999493360519409,-0.002381625585258007,-0.0021198526956140995,-0.0026047658175230026,0.99366235733032227,0.11237611621618271],"Translation":[0,0,0]},"SecondOrderScaling":[0,0,0,0,0,0,0,0,0],"SensorType":"CALIBRATION_InertialSensorType_Gyro","TemperatureBounds":[5,60],"TemperatureC":0},{"BiasTemperatureModel":[0.1411108672618866,0,0,0,0.0053673363290727139,0,0,0,-0.42933177947998047,0,0,0],"BiasUncertainty":[0.0099999997764825821,0.0099999997764825821,0.0099999997764825821],"Id":"CALIBRATION_InertialSensorId_LSM6DSM","MixingMatrixTemperatureModel":[0.99946749210357666,0,0,0,3.0261040592449717E-5,0,0,0,-0.00310571794398129,0,0,0,3.0574858101317659E-5,0,0,0,0.98919963836669922,0,0,0,0.00054294260917231441,0,0,0,-0.0031195830088108778,0,0,0,0.00053976889466866851,0,0,0,0.995025634765625,0,0,0],"ModelTypeMask":56,"Noise":[0.010700000450015068,0.010700000450015068,0.010700000450015068,0,0,0],"Rt":{"Rotation":[0.0019369830843061209,0.1084766760468483,-0.994097113609314,-0.99999403953552246,-0.0026388836558908224,-0.0022364303003996611,-0.0028659072704613209,0.994095504283905,0.10847091674804688],"Translation":[-0.0509832426905632,0.0034723200369626284,0.0013277813559398055]},"SecondOrderScaling":[0,0,0,0,0,0,0,0,0],"SensorType":"CALIBRATION_InertialSensorType_Accelerometer","TemperatureBounds":[5,60],"TemperatureC":0}],"Metadata":{"SerialId":"001514394512","FactoryCalDate":"11/9/2019 10:32:12 AM GMT","Version":{"Major":1,"Minor":2},"DeviceName":"AzureKinect-PV","Notes":"PV0_max_radius_invalid"}}}' # noqa: E501 10 | RECORD_CONFIGURATION = Configuration( 11 | color_format=ImageFormat.COLOR_MJPG, 12 | color_resolution=ColorResolution.RES_720P, 13 | depth_mode=DepthMode.NFOV_UNBINNED, 14 | camera_fps=FPS.FPS_5, 15 | color_track_enabled=True, 16 | depth_track_enabled=True, 17 | ir_track_enabled=True, 18 | imu_track_enabled=True, 19 | depth_delay_off_color_usec=0, 20 | wired_sync_mode=WiredSyncMode.STANDALONE, 21 | subordinate_delay_off_master_usec=0, 22 | start_timestamp_offset_usec=336277, 23 | ) 24 | 25 | 26 | class TestInit: 27 | @staticmethod 28 | def test_not_existing_path(): 29 | playback = PyK4APlayback(path="/some/not-exists.file") 30 | with pytest.raises(K4AException): 31 | playback.open() 32 | 33 | 34 | class TestPropertyLength: 35 | @staticmethod 36 | def test_correct_value(playback: PyK4APlayback): 37 | playback.open() 38 | assert playback.length == RECORD_LENGTH 39 | 40 | 41 | class TestPropertyCalibrationRaw: 42 | @staticmethod 43 | def test_correct_value(playback: PyK4APlayback): 44 | playback.open() 45 | assert playback.calibration_raw == RECORD_CALIBRATION_JSON 46 | 47 | 48 | class TestPropertyConfiguration: 49 | @staticmethod 50 | def test_readness(playback: PyK4APlayback): 51 | playback.open() 52 | assert playback.configuration == RECORD_CONFIGURATION 53 | 54 | 55 | class TestCalibration: 56 | @staticmethod 57 | def test_readness(playback: PyK4APlayback): 58 | playback.open() 59 | calibration = playback.calibration 60 | assert calibration 61 | 62 | 63 | class TestSeek: 64 | # playback asset has only one capture inside 65 | 66 | @staticmethod 67 | def test_seek_from_start(playback: PyK4APlayback): 68 | # TODO fetch capture/data and validate time 69 | playback.open() 70 | playback.get_next_capture() 71 | playback.seek(playback.configuration["start_timestamp_offset_usec"], origin=SeekOrigin.BEGIN) 72 | capture = playback.get_next_capture() 73 | assert capture.color is not None 74 | with pytest.raises(EOFError): 75 | playback.get_previous_capture() 76 | 77 | @staticmethod 78 | def test_seek_from_end(playback: PyK4APlayback): 79 | # TODO fetch capture/data and validate time 80 | playback.open() 81 | playback.seek(0, origin=SeekOrigin.END) 82 | capture = playback.get_previous_capture() 83 | assert capture.color is not None 84 | with pytest.raises(EOFError): 85 | playback.get_next_capture() 86 | 87 | @staticmethod 88 | def test_seek_by_device_time(playback: PyK4APlayback): 89 | # TODO fetch capture/data and validate time 90 | playback.open() 91 | playback.seek(1, origin=SeekOrigin.DEVICE_TIME) # TODO add correct timestamp from datablock here 92 | capture = playback.get_next_capture() 93 | assert capture.color is not None 94 | 95 | 96 | class TestGetCapture: 97 | @staticmethod 98 | def test_get_next_capture(playback: PyK4APlayback): 99 | playback.open() 100 | capture = playback.get_next_capture() 101 | assert capture is not None 102 | assert capture.depth is not None 103 | assert capture.color is not None 104 | assert capture.depth_timestamp_usec == 800222 105 | assert capture.color_timestamp_usec == 800222 106 | assert capture.ir_timestamp_usec == 800222 107 | assert capture._calibration is not None # Issue #81 108 | 109 | @staticmethod 110 | def test_get_previous_capture(playback: PyK4APlayback): 111 | playback.open() 112 | playback.seek(0, origin=SeekOrigin.END) 113 | capture = playback.get_previous_capture() 114 | assert capture is not None 115 | assert capture.depth is not None 116 | assert capture.color is not None 117 | assert capture.depth_timestamp_usec == 800222 118 | assert capture.color_timestamp_usec == 800222 119 | assert capture.ir_timestamp_usec == 800222 120 | assert capture._calibration is not None # Issue #81 121 | 122 | 123 | class TestGetImuSample: 124 | @staticmethod 125 | def test_get_next_imu_sample(playback: PyK4APlayback): 126 | playback.open() 127 | imu_sample = playback.get_next_imu_sample() 128 | assert imu_sample is not None 129 | assert imu_sample["temperature"] is not None 130 | assert imu_sample["acc_sample"] is not None 131 | assert len(imu_sample["acc_sample"]) == 3 132 | assert imu_sample["gyro_sample"] is not None 133 | assert len(imu_sample["gyro_sample"]) == 3 134 | assert imu_sample["acc_timestamp"] == 336277 135 | assert imu_sample["gyro_timestamp"] == 336277 136 | -------------------------------------------------------------------------------- /tests/functional/test_record.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from pyk4a import Config, ImageFormat, PyK4ACapture, PyK4APlayback, PyK4ARecord 6 | 7 | 8 | @pytest.fixture() 9 | def record_path(tmp_path: Path) -> Path: 10 | return tmp_path / "record.mkv" 11 | 12 | 13 | @pytest.fixture() 14 | def record(record_path: Path) -> PyK4ARecord: 15 | return PyK4ARecord(config=Config(color_format=ImageFormat.COLOR_MJPG), path=record_path) 16 | 17 | 18 | @pytest.fixture() 19 | def created_record(record: PyK4ARecord) -> PyK4ARecord: 20 | record.create() 21 | return record 22 | 23 | 24 | @pytest.fixture() 25 | def capture(playback: PyK4APlayback) -> PyK4ACapture: 26 | playback.open() 27 | return playback.get_next_capture() 28 | 29 | 30 | class TestCreate: 31 | @staticmethod 32 | def test_create(record: PyK4ARecord): 33 | record.create() 34 | assert record.path.exists() 35 | 36 | 37 | class TestClose: 38 | @staticmethod 39 | def test_closing(created_record: PyK4ARecord): 40 | created_record.close() 41 | assert created_record.path.exists() 42 | 43 | 44 | class TestFlush: 45 | @staticmethod 46 | def test_file_size_increased(created_record: PyK4ARecord, capture: PyK4ACapture): 47 | created_record.write_header() 48 | size_before = created_record.path.stat().st_size 49 | created_record.write_capture(capture) 50 | created_record.flush() 51 | size_after = created_record.path.stat().st_size 52 | assert size_after > size_before 53 | -------------------------------------------------------------------------------- /tests/plugins/calibration.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | 7 | @dataclass(frozen=True) 8 | class CalibrationHandle: 9 | pass 10 | 11 | 12 | @pytest.fixture() 13 | def calibration_raw() -> str: 14 | json_file = Path(__file__).parent.parent / "assets" / "calibration.json" 15 | return json_file.read_text() 16 | -------------------------------------------------------------------------------- /tests/plugins/capture.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Callable, Optional 3 | 4 | import numpy as np 5 | import pytest 6 | 7 | 8 | @dataclass(frozen=True) 9 | class CaptureHandle: 10 | color: Optional[np.ndarray] 11 | depth: Optional[np.ndarray] 12 | ir: Optional[np.ndarray] 13 | 14 | 15 | @pytest.fixture() 16 | def capture_factory() -> Callable: 17 | def random_capture(color: bool = True, depth: bool = True, ir: bool = True) -> CaptureHandle: 18 | color_image: Optional[np.ndarray] = None 19 | depth_image: Optional[np.ndarray] = None 20 | ir_image: Optional[np.ndarray] = None 21 | if color: 22 | color_image = np.random.randint(low=0, high=255, size=(720, 1280, 4), dtype=np.uint8) 23 | if depth: 24 | depth_image = np.random.randint(low=0, high=15000, size=(576, 640), dtype=np.uint16) 25 | if ir: 26 | color_image = np.random.randint(low=0, high=3000, size=(576, 640), dtype=np.uint16) 27 | 28 | return CaptureHandle(color=color_image, depth=depth_image, ir=ir_image) 29 | 30 | return random_capture 31 | -------------------------------------------------------------------------------- /tests/plugins/device.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass 3 | from typing import Any, Mapping, Optional, Tuple 4 | 5 | import pytest 6 | 7 | from pyk4a import ( 8 | Calibration, 9 | ColorControlCapabilities, 10 | ColorControlCommand, 11 | ColorControlMode, 12 | ColorResolution, 13 | DepthMode, 14 | ) 15 | from pyk4a.results import Result 16 | 17 | 18 | @dataclass(frozen=True) 19 | class DeviceMeta: 20 | id: int 21 | jack_in: bool = False 22 | jack_out: bool = False 23 | serial: str = "123456789" 24 | color_controls: Tuple[ColorControlCapabilities, ...] = ( 25 | ColorControlCapabilities( 26 | color_control_command=ColorControlCommand.EXPOSURE_TIME_ABSOLUTE, 27 | supports_auto=True, 28 | min_value=500, 29 | max_value=133330, 30 | step_value=100, 31 | default_value=16670, 32 | default_mode=ColorControlMode.AUTO, 33 | ), 34 | ColorControlCapabilities( 35 | color_control_command=ColorControlCommand.AUTO_EXPOSURE_PRIORITY, 36 | supports_auto=False, 37 | min_value=0, 38 | max_value=0, 39 | step_value=0, 40 | default_value=0, 41 | default_mode=ColorControlMode.MANUAL, 42 | ), 43 | ColorControlCapabilities( 44 | color_control_command=ColorControlCommand.BRIGHTNESS, 45 | supports_auto=False, 46 | min_value=0, 47 | max_value=255, 48 | step_value=1, 49 | default_value=128, 50 | default_mode=ColorControlMode.MANUAL, 51 | ), 52 | ColorControlCapabilities( 53 | color_control_command=ColorControlCommand.CONTRAST, 54 | supports_auto=False, 55 | min_value=0, 56 | max_value=10, 57 | step_value=1, 58 | default_value=5, 59 | default_mode=ColorControlMode.MANUAL, 60 | ), 61 | ColorControlCapabilities( 62 | color_control_command=ColorControlCommand.SATURATION, 63 | supports_auto=False, 64 | min_value=0, 65 | max_value=63, 66 | step_value=1, 67 | default_value=32, 68 | default_mode=ColorControlMode.MANUAL, 69 | ), 70 | ColorControlCapabilities( 71 | color_control_command=ColorControlCommand.SHARPNESS, 72 | supports_auto=False, 73 | min_value=0, 74 | max_value=4, 75 | step_value=1, 76 | default_value=2, 77 | default_mode=ColorControlMode.MANUAL, 78 | ), 79 | ColorControlCapabilities( 80 | color_control_command=ColorControlCommand.WHITEBALANCE, 81 | supports_auto=True, 82 | min_value=2500, 83 | max_value=12500, 84 | step_value=10, 85 | default_value=4500, 86 | default_mode=ColorControlMode.AUTO, 87 | ), 88 | ColorControlCapabilities( 89 | color_control_command=ColorControlCommand.BACKLIGHT_COMPENSATION, 90 | supports_auto=False, 91 | min_value=0, 92 | max_value=1, 93 | step_value=1, 94 | default_value=0, 95 | default_mode=ColorControlMode.MANUAL, 96 | ), 97 | ColorControlCapabilities( 98 | color_control_command=ColorControlCommand.GAIN, 99 | supports_auto=False, 100 | min_value=0, 101 | max_value=255, 102 | step_value=1, 103 | default_value=128, 104 | default_mode=ColorControlMode.MANUAL, 105 | ), 106 | ColorControlCapabilities( 107 | color_control_command=ColorControlCommand.POWERLINE_FREQUENCY, 108 | supports_auto=False, 109 | min_value=1, 110 | max_value=2, 111 | step_value=1, 112 | default_value=2, 113 | default_mode=ColorControlMode.MANUAL, 114 | ), 115 | ) 116 | 117 | 118 | DEVICE_METAS: Mapping[int, DeviceMeta] = {0: DeviceMeta(id=0)} 119 | 120 | 121 | @pytest.fixture() 122 | def patch_module_device(monkeypatch, calibration_raw, capture_factory): 123 | @dataclass 124 | class ColorControl: 125 | mode: int 126 | value: int 127 | 128 | class DeviceHandle: 129 | def __init__(self, device_id: int): 130 | self._meta: DeviceMeta = DEVICE_METAS[device_id] 131 | self._opened = True 132 | self._cameras_started = False 133 | self._imu_started = False 134 | self._color_controls: Mapping[ColorControlCommand, ColorControl] = self._default_color_controls() 135 | 136 | def _default_color_controls(self) -> Mapping[ColorControlCommand, ColorControl]: 137 | return { 138 | color_control["color_control_command"]: ColorControl( 139 | mode=color_control["default_mode"], 140 | value=color_control["default_value"], 141 | ) 142 | for color_control in self._meta.color_controls 143 | } 144 | 145 | def close(self) -> int: 146 | assert self._opened is True 147 | self._opened = False 148 | return Result.Success.value 149 | 150 | def get_sync_jack(self) -> Tuple[int, bool, bool]: 151 | assert self._opened is True 152 | return Result.Success.value, self._meta.jack_in, self._meta.jack_out 153 | 154 | def device_get_color_control(self, cmd: int) -> Tuple[int, int, int]: 155 | assert self._opened is True 156 | control = self._color_controls[ColorControlCommand(cmd)] 157 | return Result.Success.value, control.mode, control.value 158 | 159 | def device_set_color_control(self, cmd: int, mode: int, value: int): 160 | assert self._opened is True 161 | command = ColorControlCommand(cmd) 162 | control = self._color_controls[command] 163 | for color_control_meta in self._meta.color_controls: 164 | if color_control_meta["color_control_command"] == command: 165 | control_meta: ColorControlCapabilities = color_control_meta 166 | break 167 | else: 168 | # Non-reachable 169 | raise ValueError(f"Unknown cmd: {cmd}") 170 | 171 | if ColorControlMode(mode) == ColorControlMode.AUTO: 172 | if not control_meta["supports_auto"]: 173 | return Result.Failed.value 174 | control.mode = mode 175 | control.value = control_meta["default_value"] 176 | else: 177 | if value < color_control_meta["min_value"] or value > color_control_meta["max_value"]: 178 | return Result.Failed.value 179 | control.mode = mode 180 | control.value = value 181 | return Result.Success.value 182 | 183 | def device_get_color_control_capabilities(self, cmd: int) -> Tuple[int, ColorControlCapabilities]: 184 | assert self._opened is True 185 | command = ColorControlCommand(cmd) 186 | for color_control_meta in self._meta.color_controls: 187 | if color_control_meta["color_control_command"] == command: 188 | control_meta: ColorControlCapabilities = color_control_meta 189 | break 190 | else: 191 | # Non-reachable 192 | raise ValueError(f"Unknown cmd: {cmd}") 193 | return Result.Success.value, control_meta 194 | 195 | def device_start_cameras(self) -> int: 196 | assert self._opened is True 197 | if self._cameras_started: 198 | return Result.Failed.value 199 | self._cameras_started = True 200 | return Result.Success.value 201 | 202 | def device_stop_cameras(self) -> int: 203 | assert self._opened is True 204 | if not self._cameras_started: 205 | return Result.Failed.value 206 | self._cameras_started = False 207 | return Result.Success.value 208 | 209 | def device_start_imu(self) -> int: 210 | assert self._opened is True 211 | if not self._cameras_started: # imu didnt work without color camera 212 | return Result.Failed.value 213 | if self._imu_started: 214 | return Result.Failed.value 215 | self._imu_started = True 216 | return Result.Success.value 217 | 218 | def device_stop_imu(self) -> int: 219 | assert self._opened is True 220 | if not self._imu_started: 221 | return Result.Failed.value 222 | self._imu_started = False 223 | return Result.Success.value 224 | 225 | def device_get_capture(self) -> Tuple[int, Optional[object]]: 226 | assert self._opened is True 227 | if not self._cameras_started: 228 | return Result.Failed.value, None 229 | return Result.Success.value, capture_factory() 230 | 231 | def device_get_imu_sample( 232 | self, 233 | ) -> Tuple[int, Optional[Tuple[float, Tuple[float, float, float], int, Tuple[float, float, float], int]]]: 234 | assert self._opened is True 235 | if not self._cameras_started: 236 | return Result.Failed.value, None 237 | return ( 238 | Result.Success.value, 239 | (36.6, (0.1, 9.8, 0.005), int(time.time() * 1e6), (0.1, 0.2, 0.3), int(time.time() * 1e6)), 240 | ) 241 | 242 | def device_get_calibration(self, depth_mode: int, color_resolution: int) -> Tuple[int, Optional[object]]: 243 | assert self._opened is True 244 | calibration = Calibration.from_raw(calibration_raw, DepthMode.NFOV_UNBINNED, ColorResolution.RES_720P) 245 | return Result.Success.value, calibration._calibration_handle 246 | 247 | def device_get_raw_calibration(self) -> Optional[str]: 248 | assert self._opened is True 249 | return "{}" 250 | 251 | def device_get_serialnum(self) -> str: 252 | assert self._opened is True 253 | return self._meta.serial 254 | 255 | def _device_open(device_id: int, thread_safe: bool) -> Tuple[int, object]: 256 | if device_id not in DEVICE_METAS: 257 | return Result.Failed.value, None 258 | capsule = DeviceHandle(device_id) 259 | return Result.Success.value, capsule 260 | 261 | def _device_close(capsule: DeviceHandle, thread_safe: bool) -> int: 262 | capsule.close() 263 | return Result.Success.value 264 | 265 | def _device_get_sync_jack(capsule: DeviceHandle, thread_safe: bool) -> Tuple[int, bool, bool]: 266 | return capsule.get_sync_jack() 267 | 268 | def _device_get_color_control(capsule: DeviceHandle, thread_safe: bool, cmd: int) -> Tuple[int, int, int]: 269 | return capsule.device_get_color_control(cmd) 270 | 271 | def _device_set_color_control(capsule: DeviceHandle, thread_safe: bool, cmd: int, mode: int, value: int) -> int: 272 | return capsule.device_set_color_control(cmd, mode, value) 273 | 274 | def _device_get_color_control_capabilities( 275 | capsule: DeviceHandle, thread_safe: bool, cmd: int 276 | ) -> Tuple[int, ColorControlCapabilities]: 277 | return capsule.device_get_color_control_capabilities(cmd) 278 | 279 | def _device_start_cameras( 280 | capsule: DeviceHandle, 281 | thread_safe: bool, 282 | color_format: int, 283 | color_resolution: int, 284 | dept_mode: int, 285 | camera_fps: int, 286 | synchronized_images_only: bool, 287 | depth_delay_off_color_usec: int, 288 | wired_sync_mode: int, 289 | subordinate_delay_off_master_usec: int, 290 | disable_streaming_indicator: bool, 291 | ) -> int: 292 | return capsule.device_start_cameras() 293 | 294 | def _device_stop_cameras(capsule: DeviceHandle, thread_safe: bool) -> int: 295 | return capsule.device_stop_cameras() 296 | 297 | def _device_start_imu(capsule: DeviceHandle, thread_safe: bool) -> int: 298 | return capsule.device_start_imu() 299 | 300 | def _device_stop_imu(capsule: DeviceHandle, thread_safe: bool) -> int: 301 | return capsule.device_stop_imu() 302 | 303 | def _device_get_capture(capsule: DeviceHandle, thread_safe: bool, timeout: int) -> Tuple[int, Optional[object]]: 304 | return capsule.device_get_capture() 305 | 306 | def _device_get_imu_sample( 307 | capsule: DeviceHandle, thread_safe: bool, timeout: int 308 | ) -> Tuple[int, Optional[Tuple[float, Tuple[float, float, float], int, Tuple[float, float, float], int]]]: 309 | return capsule.device_get_imu_sample() 310 | 311 | def _device_get_calibration( 312 | capsule: DeviceHandle, thread_safe, depth_mode: int, color_resolution: int 313 | ) -> Tuple[int, Optional[object]]: 314 | return capsule.device_get_calibration(depth_mode, color_resolution) 315 | 316 | def _device_get_raw_calibration(capsule: DeviceHandle, thread_safe) -> Optional[str]: 317 | return capsule.device_get_raw_calibration() 318 | 319 | def _device_get_installed_count() -> int: 320 | return 1 321 | 322 | def _device_get_serialnum(capsule: DeviceHandle, thread_safe) -> Optional[str]: 323 | return capsule.device_get_serialnum() 324 | 325 | monkeypatch.setattr("k4a_module.device_open", _device_open) 326 | monkeypatch.setattr("k4a_module.device_close", _device_close) 327 | monkeypatch.setattr("k4a_module.device_get_sync_jack", _device_get_sync_jack) 328 | monkeypatch.setattr("k4a_module.device_get_color_control", _device_get_color_control) 329 | monkeypatch.setattr("k4a_module.device_set_color_control", _device_set_color_control) 330 | monkeypatch.setattr("k4a_module.device_get_color_control_capabilities", _device_get_color_control_capabilities) 331 | monkeypatch.setattr("k4a_module.device_start_cameras", _device_start_cameras) 332 | monkeypatch.setattr("k4a_module.device_stop_cameras", _device_stop_cameras) 333 | monkeypatch.setattr("k4a_module.device_start_imu", _device_start_imu) 334 | monkeypatch.setattr("k4a_module.device_stop_imu", _device_stop_imu) 335 | monkeypatch.setattr("k4a_module.device_get_capture", _device_get_capture) 336 | monkeypatch.setattr("k4a_module.device_get_imu_sample", _device_get_imu_sample) 337 | monkeypatch.setattr("k4a_module.device_get_calibration", _device_get_calibration) 338 | monkeypatch.setattr("k4a_module.device_get_raw_calibration", _device_get_raw_calibration) 339 | monkeypatch.setattr("k4a_module.device_get_installed_count", _device_get_installed_count) 340 | monkeypatch.setattr("k4a_module.device_get_serialnum", _device_get_serialnum) 341 | 342 | 343 | @pytest.fixture() 344 | def device_id_good(patch_module_device: Any) -> int: 345 | return 0 346 | 347 | 348 | @pytest.fixture() 349 | def device_id_not_exists(patch_module_device: Any) -> int: 350 | return 99 351 | -------------------------------------------------------------------------------- /tests/plugins/playback.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Mapping, Optional, Tuple 3 | 4 | import pytest 5 | 6 | from pyk4a.config import FPS, ColorResolution, DepthMode, ImageFormat, WiredSyncMode 7 | from pyk4a.playback import SeekOrigin 8 | from pyk4a.results import BufferResult, Result, StreamResult 9 | from tests.plugins.calibration import CalibrationHandle 10 | 11 | 12 | @dataclass(frozen=True) 13 | class PlaybackMeta: 14 | filename: str 15 | length: int 16 | calibration_json_str: Optional[str] = None 17 | configuration: Optional[Tuple[int, ...]] = None 18 | 19 | 20 | PLAYBACK_METAS: Mapping[str, PlaybackMeta] = { 21 | "file1.mkv": PlaybackMeta( 22 | filename="file1.mkv", 23 | length=1234, 24 | calibration_json_str=r'{"CalibrationInformation":{"Cameras":[{"Intrinsics":{"ModelParameterCount":14,"ModelParameters":[0.51296979188919067,0.51692354679107666,0.4927808940410614,0.49290284514427185,0.54793977737426758,-0.020971370860934258,-0.0026522316038608551,0.88967907428741455,0.086130209267139435,-0.013910384848713875,0,0,-7.2104157879948616E-5,2.0557534298859537E-5],"ModelType":"CALIBRATION_LensDistortionModelBrownConrady"},"Location":"CALIBRATION_CameraLocationD0","Purpose":"CALIBRATION_CameraPurposeDepth","MetricRadius":1.7399997711181641,"Rt":{"Rotation":[1,0,0,0,1,0,0,0,1],"Translation":[0,0,0]},"SensorHeight":1024,"SensorWidth":1024,"Shutter":"CALIBRATION_ShutterTypeUndefined","ThermalAdjustmentParams":{"Params":[0,0,0,0,0,0,0,0,0,0,0,0]}},{"Intrinsics":{"ModelParameterCount":14,"ModelParameters":[0.49984714388847351,0.50661760568618774,0.47796511650085449,0.63742393255233765,0.33012130856513977,-2.4893965721130371,1.453670859336853,0.20891207456588745,-2.302915096282959,1.3751275539398193,0,0,-0.00012944525224156678,-0.00016178244550246745],"ModelType":"CALIBRATION_LensDistortionModelBrownConrady"},"Location":"CALIBRATION_CameraLocationPV0","Purpose":"CALIBRATION_CameraPurposePhotoVideo","MetricRadius":0,"Rt":{"Rotation":[0.99999922513961792,0.0011817576596513391,-0.00044814043212682009,-0.0011387512786313891,0.99628061056137085,0.0861603245139122,0.0005482942215166986,-0.086159750819206238,0.9962812066078186],"Translation":[-0.032083429396152496,-0.0022053730208426714,0.0038836926687508821]},"SensorHeight":3072,"SensorWidth":4096,"Shutter":"CALIBRATION_ShutterTypeUndefined","ThermalAdjustmentParams":{"Params":[0,0,0,0,0,0,0,0,0,0,0,0]}}],"InertialSensors":[{"BiasTemperatureModel":[-0.00038840249180793762,0,0,0,0.0066333282738924026,0,0,0,0.0011737601598724723,0,0,0],"BiasUncertainty":[9.9999997473787516E-5,9.9999997473787516E-5,9.9999997473787516E-5],"Id":"CALIBRATION_InertialSensorId_LSM6DSM","MixingMatrixTemperatureModel":[0.99519604444503784,0,0,0,0.0019702909048646688,0,0,0,-0.00029000110225751996,0,0,0,0.0019510588608682156,0,0,0,1.00501549243927,0,0,0,0.0030893723014742136,0,0,0,-0.00028924690559506416,0,0,0,0.0031117112375795841,0,0,0,0.99779671430587769,0,0,0],"ModelTypeMask":16,"Noise":[0.00095000001601874828,0.00095000001601874828,0.00095000001601874828,0,0,0],"Rt":{"Rotation":[0.0018387800082564354,0.11238107085227966,-0.993663489818573,-0.99999493360519409,-0.002381625585258007,-0.0021198526956140995,-0.0026047658175230026,0.99366235733032227,0.11237611621618271],"Translation":[0,0,0]},"SecondOrderScaling":[0,0,0,0,0,0,0,0,0],"SensorType":"CALIBRATION_InertialSensorType_Gyro","TemperatureBounds":[5,60],"TemperatureC":0},{"BiasTemperatureModel":[0.1411108672618866,0,0,0,0.0053673363290727139,0,0,0,-0.42933177947998047,0,0,0],"BiasUncertainty":[0.0099999997764825821,0.0099999997764825821,0.0099999997764825821],"Id":"CALIBRATION_InertialSensorId_LSM6DSM","MixingMatrixTemperatureModel":[0.99946749210357666,0,0,0,3.0261040592449717E-5,0,0,0,-0.00310571794398129,0,0,0,3.0574858101317659E-5,0,0,0,0.98919963836669922,0,0,0,0.00054294260917231441,0,0,0,-0.0031195830088108778,0,0,0,0.00053976889466866851,0,0,0,0.995025634765625,0,0,0],"ModelTypeMask":56,"Noise":[0.010700000450015068,0.010700000450015068,0.010700000450015068,0,0,0],"Rt":{"Rotation":[0.0019369830843061209,0.1084766760468483,-0.994097113609314,-0.99999403953552246,-0.0026388836558908224,-0.0022364303003996611,-0.0028659072704613209,0.994095504283905,0.10847091674804688],"Translation":[-0.0509832426905632,0.0034723200369626284,0.0013277813559398055]},"SecondOrderScaling":[0,0,0,0,0,0,0,0,0],"SensorType":"CALIBRATION_InertialSensorType_Accelerometer","TemperatureBounds":[5,60],"TemperatureC":0}],"Metadata":{"SerialId":"001514394512","FactoryCalDate":"11/9/2019 10:32:12 AM GMT","Version":{"Major":1,"Minor":2},"DeviceName":"AzureKinect-PV","Notes":"PV0_max_radius_invalid"}}}', # noqa: E501 25 | configuration=( 26 | ImageFormat.COLOR_MJPG.value, 27 | ColorResolution.RES_720P.value, 28 | DepthMode.NFOV_UNBINNED.value, 29 | FPS.FPS_5.value, 30 | 1, 31 | 1, 32 | 1, 33 | 1, 34 | 1, 35 | WiredSyncMode.STANDALONE.value, 36 | 0, 37 | 336277, 38 | ), 39 | ), 40 | "file2_bad.mkv": PlaybackMeta(filename="file2_bad.mkv.mkv", length=0), 41 | } 42 | 43 | 44 | @pytest.fixture() 45 | def patch_module_playback(monkeypatch): 46 | class PlaybackHandle: 47 | def __init__(self, filename: str): 48 | self._filename = filename 49 | self._meta: PlaybackMeta = PLAYBACK_METAS[filename] 50 | self._opened = True 51 | self._position: int = 0 52 | 53 | def close(self) -> int: 54 | assert self._opened 55 | self._opened = False 56 | return Result.Success.value 57 | 58 | def playback_get_recording_length_usec(self) -> int: 59 | assert self._opened 60 | return self._meta.length 61 | 62 | def playback_get_raw_calibration(self) -> Tuple[int, str]: 63 | assert self._opened 64 | if self._meta.calibration_json_str: 65 | return BufferResult.Success.value, self._meta.calibration_json_str 66 | return BufferResult.TooSmall.value, "" 67 | 68 | def playback_seek_timestamp(self, offset: int, origin: int) -> int: 69 | assert self._opened 70 | if self._meta.length == 0: 71 | return StreamResult.Failed.value 72 | typed_origin = SeekOrigin(origin) 73 | position = self._position 74 | if typed_origin == SeekOrigin.BEGIN: 75 | position = position + offset 76 | if position > self._meta.length: 77 | return StreamResult.EOF.value 78 | elif typed_origin == SeekOrigin.END: 79 | position = position - offset 80 | if position < 0: 81 | return StreamResult.EOF.value 82 | elif typed_origin == SeekOrigin.DEVICE_TIME: 83 | # not supported in mock 84 | pass 85 | self._position = position 86 | return StreamResult.Success.value 87 | 88 | def playback_get_calibration(self) -> Tuple[int, object]: 89 | assert self._opened 90 | return StreamResult.Success.value, CalibrationHandle() 91 | 92 | def playback_get_record_configuration(self) -> Tuple[int, Tuple[int, ...]]: 93 | assert self._opened 94 | if self._meta.configuration: 95 | return Result.Success.value, self._meta.configuration 96 | return Result.Failed.value, () 97 | 98 | def _playback_open(filename: str, thread_safe: bool) -> Tuple[int, object]: 99 | if filename not in PLAYBACK_METAS: 100 | return Result.Failed.value, None 101 | capsule = PlaybackHandle(filename) 102 | return Result.Success.value, capsule 103 | 104 | def _playback_close(capsule: PlaybackHandle, thread_safe: bool) -> int: 105 | return capsule.close() 106 | 107 | def _playback_get_recording_length_usec(capsule: PlaybackHandle, thread_safe: bool) -> int: 108 | return capsule.playback_get_recording_length_usec() 109 | 110 | def _playback_get_raw_calibration(capsule: PlaybackHandle, thread_safe: bool) -> Tuple[int, str]: 111 | return capsule.playback_get_raw_calibration() 112 | 113 | def _playback_seek_timestamp(capsule: PlaybackHandle, thread_safe: bool, offset: int, origin: int) -> int: 114 | return capsule.playback_seek_timestamp(offset, origin) 115 | 116 | def _playback_get_calibration(capsule: PlaybackHandle, thread_safe: bool) -> Tuple[int, object]: 117 | return capsule.playback_get_calibration() 118 | 119 | def _playback_get_record_configuration(capsule: PlaybackHandle, thread_safe: bool) -> Tuple[int, Tuple[int, ...]]: 120 | return capsule.playback_get_record_configuration() 121 | 122 | monkeypatch.setattr("k4a_module.playback_open", _playback_open) 123 | monkeypatch.setattr("k4a_module.playback_close", _playback_close) 124 | monkeypatch.setattr("k4a_module.playback_get_recording_length_usec", _playback_get_recording_length_usec) 125 | monkeypatch.setattr("k4a_module.playback_get_raw_calibration", _playback_get_raw_calibration) 126 | monkeypatch.setattr("k4a_module.playback_seek_timestamp", _playback_seek_timestamp) 127 | monkeypatch.setattr("k4a_module.playback_get_calibration", _playback_get_calibration) 128 | monkeypatch.setattr("k4a_module.playback_get_record_configuration", _playback_get_record_configuration) 129 | 130 | 131 | @pytest.fixture() 132 | def recording_good_file(patch_module_playback: Any) -> str: 133 | return "file1.mkv" 134 | 135 | 136 | @pytest.fixture() 137 | def recording_bad_file(patch_module_playback: Any) -> str: 138 | return "file2_bad.mkv" 139 | 140 | 141 | @pytest.fixture() 142 | def recording_not_exists_file(patch_module_playback: Any) -> str: 143 | return "not_exists.mkv" 144 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennedub/pyk4a/8c153164b484037bd7afc02db0c2be89e7bb8b38/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_calibration.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyk4a import Calibration, CalibrationType, ColorResolution, DepthMode 5 | 6 | 7 | @pytest.fixture() 8 | def calibration(calibration_raw) -> Calibration: 9 | return Calibration.from_raw( 10 | calibration_raw, depth_mode=DepthMode.NFOV_UNBINNED, color_resolution=ColorResolution.RES_720P 11 | ) 12 | 13 | 14 | class TestCalibration: 15 | @staticmethod 16 | def test_properties(calibration_raw): 17 | calibration = Calibration.from_raw( 18 | calibration_raw, depth_mode=DepthMode.NFOV_2X2BINNED, color_resolution=ColorResolution.RES_1536P 19 | ) 20 | assert calibration.depth_mode == DepthMode.NFOV_2X2BINNED 21 | assert calibration.color_resolution == ColorResolution.RES_1536P 22 | 23 | def test_color_to_depth_3d(self, calibration: Calibration): 24 | point_color = 1000.0, 1500.0, 2000.0 25 | point_depth = 1031.4664306640625, 1325.8529052734375, 2117.6611328125 26 | converted = calibration.color_to_depth_3d(point_color) 27 | assert np.allclose(converted, point_depth) 28 | 29 | def test_depth_to_color_3d(self, calibration: Calibration): 30 | point_color = 1000.0, 1500.0, 2000.0 31 | point_depth = 1031.4664306640625, 1325.8529052734375, 2117.6611328125 32 | converted = calibration.depth_to_color_3d(point_depth) 33 | assert np.allclose(converted, point_color) 34 | 35 | def test_2d_to_3d_without_target_camera(self, calibration: Calibration): 36 | point = 250.0, 300.0 37 | depth = 250.0 38 | point3d = -154.5365753173828, -26.12171173095703, 250.0 39 | converted = calibration.convert_2d_to_3d(point, depth, CalibrationType.COLOR) 40 | assert np.allclose(converted, point3d) 41 | 42 | def test_2d_to_3d_with_target_camera(self, calibration: Calibration): 43 | point = 250.0, 300.0 44 | depth = 250.0 45 | point3d = -122.29087829589844, -45.17741394042969, 243.19528198242188 46 | converted = calibration.convert_2d_to_3d(point, depth, CalibrationType.COLOR, CalibrationType.DEPTH) 47 | assert np.allclose(converted, point3d) 48 | 49 | def test_3d_to_2d_without_target_camera(self, calibration: Calibration): 50 | point3d = -154.5365753173828, -26.12171173095703, 250.0 51 | point = 250.0, 300.0 52 | converted = calibration.convert_3d_to_2d(point3d, CalibrationType.COLOR) 53 | assert np.allclose(converted, point) 54 | 55 | def test_3d_to_2d_with_target_camera(self, calibration: Calibration): 56 | point3d = -122.29087829589844, -45.17741394042969, 243.19528198242188 57 | point = 250.0, 300.0 58 | converted = calibration.convert_3d_to_2d(point3d, CalibrationType.DEPTH, CalibrationType.COLOR) 59 | assert np.allclose(converted, point) 60 | -------------------------------------------------------------------------------- /tests/unit/test_device.py: -------------------------------------------------------------------------------- 1 | from typing import Iterator 2 | 3 | import pytest 4 | 5 | from pyk4a import K4AException, PyK4A, connected_device_count 6 | 7 | 8 | DEVICE_ID = 0 9 | DEVICE_ID_NOT_EXISTS = 99 10 | 11 | 12 | @pytest.fixture() 13 | def device(device_id_good: int) -> Iterator[PyK4A]: 14 | device = PyK4A(device_id=device_id_good) 15 | yield device 16 | # autoclose 17 | try: 18 | if device.opened: 19 | device.close() 20 | except K4AException: 21 | pass 22 | 23 | 24 | @pytest.fixture() 25 | def device_not_exists(device_id_not_exists: int) -> Iterator[PyK4A]: 26 | device = PyK4A(device_id=device_id_not_exists) 27 | yield device 28 | # autoclose 29 | try: 30 | if device.opened: 31 | device.close() 32 | except K4AException: 33 | pass 34 | 35 | 36 | class TestOpenClose: 37 | @staticmethod 38 | def test_open_none_existing_device(device_not_exists: PyK4A): 39 | with pytest.raises(K4AException): 40 | device_not_exists.open() 41 | 42 | @staticmethod 43 | def test_open_existing_device(device: PyK4A): 44 | device.open() 45 | 46 | @staticmethod 47 | def test_open_twice(device: PyK4A): 48 | device.open() 49 | with pytest.raises(K4AException, match="Device already opened"): 50 | device.open() 51 | 52 | 53 | class TestProperties: 54 | @staticmethod 55 | def test_sync_jack_status_on_closed_device(device: PyK4A): 56 | with pytest.raises(K4AException, match="Device is not opened"): 57 | assert device.sync_jack_status 58 | 59 | @staticmethod 60 | def test_sync_jack_status(device: PyK4A): 61 | device.open() 62 | jack_in, jack_out = device.sync_jack_status 63 | assert isinstance(jack_in, bool) 64 | assert isinstance(jack_out, bool) 65 | 66 | @staticmethod 67 | def test_color_property_on_closed_device(device: PyK4A): 68 | with pytest.raises(K4AException, match="Device is not opened"): 69 | assert device.brightness 70 | 71 | @staticmethod 72 | def test_color_property(device: PyK4A): 73 | device.open() 74 | assert device.brightness == 128 75 | assert device.contrast == 5 76 | assert device.saturation == 32 77 | assert device.sharpness == 2 78 | assert device.backlight_compensation == 0 79 | assert device.gain == 128 80 | assert device.powerline_frequency == 2 81 | assert device.exposure == 16670 82 | assert device.exposure_mode_auto is True 83 | assert device.whitebalance == 4500 84 | assert device.whitebalance_mode_auto is True 85 | 86 | @staticmethod 87 | def test_color_property_setter_on_closed_device(device: PyK4A): 88 | with pytest.raises(K4AException, match="Device is not opened"): 89 | device.brightness = 123 90 | 91 | @staticmethod 92 | def test_color_property_setter_incorrect_value(device: PyK4A): 93 | with pytest.raises(K4AException): 94 | device.contrast = 5000 95 | 96 | @staticmethod 97 | def test_color_property_setter(device: PyK4A): 98 | device.open() 99 | device.brightness = 123 100 | assert device.brightness == 123 101 | device.contrast = 4 102 | assert device.contrast == 4 103 | device.saturation = 20 104 | assert device.saturation == 20 105 | device.sharpness = 1 106 | assert device.sharpness == 1 107 | device.backlight_compensation = 1 108 | assert device.backlight_compensation == 1 109 | device.gain = 123 110 | assert device.gain == 123 111 | device.powerline_frequency = 1 112 | assert device.powerline_frequency == 1 113 | device.exposure = 17000 114 | assert device.exposure == 17000 115 | device.exposure_mode_auto = False 116 | assert device.exposure_mode_auto is False 117 | device.whitebalance = 5000 118 | assert device.whitebalance == 5000 119 | device.whitebalance_mode_auto = False 120 | assert device.whitebalance_mode_auto is False 121 | 122 | @staticmethod 123 | def test_reset_color_control_to_default_on_closed_device(device: PyK4A): 124 | with pytest.raises(K4AException, match="Device is not opened"): 125 | device.reset_color_control_to_default() 126 | 127 | @staticmethod 128 | def test_reset_color_control_to_default(device: PyK4A): 129 | device.open() 130 | device.brightness = 123 131 | assert device.brightness == 123 132 | device.reset_color_control_to_default() 133 | assert device.brightness == 128 134 | 135 | @staticmethod 136 | def test_calibration(device: PyK4A): 137 | device.open() 138 | device._start_cameras() 139 | calibration = device.calibration 140 | assert calibration 141 | 142 | @staticmethod 143 | def test_serial(device: PyK4A): 144 | device.open() 145 | serial = device.serial 146 | assert serial == "123456789" 147 | 148 | 149 | class TestCameras: 150 | @staticmethod 151 | def test_capture_on_closed_device(device: PyK4A): 152 | with pytest.raises(K4AException, match="Device is not opened"): 153 | device.get_capture() 154 | 155 | @staticmethod 156 | def test_get_capture(device: PyK4A): 157 | device.open() 158 | device._start_cameras() 159 | capture = device.get_capture() 160 | assert capture is not None 161 | 162 | 163 | class TestIMU: 164 | @staticmethod 165 | def test_get_imu_sample_on_closed_device(device: PyK4A): 166 | with pytest.raises(K4AException, match="Device is not opened"): 167 | device.get_imu_sample() 168 | 169 | @staticmethod 170 | def test_get_imu_sample(device: PyK4A): 171 | device.open() 172 | device._start_cameras() 173 | device._start_imu() 174 | sample = device.get_imu_sample() 175 | assert sample 176 | 177 | 178 | class TestCalibrationRaw: 179 | @staticmethod 180 | def test_calibration_raw_on_closed_device(device: PyK4A): 181 | with pytest.raises(K4AException, match="Device is not opened"): 182 | assert device.calibration_raw 183 | 184 | @staticmethod 185 | def test_calibration_raw(device: PyK4A): 186 | device.open() 187 | assert device.calibration_raw 188 | 189 | 190 | class TestInstalledCount: 191 | @staticmethod 192 | def test_count(patch_module_device): 193 | count = connected_device_count() 194 | assert count == 1 195 | -------------------------------------------------------------------------------- /tests/unit/test_playback.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterator 3 | 4 | import pytest 5 | 6 | from pyk4a import K4AException, PyK4APlayback 7 | 8 | 9 | @pytest.fixture() 10 | def playback(recording_good_file: str) -> Iterator[PyK4APlayback]: 11 | playback = PyK4APlayback(path=recording_good_file) 12 | yield playback 13 | # autoclose 14 | try: 15 | playback.close() 16 | except K4AException: 17 | pass 18 | 19 | 20 | @pytest.fixture() 21 | def playback_bad(recording_bad_file: str) -> Iterator[PyK4APlayback]: 22 | playback = PyK4APlayback(path=recording_bad_file) 23 | yield playback 24 | # autoclose 25 | try: 26 | playback.close() 27 | except K4AException: 28 | pass 29 | 30 | 31 | class TestInit: 32 | @staticmethod 33 | def test_path_argument(): 34 | playback = PyK4APlayback(path=Path("some.mkv")) 35 | assert playback.path == Path("some.mkv") 36 | 37 | playback = PyK4APlayback(path="some.mkv") 38 | assert playback.path == Path("some.mkv") 39 | 40 | @staticmethod 41 | def test_not_existing_path(recording_not_exists_file: str): 42 | playback = PyK4APlayback(path=recording_not_exists_file) 43 | with pytest.raises(K4AException): 44 | playback.open() 45 | 46 | 47 | class TestOpen: 48 | @staticmethod 49 | def test_double_open(playback: PyK4APlayback): 50 | playback.open() 51 | with pytest.raises(K4AException, match=r"Playback already opened"): 52 | playback.open() 53 | 54 | 55 | class TestPropertyLength: 56 | @staticmethod 57 | def test_validate_if_record_opened(playback: PyK4APlayback): 58 | with pytest.raises(K4AException, match="Playback not opened."): 59 | playback.length 60 | 61 | @staticmethod 62 | def test_good_file(playback: PyK4APlayback): 63 | playback.open() 64 | assert playback.length == 1234 65 | 66 | 67 | class TestPropertyConfiguration: 68 | @staticmethod 69 | def test_validate_if_record_opened(playback: PyK4APlayback): 70 | with pytest.raises(K4AException, match="Playback not opened."): 71 | assert playback.configuration 72 | 73 | 74 | class TestPropertyCalibrationRaw: 75 | @staticmethod 76 | def test_validate_if_record_opened(playback: PyK4APlayback): 77 | with pytest.raises(K4AException, match="Playback not opened."): 78 | assert playback.calibration_raw 79 | 80 | @staticmethod 81 | def test_good_file(playback: PyK4APlayback): 82 | playback.open() 83 | assert playback.calibration_raw 84 | 85 | @staticmethod 86 | def test_bad_file(playback_bad: PyK4APlayback): 87 | playback_bad.open() 88 | with pytest.raises(K4AException): 89 | assert playback_bad.calibration_raw 90 | 91 | 92 | class TestPropertyCalibration: 93 | @staticmethod 94 | def test_validate_if_record_opened(playback: PyK4APlayback): 95 | with pytest.raises(K4AException, match="Playback not opened."): 96 | assert playback.calibration 97 | 98 | @staticmethod 99 | def test_good_file(playback: PyK4APlayback): 100 | playback.open() 101 | assert playback.calibration 102 | 103 | @staticmethod 104 | def test_bad_file(playback_bad: PyK4APlayback): 105 | playback_bad.open() 106 | with pytest.raises(K4AException): 107 | assert playback_bad.calibration 108 | 109 | 110 | class TestSeek: 111 | @staticmethod 112 | def test_validate_if_record_opened(playback: PyK4APlayback): 113 | with pytest.raises(K4AException, match="Playback not opened."): 114 | playback.seek(1) 115 | 116 | @staticmethod 117 | def test_bad_file(playback_bad: PyK4APlayback): 118 | playback_bad.open() 119 | with pytest.raises(K4AException): 120 | playback_bad.seek(10) 121 | 122 | @staticmethod 123 | def test_good_file(playback: PyK4APlayback): 124 | playback.open() 125 | playback.seek(10) 126 | 127 | @staticmethod 128 | def test_seek_eof(playback: PyK4APlayback): 129 | playback.open() 130 | with pytest.raises(EOFError): 131 | playback.seek(9999) 132 | -------------------------------------------------------------------------------- /tests/unit/test_record.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from pyk4a import Config, ImageFormat, K4AException, PyK4ACapture, PyK4APlayback, PyK4ARecord 6 | 7 | 8 | @pytest.fixture() 9 | def record_path(tmp_path: Path) -> Path: 10 | return tmp_path / "record.mkv" 11 | 12 | 13 | @pytest.fixture() 14 | def record(record_path: Path) -> PyK4ARecord: 15 | return PyK4ARecord(config=Config(color_format=ImageFormat.COLOR_MJPG), path=record_path) 16 | 17 | 18 | @pytest.fixture() 19 | def created_record(record: PyK4ARecord) -> PyK4ARecord: 20 | record.create() 21 | return record 22 | 23 | 24 | @pytest.fixture() 25 | def capture(playback: PyK4APlayback) -> PyK4ACapture: 26 | playback.open() 27 | return playback.get_next_capture() 28 | 29 | 30 | class TestCreate: 31 | @staticmethod 32 | def test_bad_path(tmp_path: Path): 33 | path = tmp_path / "not-exists" / "file.mkv" 34 | record = PyK4ARecord(config=Config(), path=path) 35 | with pytest.raises(K4AException, match=r"Cannot create"): 36 | record.create() 37 | assert not record.created 38 | 39 | @staticmethod 40 | def test_create(record: PyK4ARecord): 41 | record.create() 42 | assert record.created 43 | 44 | @staticmethod 45 | def test_recreate(created_record: PyK4ARecord): 46 | with pytest.raises(K4AException, match=r"already"): 47 | created_record.create() 48 | 49 | 50 | class TestClose: 51 | @staticmethod 52 | def test_not_created_record(record: PyK4ARecord): 53 | with pytest.raises(K4AException, match=r"not created"): 54 | record.close() 55 | 56 | @staticmethod 57 | def test_closing(created_record: PyK4ARecord): 58 | created_record.close() 59 | assert not created_record.created 60 | 61 | 62 | class TestWriteHeader: 63 | @staticmethod 64 | def test_not_created_record(record: PyK4ARecord): 65 | with pytest.raises(K4AException, match=r"not created"): 66 | record.write_header() 67 | 68 | @staticmethod 69 | def test_double_writing(created_record: PyK4ARecord): 70 | created_record.write_header() 71 | with pytest.raises(K4AException, match=r"already written"): 72 | created_record.write_header() 73 | 74 | 75 | class TestWriteCapture: 76 | @staticmethod 77 | def test_not_created_record(record: PyK4ARecord, capture: PyK4ACapture): 78 | with pytest.raises(K4AException, match=r"not created"): 79 | record.write_capture(capture) 80 | 81 | @staticmethod 82 | def test_header_created(created_record: PyK4ARecord, capture: PyK4ACapture): 83 | created_record.write_capture(capture) 84 | assert created_record.header_written 85 | 86 | @staticmethod 87 | def test_captures_count_increased(created_record: PyK4ARecord, capture: PyK4ACapture): 88 | created_record.write_capture(capture) 89 | assert created_record.captures_count == 1 90 | 91 | 92 | class TestFlush: 93 | @staticmethod 94 | def test_not_created_record(record: PyK4ARecord): 95 | with pytest.raises(K4AException, match=r"not created"): 96 | record.flush() 97 | --------------------------------------------------------------------------------