Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example showing how to capture and store images #45

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
00816be
Remove reference to controls when closing the device, they wouldn't
otaku42 May 7, 2023
43ae912
BooleanControl: convert value to lower-case before trying to coerce it
otaku42 May 7, 2023
4c2024e
Initial implementation of configuration file support
otaku42 May 11, 2023
8be4a26
Controls: add method named_keys, which returns the config_name of eac…
otaku42 May 11, 2023
3434df8
ConfigManager: add method validate, which tries to assert as good as …
otaku42 May 11, 2023
b164e12
Move ConfigManager to config.py
otaku42 May 11, 2023
0c6617d
Add and make use of more differentiated exceptions
otaku42 May 11, 2023
57f931e
Trivial cosmetics
otaku42 May 11, 2023
19e84ee
Take legacy_controls status into account, too
otaku42 May 11, 2023
7d8ef1d
Extend v4l2py-ctl to allow for saving and loading configurations
otaku42 May 11, 2023
5894dfb
Allow for listing all available video capture devices
otaku42 May 12, 2023
dfec3fb
Improve help text
otaku42 May 12, 2023
e20e852
Document configuration file support in README.md.
otaku42 May 13, 2023
567b7c9
Apply some black magic
otaku42 May 13, 2023
134dcbf
Merge branch 'tiagocoutinho:master' into improve_controls+config+snap…
otaku42 May 15, 2023
d32ee71
Merge pull request #28 from otaku42/improve_controls
otaku42 May 21, 2023
2ad0092
Merge branch 'tiagocoutinho:master' into improve_controls+config+snap…
otaku42 May 22, 2023
546c981
Initial implementation of an example tool to take a snapshot and save…
otaku42 May 24, 2023
c1275bb
Extend the v4l2py-snapshot.py example with some Pillow-based function…
otaku42 May 24, 2023
4a2a8a5
Reformatting by black to satisfy the CI tests
otaku42 May 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Format(width=640, height=480, pixelformat=<PixelFormat.MJPEG: 1196444237>}

v4l2py is asyncio friendly:

```bash
```python
$ python -m asyncio

>>> from v4l2py import Device
Expand All @@ -142,7 +142,7 @@ frame 10136

v4l2py is also gevent friendly:

```
```python
$ python

>>> from v4l2py import Device, GeventIO
Expand All @@ -158,6 +158,78 @@ frame 10136

(check [basic gevent](examples/basic_gevent.py) and [web gevent](examples/web/sync.py) examples)

## Configuration files

v4l2py now supports configuration files, allowing to save the current settings
(controls only at this time) of a device to a file:

```python
from v4l2py import Device
from v4l2py.config import ConfigManager

with Device.from_id(0) as cam:
cfg = ConfigManager(cam)
cfg.acquire()
cfg.save("cam.ini")
...
```

The configuration is written to an ini-style file, which might look like this:

```dosini
[device]
driver = uvcvideo
card = Integrated Camera: Integrated C
bus_info = usb-0000:00:14.0-8
version = 6.1.15
legacy_controls = False

[controls]
brightness = 128
contrast = 32
saturation = 64
hue = 0
white_balance_automatic = True
...
```

When loading a configuration file, the content may be validated to ensure it
fits the device it's going to be applied to, and after applying the
configuration it can be verified that the device is in the state that the
configuration file describes:

```python
from v4l2py import Device
from v4l2py.config import ConfigManager

with Device.from_id(0) as cam:
cfg = ConfigManager(cam)
cfg.load("cam.ini")
cfg.validate(pedantic=True)
cfg.apply()
cfg.verify()
```

[v4l2py-ctl](examples/v4l2py-ctl.py) can be used for that purpose, too:

```bash
$ python v4l2py-ctl.py --device /dev/video2 --reset-all
Resetting all controls to default ...

Done.
$ python v4l2py-ctl.py --device /dev/video2 --save cam-defaults.ini
Saving device configuration to /home/mrenzmann/src/v4l2py-o42/cam-defaults.ini

Done.
$
$ # ... after messing around with the controls ...
$ python v4l2py-ctl.py --device /dev/video2 --load cam-defaults.ini
Loading device configuration from /home/mrenzmann/src/v4l2py-o42/cam-defaults.ini

Done.
$
```

## Bonus track

You've been patient enough to read until here so, just for you,
Expand Down
131 changes: 118 additions & 13 deletions examples/v4l2py-ctl.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import argparse
import pathlib

from v4l2py.device import Device, MenuControl, LegacyControl
from v4l2py.device import iter_video_capture_devices, Capability
from v4l2py.config import ConfigManager


def _get_ctrl(cam, control):
Expand All @@ -17,6 +20,26 @@ def _get_ctrl(cam, control):
return ctrl


def list_devices() -> None:
print("Listing all video capture devices ...\n")
for dev in iter_video_capture_devices():
with dev as cam:
print(f"{cam.index:>2}: {cam.info.card}")
print(f"\tdriver : {cam.info.driver}")
print(f"\tversion : {cam.info.version}")
print(f"\tbus : {cam.info.bus_info}")
caps = [
cap.name.lower()
for cap in Capability
if ((cam.info.device_capabilities & cap) == cap)
]
if caps:
print("\tcaps :", ", ".join(caps))
else:
print("\tcaps : none")
print()


def show_control_status(device: str, legacy_controls: bool) -> None:
with Device(device, legacy_controls=legacy_controls) as cam:
print("Showing current status of all controls ...\n")
Expand Down Expand Up @@ -126,58 +149,134 @@ def reset_all_controls(device: str, legacy_controls: bool) -> None:
cam.controls.set_to_default()


def save_to_file(device: str, legacy_controls: bool, filename) -> None:
if isinstance(filename, pathlib.Path):
pass
elif isinstance(filename, str):
filename = pathlib.Path(filename)
else:
raise TypeError(
f"filename expected to be str or pathlib.Path, not {filename.__class__.__name__}"
)

with Device(device, legacy_controls) as cam:
print(f"Saving device configuration to {filename.resolve()}")
cfg = ConfigManager(cam)
cfg.acquire()
cfg.save(filename)
print("")


def load_from_file(
device: str, legacy_controls: bool, filename, pedantic: bool
) -> None:
if isinstance(filename, pathlib.Path):
pass
elif isinstance(filename, str):
filename = pathlib.Path(filename)
else:
raise TypeError(
f"filename expected to be str or pathlib.Path, not {filename.__class__.__name__}"
)

with Device(device, legacy_controls) as cam:
print(f"Loading device configuration from {filename.resolve()}")
cfg = ConfigManager(cam)
cfg.load(filename)
cfg.validate(pedantic=pedantic)
cfg.apply()
cfg.verify()
print("")


def csv(string: str) -> list:
return [v.strip() for v in string.split(",")]


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(
prog="v4l2py-ctl",
description="Example utility to control video capture devices.",
epilog="When no action is given, the control status of the selected device is shown.",
)
parser.add_argument(
"--device",
type=str,
default="0",
metavar="<dev>",
help="use device <dev> instead of /dev/video0; if <dev> starts with a digit, then /dev/video<dev> is used",
)

flags = parser.add_argument_group("Flags")
flags.add_argument(
"--legacy",
default=False,
action="store_true",
help="use legacy controls (default: %(default)s)",
)
parser.add_argument(
flags.add_argument(
"--clipping",
default=False,
action="store_true",
help="when changing numeric controls, enforce the written value to be within allowed range (default: %(default)s)",
)
parser.add_argument(
"--device",
type=str,
default="0",
metavar="<dev>",
help="use device <dev> instead of /dev/video0; if <dev> starts with a digit, then /dev/video<dev> is used",
flags.add_argument(
"--pedantic",
default=False,
action="store_true",
help="be pedantic when validating a loaded configuration (default: %(default)s)",
)
parser.add_argument(

actions = parser.add_argument_group("Actions")
actions.add_argument(
"--list-devices",
default=False,
action="store_true",
help="list all video capture devices",
)
actions.add_argument(
"--get-ctrl",
type=csv,
default=[],
metavar="<ctrl>[,<ctrl>...]",
help="get the values of the specified controls",
)
parser.add_argument(
actions.add_argument(
"--set-ctrl",
type=csv,
default=[],
metavar="<ctrl>=<val>[,<ctrl>=<val>...]",
help="set the values of the specified controls",
)
parser.add_argument(
actions.add_argument(
"--reset-ctrl",
type=csv,
default=[],
metavar="<ctrl>[,<ctrl>...]",
help="reset the specified controls to their default values",
)
parser.add_argument(
actions.add_argument(
"--reset-all",
default=False,
action="store_true",
help="reset all controls to their default value",
)
actions.add_argument(
"--save",
type=str,
dest="save_file",
default=None,
metavar="<filename>",
help="save current configuration to <filename>",
)
actions.add_argument(
"--load",
type=str,
dest="load_file",
default=None,
metavar="<filename>",
help="load configuration from <filename> and apply it to selected device",
)

args = parser.parse_args()

Expand All @@ -186,14 +285,20 @@ def csv(string: str) -> list:
else:
dev = args.device

if args.reset_all:
if args.list_devices:
list_devices()
elif args.reset_all:
reset_all_controls(dev, args.legacy)
elif args.reset_ctrl:
reset_controls(dev, args.reset_ctrl, args.legacy)
elif args.get_ctrl:
get_controls(dev, args.get_ctrl, args.legacy)
elif args.set_ctrl:
set_controls(dev, args.set_ctrl, args.legacy, args.clipping)
elif args.save_file is not None:
save_to_file(dev, args.legacy, args.save_file)
elif args.load_file is not None:
load_from_file(dev, args.legacy, args.load_file, args.pedantic)
else:
show_control_status(dev, args.legacy)

Expand Down
Loading