adafruit-circuitpython-usb-host-camera 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/METADATA +151 -0
- adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/RECORD +6 -0
- adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/WHEEL +5 -0
- adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/licenses/LICENSE +21 -0
- adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/top_level.txt +1 -0
- adafruit_usb_host_camera.py +587 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adafruit-circuitpython-usb-host-camera
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CircuitPython USB host driver for cameras
|
|
5
|
+
Author-email: Adafruit Industries <circuitpython@adafruit.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera
|
|
8
|
+
Keywords: adafruit,blinka,circuitpython,micropython,usb_host_camera,camera,webcam,usb,host,usbhost,picture,image,video
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
12
|
+
Classifier: Topic :: System :: Hardware
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Description-Content-Type: text/x-rst
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: Adafruit-Blinka
|
|
17
|
+
Requires-Dist: adafruit-circuitpython-usb-host-descriptors
|
|
18
|
+
Provides-Extra: optional
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
Introduction
|
|
22
|
+
============
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
.. image:: https://readthedocs.org/projects/adafruit-circuitpython-usb-host-camera/badge/?version=latest
|
|
26
|
+
:target: https://docs.circuitpython.org/projects/usb_host_camera/en/latest/
|
|
27
|
+
:alt: Documentation Status
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
.. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg
|
|
31
|
+
:target: https://adafru.it/discord
|
|
32
|
+
:alt: Discord
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
.. image:: https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera/workflows/Build%20CI/badge.svg
|
|
36
|
+
:target: https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera/actions
|
|
37
|
+
:alt: Build Status
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
41
|
+
:target: https://github.com/astral-sh/ruff
|
|
42
|
+
:alt: Code Style: Ruff
|
|
43
|
+
|
|
44
|
+
CircuitPython USB host driver for cameras
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Dependencies
|
|
48
|
+
=============
|
|
49
|
+
This driver depends on:
|
|
50
|
+
|
|
51
|
+
* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
|
|
52
|
+
|
|
53
|
+
Please ensure all dependencies are available on the CircuitPython filesystem.
|
|
54
|
+
This is easily achieved by downloading
|
|
55
|
+
`the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
|
|
56
|
+
or individual libraries can be installed using
|
|
57
|
+
`circup <https://github.com/adafruit/circup>`_.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
`Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_
|
|
61
|
+
|
|
62
|
+
Installing from PyPI
|
|
63
|
+
=====================
|
|
64
|
+
|
|
65
|
+
On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
|
|
66
|
+
PyPI <https://pypi.org/project/adafruit-circuitpython-usb-host-camera/>`_.
|
|
67
|
+
To install for current user:
|
|
68
|
+
|
|
69
|
+
.. code-block:: shell
|
|
70
|
+
|
|
71
|
+
pip3 install adafruit-circuitpython-usb-host-camera
|
|
72
|
+
|
|
73
|
+
To install system-wide (this may be required in some cases):
|
|
74
|
+
|
|
75
|
+
.. code-block:: shell
|
|
76
|
+
|
|
77
|
+
sudo pip3 install adafruit-circuitpython-usb-host-camera
|
|
78
|
+
|
|
79
|
+
To install in a virtual environment in your current project:
|
|
80
|
+
|
|
81
|
+
.. code-block:: shell
|
|
82
|
+
|
|
83
|
+
mkdir project-name && cd project-name
|
|
84
|
+
python3 -m venv .venv
|
|
85
|
+
source .env/bin/activate
|
|
86
|
+
pip3 install adafruit-circuitpython-usb-host-camera
|
|
87
|
+
|
|
88
|
+
Installing to a Connected CircuitPython Device with Circup
|
|
89
|
+
==========================================================
|
|
90
|
+
|
|
91
|
+
Make sure that you have ``circup`` installed in your Python environment.
|
|
92
|
+
Install it with the following command if necessary:
|
|
93
|
+
|
|
94
|
+
.. code-block:: shell
|
|
95
|
+
|
|
96
|
+
pip3 install circup
|
|
97
|
+
|
|
98
|
+
With ``circup`` installed and your CircuitPython device connected use the
|
|
99
|
+
following command to install:
|
|
100
|
+
|
|
101
|
+
.. code-block:: shell
|
|
102
|
+
|
|
103
|
+
circup install adafruit_usb_host_camera
|
|
104
|
+
|
|
105
|
+
Or the following command to update an existing version:
|
|
106
|
+
|
|
107
|
+
.. code-block:: shell
|
|
108
|
+
|
|
109
|
+
circup update
|
|
110
|
+
|
|
111
|
+
Usage Example
|
|
112
|
+
=============
|
|
113
|
+
|
|
114
|
+
.. code-block:: python
|
|
115
|
+
|
|
116
|
+
import time
|
|
117
|
+
|
|
118
|
+
import adafruit_usb_host_camera
|
|
119
|
+
|
|
120
|
+
camera = None
|
|
121
|
+
while camera is None:
|
|
122
|
+
try:
|
|
123
|
+
# Finds the camera among the attached devices (a keyboard, a mouse...)
|
|
124
|
+
camera = adafruit_usb_host_camera.UVCCamera()
|
|
125
|
+
except ValueError:
|
|
126
|
+
print("Plug a USB camera into the USB host port")
|
|
127
|
+
time.sleep(2)
|
|
128
|
+
|
|
129
|
+
print("Camera modes:", camera.modes)
|
|
130
|
+
mode = camera.find_mode(640, 480)
|
|
131
|
+
print("Using", mode)
|
|
132
|
+
camera.start(mode)
|
|
133
|
+
frame = camera.capture()
|
|
134
|
+
camera.stop()
|
|
135
|
+
|
|
136
|
+
camera.save_jpeg("/saves/photo.jpg", frame)
|
|
137
|
+
print("Saved /saves/photo.jpg,", len(frame), "bytes")
|
|
138
|
+
|
|
139
|
+
Documentation
|
|
140
|
+
=============
|
|
141
|
+
API documentation for this library can be found on `Read the Docs <https://docs.circuitpython.org/projects/usb_host_camera/en/latest/>`_.
|
|
142
|
+
|
|
143
|
+
For information on building library documentation, please check out
|
|
144
|
+
`this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
|
|
145
|
+
|
|
146
|
+
Contributing
|
|
147
|
+
============
|
|
148
|
+
|
|
149
|
+
Contributions are welcome! Please read our `Code of Conduct
|
|
150
|
+
<https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera/blob/HEAD/CODE_OF_CONDUCT.md>`_
|
|
151
|
+
before contributing to help this project stay welcoming.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
adafruit_usb_host_camera.py,sha256=CFZF3Fys28nkw8g6LbZUPnlWL_D32c6R9C2-zWyiFU0,23690
|
|
2
|
+
adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/licenses/LICENSE,sha256=IrDmGxfX4xdm-jZka6acotFMss63SMR3pHWyFp7jA3o,1100
|
|
3
|
+
adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/METADATA,sha256=lu1NuXKIwXHV1NsvoGDQMzkoENkXSVMJx-se_mkVGis,4832
|
|
4
|
+
adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/top_level.txt,sha256=UfDIw7qlHytH3MCtD0hB2hMkuEIcqpbSsFJ3ziCyZrE,25
|
|
6
|
+
adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tim Cocks for Adafruit Industries
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adafruit_usb_host_camera
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""
|
|
5
|
+
`adafruit_usb_host_camera`
|
|
6
|
+
================================================================================
|
|
7
|
+
|
|
8
|
+
CircuitPython USB host driver for UVC (USB Video Class) cameras.
|
|
9
|
+
|
|
10
|
+
Negotiates a video format with the camera, then reads whole frames. MJPEG
|
|
11
|
+
frames are complete JPEG images (see `UVCCamera.add_huffman_tables`) that can
|
|
12
|
+
be saved or decoded with ``jpegio``. YUY2 (uncompressed) frames are raw 4:2:2
|
|
13
|
+
pixels.
|
|
14
|
+
|
|
15
|
+
* Author(s): Tim Cocks
|
|
16
|
+
|
|
17
|
+
Implementation Notes
|
|
18
|
+
--------------------
|
|
19
|
+
|
|
20
|
+
**Hardware:**
|
|
21
|
+
|
|
22
|
+
* `Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_
|
|
23
|
+
|
|
24
|
+
**Software and Dependencies:**
|
|
25
|
+
|
|
26
|
+
* Adafruit CircuitPython firmware for the supported boards:
|
|
27
|
+
https://circuitpython.org/downloads
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import struct
|
|
32
|
+
import time
|
|
33
|
+
|
|
34
|
+
import adafruit_usb_host_descriptors
|
|
35
|
+
import usb.core
|
|
36
|
+
from micropython import const
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import displayio
|
|
40
|
+
import jpegio
|
|
41
|
+
except ImportError:
|
|
42
|
+
jpegio = None # only UVCCamera.bitmap needs these
|
|
43
|
+
|
|
44
|
+
__version__ = "1.0.0"
|
|
45
|
+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera.git"
|
|
46
|
+
|
|
47
|
+
_DIR_IN = const(0x80)
|
|
48
|
+
_REQ_CLASS_IF_OUT = const(0x21)
|
|
49
|
+
_REQ_CLASS_IF_IN = const(0xA1)
|
|
50
|
+
_REQ_STD_IF_OUT = const(0x01)
|
|
51
|
+
_REQ_SET_INTERFACE = const(11)
|
|
52
|
+
_SET_CUR = const(0x01)
|
|
53
|
+
_GET_CUR = const(0x81)
|
|
54
|
+
|
|
55
|
+
_VS_PROBE_CONTROL = const(0x01)
|
|
56
|
+
_VS_COMMIT_CONTROL = const(0x02)
|
|
57
|
+
|
|
58
|
+
_CLASS_VIDEO = const(0x0E)
|
|
59
|
+
_SUBCLASS_VC = const(1)
|
|
60
|
+
_SUBCLASS_VS = const(2)
|
|
61
|
+
_CS_INTERFACE = const(0x24)
|
|
62
|
+
_VC_HEADER = const(0x01)
|
|
63
|
+
_VS_FORMAT_UNCOMPRESSED = const(0x04)
|
|
64
|
+
_VS_FRAME_UNCOMPRESSED = const(0x05)
|
|
65
|
+
_VS_FORMAT_MJPEG = const(0x06)
|
|
66
|
+
_VS_FRAME_MJPEG = const(0x07)
|
|
67
|
+
|
|
68
|
+
# Payload header bmHeaderInfo bits
|
|
69
|
+
_HDR_FID = const(0x01)
|
|
70
|
+
_HDR_EOF = const(0x02)
|
|
71
|
+
_HDR_ERR = const(0x40)
|
|
72
|
+
|
|
73
|
+
# Isochronous reads return records of a 16-bit length then that many bytes.
|
|
74
|
+
# This length marks packets the firmware could not keep.
|
|
75
|
+
_LOST = const(0xFFFF)
|
|
76
|
+
|
|
77
|
+
FORMAT_MJPEG = "MJPEG"
|
|
78
|
+
FORMAT_YUY2 = "YUY2"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Mode:
|
|
82
|
+
"""One frame size and format the camera offers."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, format_type, format_index, frame_index, width, height, interval, max_size):
|
|
85
|
+
self.format = format_type
|
|
86
|
+
"""``FORMAT_MJPEG``, ``FORMAT_YUY2``, or the raw character code"""
|
|
87
|
+
self.format_index = format_index
|
|
88
|
+
self.frame_index = frame_index
|
|
89
|
+
self.width = width
|
|
90
|
+
self.height = height
|
|
91
|
+
self.interval = interval
|
|
92
|
+
"""Default frame interval in 100 ns units"""
|
|
93
|
+
self.max_frame_size = max_size
|
|
94
|
+
"""Largest frame in bytes, from the frame descriptor"""
|
|
95
|
+
|
|
96
|
+
def __repr__(self):
|
|
97
|
+
return f"<Mode {self.format} {self.width}x{self.height}>"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def find_camera():
|
|
101
|
+
"""Return the first attached ``usb.core.Device`` that has a UVC streaming
|
|
102
|
+
interface, or None. Other devices (keyboards, mice, hubs...) are skipped."""
|
|
103
|
+
for device in usb.core.find(find_all=True):
|
|
104
|
+
try:
|
|
105
|
+
cfg = adafruit_usb_host_descriptors.get_configuration_descriptor(device, 0)
|
|
106
|
+
except usb.core.USBError:
|
|
107
|
+
continue # could not read it, so not one we can drive
|
|
108
|
+
i = 0
|
|
109
|
+
while i + 6 < len(cfg) and cfg[i]:
|
|
110
|
+
if (
|
|
111
|
+
cfg[i + 1] == adafruit_usb_host_descriptors.DESC_INTERFACE
|
|
112
|
+
and cfg[i + 5] == _CLASS_VIDEO
|
|
113
|
+
and cfg[i + 6] == _SUBCLASS_VS
|
|
114
|
+
):
|
|
115
|
+
return device
|
|
116
|
+
i += cfg[i]
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class UVCCamera:
|
|
121
|
+
"""A USB Video Class camera attached to the USB host port.
|
|
122
|
+
|
|
123
|
+
:param device: a ``usb.core.Device`` for the camera. The default is the
|
|
124
|
+
first camera `find_camera` sees, so other devices such as a keyboard
|
|
125
|
+
can share the host ports.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, device=None):
|
|
129
|
+
if device is None:
|
|
130
|
+
device = find_camera()
|
|
131
|
+
if device is None:
|
|
132
|
+
raise ValueError("No UVC camera found")
|
|
133
|
+
self.device = device
|
|
134
|
+
self.modes = []
|
|
135
|
+
"""The `Mode` objects the camera offers"""
|
|
136
|
+
self.dropped_frames = 0
|
|
137
|
+
"""Frames discarded because they were not valid JPEG data"""
|
|
138
|
+
self.lost_packets = 0
|
|
139
|
+
"""Gaps in the stream: the firmware dropped its oldest packets because
|
|
140
|
+
`capture` was not called for too long, or a packet arrived corrupted"""
|
|
141
|
+
self.read_errors = 0
|
|
142
|
+
"""Reads that failed outright"""
|
|
143
|
+
self.max_frame_size = 0
|
|
144
|
+
self._vc_interface = None
|
|
145
|
+
self._vs_interface = None
|
|
146
|
+
self._uvc_version = 0x0100
|
|
147
|
+
# (alternate setting, endpoint address, is_bulk, max packet size)
|
|
148
|
+
self._endpoints = []
|
|
149
|
+
self._parse(adafruit_usb_host_descriptors.get_configuration_descriptor(device, 0))
|
|
150
|
+
if self._vs_interface is None or not self._endpoints:
|
|
151
|
+
raise ValueError("No UVC streaming interface found")
|
|
152
|
+
device.set_configuration()
|
|
153
|
+
self._streaming = None
|
|
154
|
+
self._ep = None
|
|
155
|
+
self._bulk = False
|
|
156
|
+
self._read_buf = None
|
|
157
|
+
self._frame = None
|
|
158
|
+
self._bitmap = None
|
|
159
|
+
self._decoder = None
|
|
160
|
+
|
|
161
|
+
def _parse(self, cfg):
|
|
162
|
+
i = 0
|
|
163
|
+
kind = None
|
|
164
|
+
format_type = None
|
|
165
|
+
format_index = 0
|
|
166
|
+
alt = 0
|
|
167
|
+
while i < len(cfg):
|
|
168
|
+
length = cfg[i]
|
|
169
|
+
if length == 0:
|
|
170
|
+
break
|
|
171
|
+
dtype = cfg[i + 1]
|
|
172
|
+
if dtype == adafruit_usb_host_descriptors.DESC_INTERFACE:
|
|
173
|
+
kind = None
|
|
174
|
+
if cfg[i + 5] == _CLASS_VIDEO:
|
|
175
|
+
kind = cfg[i + 6]
|
|
176
|
+
if kind == _SUBCLASS_VC and self._vc_interface is None:
|
|
177
|
+
self._vc_interface = cfg[i + 2]
|
|
178
|
+
elif kind == _SUBCLASS_VS and self._vs_interface is None:
|
|
179
|
+
self._vs_interface = cfg[i + 2]
|
|
180
|
+
elif kind == _SUBCLASS_VS and cfg[i + 2] != self._vs_interface:
|
|
181
|
+
kind = None # only drive the first streaming interface
|
|
182
|
+
alt = cfg[i + 3]
|
|
183
|
+
elif dtype == adafruit_usb_host_descriptors.DESC_ENDPOINT and kind == _SUBCLASS_VS:
|
|
184
|
+
address = cfg[i + 2]
|
|
185
|
+
if address & _DIR_IN:
|
|
186
|
+
mps = cfg[i + 4] | cfg[i + 5] << 8
|
|
187
|
+
size = (mps & 0x7FF) * (((mps >> 11) & 3) + 1)
|
|
188
|
+
self._endpoints.append((alt, address, cfg[i + 3] & 3 == 2, size))
|
|
189
|
+
elif dtype == _CS_INTERFACE and kind == _SUBCLASS_VC:
|
|
190
|
+
if cfg[i + 2] == _VC_HEADER:
|
|
191
|
+
self._uvc_version = cfg[i + 3] | cfg[i + 4] << 8
|
|
192
|
+
elif dtype == _CS_INTERFACE and kind == _SUBCLASS_VS:
|
|
193
|
+
sub = cfg[i + 2]
|
|
194
|
+
if sub == _VS_FORMAT_MJPEG:
|
|
195
|
+
format_type = FORMAT_MJPEG
|
|
196
|
+
format_index = cfg[i + 3]
|
|
197
|
+
elif sub == _VS_FORMAT_UNCOMPRESSED:
|
|
198
|
+
format_type = bytes(cfg[i + 5 : i + 9]).decode()
|
|
199
|
+
format_index = cfg[i + 3]
|
|
200
|
+
elif sub in {_VS_FRAME_MJPEG, _VS_FRAME_UNCOMPRESSED}:
|
|
201
|
+
width, height = struct.unpack_from("<HH", cfg, i + 5)
|
|
202
|
+
max_size, interval = struct.unpack_from("<II", cfg, i + 17)
|
|
203
|
+
self.modes.append(
|
|
204
|
+
Mode(
|
|
205
|
+
format_type,
|
|
206
|
+
format_index,
|
|
207
|
+
cfg[i + 3],
|
|
208
|
+
width,
|
|
209
|
+
height,
|
|
210
|
+
interval,
|
|
211
|
+
max_size,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
i += length
|
|
215
|
+
|
|
216
|
+
def find_mode(self, width=None, height=None, format_type=FORMAT_MJPEG):
|
|
217
|
+
"""Return the `Mode` matching ``format_type`` whose size is closest to
|
|
218
|
+
``width`` x ``height`` (smallest available when no size is given)."""
|
|
219
|
+
best = None
|
|
220
|
+
best_score = None
|
|
221
|
+
for mode in self.modes:
|
|
222
|
+
if mode.format != format_type:
|
|
223
|
+
continue
|
|
224
|
+
if width is None:
|
|
225
|
+
score = mode.width * mode.height
|
|
226
|
+
else:
|
|
227
|
+
score = abs(mode.width - width) + abs(mode.height - height)
|
|
228
|
+
if best is None or score < best_score:
|
|
229
|
+
best = mode
|
|
230
|
+
best_score = score
|
|
231
|
+
return best
|
|
232
|
+
|
|
233
|
+
def _probe(self, request, selector, data):
|
|
234
|
+
bm = _REQ_CLASS_IF_IN if request & 0x80 else _REQ_CLASS_IF_OUT
|
|
235
|
+
self.device.ctrl_transfer(bm, request, selector << 8, self._vs_interface, data)
|
|
236
|
+
|
|
237
|
+
def start(self, mode, max_packet_size=None, frame_interval=None, read_size=8192):
|
|
238
|
+
"""Negotiate ``mode`` with the camera and start streaming.
|
|
239
|
+
|
|
240
|
+
:param Mode mode: the format and frame size to stream
|
|
241
|
+
:param int max_packet_size: largest isochronous packet to accept per
|
|
242
|
+
1 ms USB frame, which limits the bandwidth the camera may use
|
|
243
|
+
:param int frame_interval: frame interval in 100 ns units, overriding
|
|
244
|
+
the mode's default. It must be one the camera supports, for
|
|
245
|
+
example 2000000 for 5 fps.
|
|
246
|
+
:param int read_size: bytes to request per isochronous read
|
|
247
|
+
"""
|
|
248
|
+
size = 26 if self._uvc_version < 0x0110 else 34
|
|
249
|
+
probe = bytearray(size)
|
|
250
|
+
interval = frame_interval or mode.interval
|
|
251
|
+
struct.pack_into("<HBBI", probe, 0, 1, mode.format_index, mode.frame_index, interval)
|
|
252
|
+
self._probe(_SET_CUR, _VS_PROBE_CONTROL, probe)
|
|
253
|
+
self._probe(_GET_CUR, _VS_PROBE_CONTROL, probe)
|
|
254
|
+
self._probe(_SET_CUR, _VS_COMMIT_CONTROL, probe)
|
|
255
|
+
negotiated, max_payload = struct.unpack_from("<II", probe, 18)
|
|
256
|
+
self.max_frame_size = max(negotiated, mode.max_frame_size)
|
|
257
|
+
|
|
258
|
+
# Bulk cameras stream on alternate setting 0. For isochronous ones use
|
|
259
|
+
# the smallest allowed alternate setting whose packets hold the
|
|
260
|
+
# camera's payload, or the largest allowed one if none do.
|
|
261
|
+
choice = None
|
|
262
|
+
for ep in self._endpoints:
|
|
263
|
+
packet = ep[3]
|
|
264
|
+
if ep[2]:
|
|
265
|
+
choice = ep
|
|
266
|
+
break
|
|
267
|
+
if max_packet_size and packet > max_packet_size:
|
|
268
|
+
continue
|
|
269
|
+
if choice is None:
|
|
270
|
+
choice = ep
|
|
271
|
+
elif choice[3] < max_payload:
|
|
272
|
+
if packet > choice[3]:
|
|
273
|
+
choice = ep
|
|
274
|
+
elif max_payload <= packet < choice[3]:
|
|
275
|
+
choice = ep
|
|
276
|
+
if choice is None:
|
|
277
|
+
raise ValueError("No streaming endpoint fits max_packet_size")
|
|
278
|
+
alt, self._ep, self._bulk, packet = choice
|
|
279
|
+
if not self._bulk or alt:
|
|
280
|
+
self.device.ctrl_transfer(
|
|
281
|
+
_REQ_STD_IF_OUT, _REQ_SET_INTERFACE, alt, self._vs_interface, None
|
|
282
|
+
)
|
|
283
|
+
# A bulk read ends at each payload boundary (short packet), so the
|
|
284
|
+
# buffer must hold a whole payload. Isochronous reads return whole
|
|
285
|
+
# records, so a read that leaves room for a largest record means the
|
|
286
|
+
# firmware had nothing more buffered (see capture()).
|
|
287
|
+
self._slack = packet + 2
|
|
288
|
+
size = max_payload if self._bulk else max(read_size, 2 * self._slack)
|
|
289
|
+
if self._read_buf is None or len(self._read_buf) != size:
|
|
290
|
+
self._read_buf = bytearray(size)
|
|
291
|
+
if self._frame is None or len(self._frame) < self.max_frame_size:
|
|
292
|
+
self._frame = None
|
|
293
|
+
self._frame = bytearray(self.max_frame_size)
|
|
294
|
+
self._pos = 0
|
|
295
|
+
self._count = 0
|
|
296
|
+
self._records = False
|
|
297
|
+
self._caught_up = False
|
|
298
|
+
self._fid = None # None until synchronized to a frame boundary
|
|
299
|
+
self._reset_assembly(None)
|
|
300
|
+
self._streaming = mode
|
|
301
|
+
|
|
302
|
+
def _reset_assembly(self, buffer):
|
|
303
|
+
self._out = buffer # the buffer the state below refers to
|
|
304
|
+
self._n = 0 # bytes of the frame being assembled
|
|
305
|
+
self._base = 0 # where it starts in the frame buffer
|
|
306
|
+
self._good = True
|
|
307
|
+
self._done = 0 # length of the complete frame just before it
|
|
308
|
+
|
|
309
|
+
def stop(self):
|
|
310
|
+
"""Stop streaming."""
|
|
311
|
+
if self._streaming is None:
|
|
312
|
+
return
|
|
313
|
+
self.device.ctrl_transfer(_REQ_STD_IF_OUT, _REQ_SET_INTERFACE, 0, self._vs_interface, None)
|
|
314
|
+
self._streaming = None
|
|
315
|
+
|
|
316
|
+
def capture(self, buffer=None, timeout=2):
|
|
317
|
+
"""Return the newest complete frame.
|
|
318
|
+
|
|
319
|
+
The firmware buffers about 200 ms of an isochronous stream. When the
|
|
320
|
+
caller takes longer than a frame interval between calls, the frames
|
|
321
|
+
that piled up meanwhile are skipped and the newest complete one is
|
|
322
|
+
returned, so the caller never falls behind until that buffer
|
|
323
|
+
overflows. Nothing counts the skipped frames; to get every frame,
|
|
324
|
+
keep up with the camera or ask it for a lower rate with the
|
|
325
|
+
``frame_interval`` argument of `start`.
|
|
326
|
+
|
|
327
|
+
:param bytearray buffer: optional buffer to fill; it must hold
|
|
328
|
+
``max_frame_size`` bytes and be the same object on every call.
|
|
329
|
+
The default is an internal buffer that the next call overwrites.
|
|
330
|
+
:param float timeout: seconds to wait for a whole frame
|
|
331
|
+
:return: a memoryview of the frame data, at the start of the buffer
|
|
332
|
+
"""
|
|
333
|
+
mode = self._streaming
|
|
334
|
+
if mode is None:
|
|
335
|
+
raise RuntimeError("call start() first")
|
|
336
|
+
out = memoryview(buffer if buffer is not None else self._frame)
|
|
337
|
+
if buffer is not self._out:
|
|
338
|
+
self._reset_assembly(buffer)
|
|
339
|
+
self._fid = None
|
|
340
|
+
check_jpeg = mode.format == FORMAT_MJPEG
|
|
341
|
+
buf = self._read_buf
|
|
342
|
+
view = memoryview(buf)
|
|
343
|
+
size = len(out)
|
|
344
|
+
deadline = time.monotonic() + timeout
|
|
345
|
+
# A complete frame occupies out[base - done:base] while the next one
|
|
346
|
+
# assembles at out[base:base + n].
|
|
347
|
+
n = self._n
|
|
348
|
+
base = self._base
|
|
349
|
+
good = self._good
|
|
350
|
+
done = self._done
|
|
351
|
+
while True:
|
|
352
|
+
if self._pos >= self._count:
|
|
353
|
+
if done and self._caught_up:
|
|
354
|
+
break
|
|
355
|
+
if time.monotonic() > deadline:
|
|
356
|
+
self._n, self._base, self._good, self._done = n, base, good, done
|
|
357
|
+
raise RuntimeError("Timed out waiting for a frame")
|
|
358
|
+
self._pos = self._count = 0
|
|
359
|
+
try:
|
|
360
|
+
count = self.device.read(self._ep, buf, 100)
|
|
361
|
+
except usb.core.USBTimeoutError:
|
|
362
|
+
continue
|
|
363
|
+
except usb.core.USBError:
|
|
364
|
+
good = False # data lost
|
|
365
|
+
self.read_errors += 1
|
|
366
|
+
continue
|
|
367
|
+
if count < 2:
|
|
368
|
+
continue
|
|
369
|
+
# A raw UVC payload starts with the header length and then
|
|
370
|
+
# header flags with the end-of-header bit (0x80) set, so it never
|
|
371
|
+
# looks like a record length.
|
|
372
|
+
first = buf[0] | buf[1] << 8
|
|
373
|
+
self._records = not self._bulk and (first < 0x8000 or first == _LOST)
|
|
374
|
+
self._count = count
|
|
375
|
+
self._caught_up = count + self._slack <= len(buf)
|
|
376
|
+
|
|
377
|
+
pos = self._pos
|
|
378
|
+
if self._records:
|
|
379
|
+
length = buf[pos] | buf[pos + 1] << 8
|
|
380
|
+
pos += 2
|
|
381
|
+
if length == _LOST:
|
|
382
|
+
# Packets are missing here, so the frame in progress is
|
|
383
|
+
# unusable and what follows may be the tail of a later
|
|
384
|
+
# one. Resynchronize at the next end of frame.
|
|
385
|
+
self._pos = pos
|
|
386
|
+
self.lost_packets += 1
|
|
387
|
+
n = 0
|
|
388
|
+
good = True
|
|
389
|
+
self._fid = None
|
|
390
|
+
continue
|
|
391
|
+
end = pos + length
|
|
392
|
+
else:
|
|
393
|
+
end = self._count
|
|
394
|
+
if end - pos < 2 or buf[pos] > end - pos:
|
|
395
|
+
self._pos = end
|
|
396
|
+
continue
|
|
397
|
+
header_len = buf[pos]
|
|
398
|
+
info = buf[pos + 1]
|
|
399
|
+
fid = info & _HDR_FID
|
|
400
|
+
if self._fid is None:
|
|
401
|
+
# Skip the rest of the frame in progress when streaming began.
|
|
402
|
+
self._pos = end
|
|
403
|
+
if info & _HDR_EOF:
|
|
404
|
+
self._fid = fid ^ 1
|
|
405
|
+
continue
|
|
406
|
+
if fid != self._fid:
|
|
407
|
+
self._fid = fid
|
|
408
|
+
if n:
|
|
409
|
+
# A new frame began without an EOF on the last one. Finish
|
|
410
|
+
# that frame and handle this payload on the next pass.
|
|
411
|
+
if good and self._complete(out, base, n, check_jpeg):
|
|
412
|
+
done = n
|
|
413
|
+
base += n
|
|
414
|
+
n = 0
|
|
415
|
+
good = True
|
|
416
|
+
# Only look for a newer frame when another one of
|
|
417
|
+
# this size fits behind it and reads return promptly.
|
|
418
|
+
if not self._records or 2 * done > size:
|
|
419
|
+
break
|
|
420
|
+
continue
|
|
421
|
+
n = 0
|
|
422
|
+
good = True
|
|
423
|
+
continue
|
|
424
|
+
self._pos = end
|
|
425
|
+
if info & _HDR_ERR:
|
|
426
|
+
good = False
|
|
427
|
+
data = end - pos - header_len
|
|
428
|
+
if data > 0 and good:
|
|
429
|
+
if base + n + data > size:
|
|
430
|
+
good = False
|
|
431
|
+
else:
|
|
432
|
+
# Assigning into a slice of a big buffer moves everything
|
|
433
|
+
# after the slice (about 19 ms for a 150 KB frame buffer);
|
|
434
|
+
# filling a view of just the right size does not.
|
|
435
|
+
out[base + n : base + n + data][:] = view[pos + header_len : end]
|
|
436
|
+
n += data
|
|
437
|
+
if info & _HDR_EOF:
|
|
438
|
+
self._fid = fid ^ 1
|
|
439
|
+
if good and self._complete(out, base, n, check_jpeg):
|
|
440
|
+
done = n
|
|
441
|
+
base += n
|
|
442
|
+
if not self._records or 2 * done > size:
|
|
443
|
+
n = 0
|
|
444
|
+
good = True
|
|
445
|
+
break
|
|
446
|
+
n = 0
|
|
447
|
+
good = True
|
|
448
|
+
# Move the complete frame to the start of the buffer, and the frame in
|
|
449
|
+
# progress right behind it.
|
|
450
|
+
start = base - done
|
|
451
|
+
if start:
|
|
452
|
+
out[0:done][:] = out[start:base]
|
|
453
|
+
if n:
|
|
454
|
+
out[done : done + n][:] = out[base : base + n]
|
|
455
|
+
base = done
|
|
456
|
+
self._n, self._base, self._good, self._done = n, base, good, 0
|
|
457
|
+
return out[:done]
|
|
458
|
+
|
|
459
|
+
def _complete(self, out, base, n, check_jpeg):
|
|
460
|
+
if n == 0:
|
|
461
|
+
return False
|
|
462
|
+
if check_jpeg and (n < 4 or out[base] != 0xFF or out[base + 1] != 0xD8):
|
|
463
|
+
self.dropped_frames += 1
|
|
464
|
+
return False
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
@staticmethod
|
|
468
|
+
def add_huffman_tables(frame):
|
|
469
|
+
"""Return an MJPEG frame as a standalone JPEG.
|
|
470
|
+
|
|
471
|
+
MJPEG cameras usually leave out the Huffman tables, which most decoders
|
|
472
|
+
and image viewers need. This inserts the standard ones when missing.
|
|
473
|
+
|
|
474
|
+
:param frame: the frame from `capture`
|
|
475
|
+
:return: ``bytes`` of a complete JPEG image
|
|
476
|
+
"""
|
|
477
|
+
if _has_dht(frame):
|
|
478
|
+
return bytes(frame)
|
|
479
|
+
return b"\xff\xd8" + _DHT + bytes(frame[2:])
|
|
480
|
+
|
|
481
|
+
def save_jpeg(self, path, frame=None):
|
|
482
|
+
"""Write an MJPEG frame to ``path`` as a standalone JPEG file.
|
|
483
|
+
|
|
484
|
+
:param str path: the file to write
|
|
485
|
+
:param frame: the frame from `capture`. The default captures a new
|
|
486
|
+
one, which needs the camera to be streaming.
|
|
487
|
+
"""
|
|
488
|
+
if frame is None:
|
|
489
|
+
frame = self.capture()
|
|
490
|
+
with open(path, "wb") as f:
|
|
491
|
+
if _has_dht(frame):
|
|
492
|
+
f.write(frame)
|
|
493
|
+
else:
|
|
494
|
+
f.write(b"\xff\xd8")
|
|
495
|
+
f.write(_DHT)
|
|
496
|
+
f.write(frame[2:])
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def bitmap(self):
|
|
500
|
+
"""A ``displayio.Bitmap`` of the camera image.
|
|
501
|
+
|
|
502
|
+
The first access captures a frame and creates the bitmap at its size;
|
|
503
|
+
later ones return the same bitmap unchanged. Call `update_bitmap` to
|
|
504
|
+
show a new frame in it. Show it with a
|
|
505
|
+
``displayio.ColorConverter(input_colorspace=displayio.Colorspace.RGB565_SWAPPED)``
|
|
506
|
+
pixel shader.
|
|
507
|
+
"""
|
|
508
|
+
if self._bitmap is None:
|
|
509
|
+
self.update_bitmap()
|
|
510
|
+
return self._bitmap
|
|
511
|
+
|
|
512
|
+
def update_bitmap(self):
|
|
513
|
+
"""Capture a frame with `capture` and decode it into `bitmap`,
|
|
514
|
+
creating the bitmap the first time. A later, larger mode is shrunk by
|
|
515
|
+
halves to fit.
|
|
516
|
+
|
|
517
|
+
Needs an MJPEG mode and the ``jpegio`` module.
|
|
518
|
+
|
|
519
|
+
:return: the `bitmap`
|
|
520
|
+
"""
|
|
521
|
+
mode = self._streaming
|
|
522
|
+
if mode is None:
|
|
523
|
+
raise RuntimeError("call start() first")
|
|
524
|
+
if mode.format != FORMAT_MJPEG:
|
|
525
|
+
raise ValueError("bitmap needs an MJPEG mode")
|
|
526
|
+
if jpegio is None:
|
|
527
|
+
raise RuntimeError("bitmap needs jpegio")
|
|
528
|
+
if self._decoder is None:
|
|
529
|
+
self._decoder = jpegio.JpegDecoder()
|
|
530
|
+
width, height = self._decoder.open(self.add_huffman_tables(self.capture()))
|
|
531
|
+
bitmap = self._bitmap
|
|
532
|
+
if bitmap is None:
|
|
533
|
+
bitmap = self._bitmap = displayio.Bitmap(width, height, 65536)
|
|
534
|
+
scale = 0
|
|
535
|
+
while (width >> scale) > bitmap.width or (height >> scale) > bitmap.height:
|
|
536
|
+
scale += 1
|
|
537
|
+
self._decoder.decode(bitmap, scale=scale)
|
|
538
|
+
return bitmap
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# The standard Huffman tables that MJPEG streams leave out (ITU-T T.81 K.3),
|
|
542
|
+
# as one DHT segment.
|
|
543
|
+
_DHT = (
|
|
544
|
+
b"\xff\xc4\x01\xa2"
|
|
545
|
+
b"\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00"
|
|
546
|
+
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b"
|
|
547
|
+
b"\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00"
|
|
548
|
+
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b"
|
|
549
|
+
b"\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01\x7d"
|
|
550
|
+
b"\x01\x02\x03\x00\x04\x11\x05\x12\x21\x31\x41\x06\x13\x51\x61\x07"
|
|
551
|
+
b"\x22\x71\x14\x32\x81\x91\xa1\x08\x23\x42\xb1\xc1\x15\x52\xd1\xf0"
|
|
552
|
+
b"\x24\x33\x62\x72\x82\x09\x0a\x16\x17\x18\x19\x1a\x25\x26\x27\x28"
|
|
553
|
+
b"\x29\x2a\x34\x35\x36\x37\x38\x39\x3a\x43\x44\x45\x46\x47\x48\x49"
|
|
554
|
+
b"\x4a\x53\x54\x55\x56\x57\x58\x59\x5a\x63\x64\x65\x66\x67\x68\x69"
|
|
555
|
+
b"\x6a\x73\x74\x75\x76\x77\x78\x79\x7a\x83\x84\x85\x86\x87\x88\x89"
|
|
556
|
+
b"\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7"
|
|
557
|
+
b"\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5"
|
|
558
|
+
b"\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2"
|
|
559
|
+
b"\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8"
|
|
560
|
+
b"\xf9\xfa"
|
|
561
|
+
b"\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02\x77"
|
|
562
|
+
b"\x00\x01\x02\x03\x11\x04\x05\x21\x31\x06\x12\x41\x51\x07\x61\x71"
|
|
563
|
+
b"\x13\x22\x32\x81\x08\x14\x42\x91\xa1\xb1\xc1\x09\x23\x33\x52\xf0"
|
|
564
|
+
b"\x15\x62\x72\xd1\x0a\x16\x24\x34\xe1\x25\xf1\x17\x18\x19\x1a\x26"
|
|
565
|
+
b"\x27\x28\x29\x2a\x35\x36\x37\x38\x39\x3a\x43\x44\x45\x46\x47\x48"
|
|
566
|
+
b"\x49\x4a\x53\x54\x55\x56\x57\x58\x59\x5a\x63\x64\x65\x66\x67\x68"
|
|
567
|
+
b"\x69\x6a\x73\x74\x75\x76\x77\x78\x79\x7a\x82\x83\x84\x85\x86\x87"
|
|
568
|
+
b"\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5"
|
|
569
|
+
b"\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3"
|
|
570
|
+
b"\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda"
|
|
571
|
+
b"\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf2\xf3\xf4\xf5\xf6\xf7\xf8"
|
|
572
|
+
b"\xf9\xfa"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _has_dht(frame):
|
|
577
|
+
"""Scan the JPEG marker segments before the scan data for a DHT."""
|
|
578
|
+
i = 2
|
|
579
|
+
n = len(frame)
|
|
580
|
+
while i + 4 <= n and frame[i] == 0xFF:
|
|
581
|
+
marker = frame[i + 1]
|
|
582
|
+
if marker == 0xC4:
|
|
583
|
+
return True
|
|
584
|
+
if marker == 0xDA:
|
|
585
|
+
return False
|
|
586
|
+
i += 2 + (frame[i + 2] << 8 | frame[i + 3])
|
|
587
|
+
return False
|