omnicam 0.1.0__tar.gz

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.
omnicam-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Oğuzhan
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.
omnicam-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: omnicam
3
+ Version: 0.1.0
4
+ Summary: A Python package for camera utilities
5
+ Author-email: OguzhanUmutlu <contact@oguzhanumutlu.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Oğuzhan
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/OguzhanUmutlu/omnicam
29
+ Project-URL: Issues, https://github.com/OguzhanUmutlu/omnicam/issues
30
+ Project-URL: Documentation, https://github.com/OguzhanUmutlu/omnicam#readme
31
+ Keywords: camera,opencv,vision,stream,capture
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.8
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Topic :: Multimedia :: Video
43
+ Classifier: Topic :: Software Development :: Libraries
44
+ Requires-Python: >=3.8
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Requires-Dist: numpy>=1.21
48
+ Provides-Extra: opencv
49
+ Requires-Dist: opencv-python>=4.7; extra == "opencv"
50
+ Provides-Extra: screen
51
+ Requires-Dist: mss>=9.0; extra == "screen"
52
+ Provides-Extra: pi
53
+ Requires-Dist: picamera2>=0.3; extra == "pi"
54
+ Provides-Extra: dev
55
+ Requires-Dist: pytest>=7.0; extra == "dev"
56
+ Dynamic: license-file
57
+
58
+ # omnicam
59
+
60
+ A small, unified API for reading frames from USB cameras, IP streams, video files, screen capture, and Raspberry Pi
61
+ cameras.
62
+
63
+ ## Features
64
+
65
+ - Unified `BaseCamera` interface across backends
66
+ - OpenCV-based capture for USB webcams, video files, RTSP/HTTP streams, and GStreamer pipelines
67
+ - Optional Raspberry Pi Camera support (Picamera2)
68
+ - Optional screen capture via `mss`
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install omnicam
74
+ ```
75
+
76
+ Optional extras:
77
+
78
+ ```bash
79
+ pip install omnicam[opencv]
80
+ pip install omnicam[screen]
81
+ pip install omnicam[pi]
82
+ ```
83
+
84
+ ## Quickstart
85
+
86
+ ```python
87
+ from omnicam import SimpleCamera
88
+
89
+ with SimpleCamera(index=0) as cam:
90
+ cam.open()
91
+ frame = cam.read()
92
+ if frame is not None:
93
+ print(frame.shape)
94
+ ```
95
+
96
+ ## Camera types
97
+
98
+ ```python
99
+ from omnicam import FileCapture, InternetCapture, ScreenCapture
100
+
101
+ # Video file
102
+ cam = FileCapture("/path/to/video.mp4")
103
+
104
+ # IP camera or MJPEG stream
105
+ cam = InternetCapture("http://192.168.1.10:8080/video")
106
+
107
+ # Screen capture (requires omnicam[screen])
108
+ cam = ScreenCapture(index=1)
109
+ ```
110
+
111
+ ## Raspberry Pi camera
112
+
113
+ ```python
114
+ from omnicam import PiCamera
115
+
116
+ cam = PiCamera(model="IMX219", resolution="720p")
117
+ ```
118
+
119
+ ## GStreamer and Gazebo
120
+
121
+ ```python
122
+ from omnicam import GStreamerCapture, GazeboCamera
123
+
124
+ pipeline = (
125
+ GStreamerCapture.GstPipeline()
126
+ .add("v4l2src", device="/dev/video0")
127
+ .add("videoconvert")
128
+ .add("appsink")
129
+ )
130
+ cam = GStreamerCapture(pipeline)
131
+
132
+ # Gazebo (requires gz tools available on PATH)
133
+ cam = GazeboCamera(topic_name="my_camera")
134
+ ```
135
+
136
+ ## Notes
137
+
138
+ - `numpy` is required. OpenCV-based backends need `omnicam[opencv]`.
139
+ - GStreamer and Gazebo features rely on system-level tooling.
140
+
141
+ ## License
142
+
143
+ MIT. See `LICENSE`.
@@ -0,0 +1,86 @@
1
+ # omnicam
2
+
3
+ A small, unified API for reading frames from USB cameras, IP streams, video files, screen capture, and Raspberry Pi
4
+ cameras.
5
+
6
+ ## Features
7
+
8
+ - Unified `BaseCamera` interface across backends
9
+ - OpenCV-based capture for USB webcams, video files, RTSP/HTTP streams, and GStreamer pipelines
10
+ - Optional Raspberry Pi Camera support (Picamera2)
11
+ - Optional screen capture via `mss`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install omnicam
17
+ ```
18
+
19
+ Optional extras:
20
+
21
+ ```bash
22
+ pip install omnicam[opencv]
23
+ pip install omnicam[screen]
24
+ pip install omnicam[pi]
25
+ ```
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ from omnicam import SimpleCamera
31
+
32
+ with SimpleCamera(index=0) as cam:
33
+ cam.open()
34
+ frame = cam.read()
35
+ if frame is not None:
36
+ print(frame.shape)
37
+ ```
38
+
39
+ ## Camera types
40
+
41
+ ```python
42
+ from omnicam import FileCapture, InternetCapture, ScreenCapture
43
+
44
+ # Video file
45
+ cam = FileCapture("/path/to/video.mp4")
46
+
47
+ # IP camera or MJPEG stream
48
+ cam = InternetCapture("http://192.168.1.10:8080/video")
49
+
50
+ # Screen capture (requires omnicam[screen])
51
+ cam = ScreenCapture(index=1)
52
+ ```
53
+
54
+ ## Raspberry Pi camera
55
+
56
+ ```python
57
+ from omnicam import PiCamera
58
+
59
+ cam = PiCamera(model="IMX219", resolution="720p")
60
+ ```
61
+
62
+ ## GStreamer and Gazebo
63
+
64
+ ```python
65
+ from omnicam import GStreamerCapture, GazeboCamera
66
+
67
+ pipeline = (
68
+ GStreamerCapture.GstPipeline()
69
+ .add("v4l2src", device="/dev/video0")
70
+ .add("videoconvert")
71
+ .add("appsink")
72
+ )
73
+ cam = GStreamerCapture(pipeline)
74
+
75
+ # Gazebo (requires gz tools available on PATH)
76
+ cam = GazeboCamera(topic_name="my_camera")
77
+ ```
78
+
79
+ ## Notes
80
+
81
+ - `numpy` is required. OpenCV-based backends need `omnicam[opencv]`.
82
+ - GStreamer and Gazebo features rely on system-level tooling.
83
+
84
+ ## License
85
+
86
+ MIT. See `LICENSE`.
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "omnicam"
7
+ version = "0.1.0"
8
+ description = "A Python package for camera utilities"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [
12
+ { name = "OguzhanUmutlu", email = "contact@oguzhanumutlu.com" }
13
+ ]
14
+ requires-python = ">=3.8"
15
+ dependencies = [
16
+ "numpy>=1.21"
17
+ ]
18
+ keywords = ["camera", "opencv", "vision", "stream", "capture"]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.8",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Topic :: Multimedia :: Video",
31
+ "Topic :: Software Development :: Libraries",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ opencv = ["opencv-python>=4.7"]
36
+ screen = ["mss>=9.0"]
37
+ pi = ["picamera2>=0.3"]
38
+ dev = ["pytest>=7.0"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/OguzhanUmutlu/omnicam"
42
+ Issues = "https://github.com/OguzhanUmutlu/omnicam/issues"
43
+ Documentation = "https://github.com/OguzhanUmutlu/omnicam#readme"
44
+
45
+ [tool.setuptools]
46
+ package-dir = {"" = "src"}
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .base_camera import BaseCamera
4
+ from .cv_camera_base import CVCameraBase
5
+ from .file_capture import FileCapture
6
+ from .gazebo_camera import GazeboCamera
7
+ from .gstreamer_capture import GStreamerCapture
8
+ from .internet_capture import InternetCapture
9
+ from .pi_camera import PiCamera
10
+ from .readonly_camera import ReadonlyCamera
11
+ from .screen_capture import ScreenCapture
12
+ from .simple_camera import SimpleCamera
13
+
14
+ try:
15
+ __version__ = version("omnicam")
16
+ except PackageNotFoundError:
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "BaseCamera",
21
+ "CVCameraBase",
22
+ "FileCapture",
23
+ "GazeboCamera",
24
+ "GStreamerCapture",
25
+ "InternetCapture",
26
+ "PiCamera",
27
+ "ReadonlyCamera",
28
+ "ScreenCapture",
29
+ "SimpleCamera",
30
+ "__version__",
31
+ ]
@@ -0,0 +1,161 @@
1
+ from abc import abstractmethod, ABC
2
+ from typing import Optional, Literal
3
+
4
+ import numpy as np
5
+
6
+ resolutions = {
7
+ "240p": (426, 240),
8
+ "360p": (640, 360),
9
+ "480p": (854, 480),
10
+ "720p": (1280, 720),
11
+ "HD": (1280, 720),
12
+ "1080p": (1920, 1080),
13
+ "HD+": (1920, 1080),
14
+ "1440p": (2560, 1440),
15
+ "2160p": (3840, 2160)
16
+ }
17
+
18
+
19
+ class CameraInfo:
20
+ def __init__(
21
+ self,
22
+ name: str,
23
+ short_name: str,
24
+ focal_length_mm: tuple[float, float],
25
+ pixel_size_um: float,
26
+ max_resolution: tuple[int, int],
27
+ aperture_f: float,
28
+ hfov_deg: float,
29
+ focus_type: Literal["Fixed", "Autofocus", "Manual", "Unknown"],
30
+ has_ir_filter: bool
31
+ ):
32
+ self.name = name
33
+ self.short_name = short_name
34
+ self.focal_length_mm = focal_length_mm
35
+ self.pixel_size_um = pixel_size_um
36
+ self.max_resolution = max_resolution
37
+ self.aperture_f = aperture_f
38
+ self.hfov_deg = hfov_deg
39
+ self.focus_type = focus_type
40
+ self.has_ir_filter = has_ir_filter
41
+
42
+ pixel_size_mm = self.pixel_size_um / 1000.0
43
+ self.base_focal_length = (self.focal_length_mm[0] / pixel_size_mm, self.focal_length_mm[1] / pixel_size_mm)
44
+
45
+ def focal_length(self, resolution: tuple[int, int]):
46
+ return (
47
+ self.base_focal_length[0] * resolution[0] / self.max_resolution[0],
48
+ self.base_focal_length[1] * resolution[1] / self.max_resolution[1]
49
+ )
50
+
51
+ @staticmethod
52
+ def from_focal_length(name: str, short_name: str, focal_length: tuple[float, float],
53
+ resolution: tuple[int, int] = (9999, 9999)):
54
+ pixel_size_mm = 0.001
55
+ focal_length_mm = (focal_length[0] * pixel_size_mm, focal_length[1] * pixel_size_mm)
56
+ return CameraInfo(
57
+ name=name,
58
+ short_name=short_name,
59
+ focal_length_mm=focal_length_mm,
60
+ pixel_size_um=pixel_size_mm * 1000,
61
+ max_resolution=resolution,
62
+ aperture_f=0.0,
63
+ hfov_deg=0.0,
64
+ focus_type="Unknown",
65
+ has_ir_filter=False
66
+ )
67
+
68
+
69
+ class BaseCamera(ABC):
70
+ def __init__(self, info: Optional[CameraInfo] = None):
71
+ self.closed = False
72
+ self.roll_deg = 0.0
73
+ self.pitch_deg = 0.0
74
+ self.yaw_deg = 0.0
75
+ self.offset = np.array([0.0, 0.0, -0.1], dtype=np.float64)
76
+ self.info = info
77
+
78
+ @abstractmethod
79
+ def _open(self):
80
+ raise NotImplementedError("open method must be implemented by subclass")
81
+
82
+ @abstractmethod
83
+ def _read(self) -> "np.ndarray | None":
84
+ raise NotImplementedError("read method must be implemented by subclass")
85
+
86
+ @abstractmethod
87
+ def _release(self):
88
+ raise NotImplementedError("release method must be implemented by subclass")
89
+
90
+ @abstractmethod
91
+ def _size(self) -> tuple[int, int]:
92
+ raise NotImplementedError("size method must be implemented by subclass")
93
+
94
+ def _focus(self, rectangle: tuple[int, int, int, int]):
95
+ raise NotImplementedError("focus method must be implemented by subclass")
96
+
97
+ def open(self):
98
+ if self.closed:
99
+ raise PermissionError("Attempted to open a closed camera.")
100
+ self._open()
101
+ return self
102
+
103
+ def size(self):
104
+ return self._size()
105
+
106
+ def focus(self, rectangle: tuple[int, int, int, int]):
107
+ if self.closed:
108
+ raise PermissionError("Attempted to focus a closed camera.")
109
+ if self.info is not None and self.info.focus_type in ("Fixed", "Unknown"):
110
+ raise ValueError(f"This camera has a {self.info.focus_type} focus and cannot be adjusted.")
111
+ self._focus(rectangle)
112
+ return self
113
+
114
+ def focal_length(self) -> tuple[float, float]:
115
+ if self.info is None:
116
+ raise ValueError("CameraInfo must be provided to calculate focal length")
117
+ return self.info.focal_length(self.size())
118
+
119
+ def width(self):
120
+ return self.size()[0]
121
+
122
+ def height(self):
123
+ return self.size()[1]
124
+
125
+ def fx(self):
126
+ return self.focal_length()[0]
127
+
128
+ def fy(self):
129
+ return self.focal_length()[1]
130
+
131
+ def read(self):
132
+ if self.closed:
133
+ raise PermissionError("Attempted to read from a closed camera.")
134
+ frame = self._read()
135
+ if frame is None:
136
+ self.closed = True
137
+ return frame
138
+
139
+ def release(self):
140
+ if not self.closed:
141
+ self.closed = True
142
+ self._release()
143
+
144
+ def frames(self):
145
+ while True:
146
+ frame = self.read()
147
+ if frame is None:
148
+ break
149
+ yield frame
150
+
151
+ def __enter__(self):
152
+ return self
153
+
154
+ def __exit__(self, exc_type, exc_val, exc_tb):
155
+ self.release()
156
+
157
+ def readonly(self):
158
+ from .readonly_camera import ReadonlyCamera
159
+ if isinstance(self, ReadonlyCamera):
160
+ return self
161
+ return ReadonlyCamera(self)
@@ -0,0 +1,44 @@
1
+ from .base_camera import BaseCamera, CameraInfo
2
+
3
+
4
+ class CVCameraBase(BaseCamera):
5
+ def __init__(self, args: list, timeout=None, info: CameraInfo = None, open_error=None, timeout_error=None,
6
+ **kwargs):
7
+ super().__init__(info=info)
8
+ try:
9
+ import cv2
10
+ except Exception as exc:
11
+ raise ImportError(
12
+ "OpenCV is required for this feature. Install with 'pip install omnicam[opencv]'.") from exc
13
+ self._cv2 = cv2
14
+ self.open_error = open_error or RuntimeError(f"Camera could not be opened with args {args}")
15
+ self.timeout_error = timeout_error or TimeoutError(
16
+ f"Failed to open camera with args {args} within {timeout} seconds")
17
+ self.cv_args = args
18
+ self.cv_kwargs = kwargs
19
+ self.cam = None
20
+ self._frame_size = None
21
+ self.timeout = timeout
22
+
23
+ def _open(self):
24
+ self.cam = self._cv2.VideoCapture(*self.cv_args, **self.cv_kwargs)
25
+ if self.timeout is not None:
26
+ start_time = self._cv2.getTickCount()
27
+ while not self.cam.isOpened():
28
+ elapsed_time = (self._cv2.getTickCount() - start_time) / self._cv2.getTickFrequency()
29
+ if elapsed_time > self.timeout:
30
+ raise self.timeout_error
31
+ elif not self.cam.isOpened():
32
+ raise self.open_error
33
+ self._frame_size = int(self.cam.get(self._cv2.CAP_PROP_FRAME_WIDTH)), int(
34
+ self.cam.get(self._cv2.CAP_PROP_FRAME_HEIGHT))
35
+
36
+ def _read(self):
37
+ ret, frame = self.cam.read()
38
+ return frame if ret else None
39
+
40
+ def _release(self):
41
+ self.cam.release()
42
+
43
+ def _size(self):
44
+ return self._frame_size
@@ -0,0 +1,12 @@
1
+ import os
2
+
3
+ from .base_camera import CameraInfo
4
+ from .cv_camera_base import CVCameraBase
5
+
6
+
7
+ class FileCapture(CVCameraBase):
8
+ def __init__(self, path: "os.PathLike | str", info: CameraInfo = None, open_error=None):
9
+ if not os.path.exists(path):
10
+ raise ValueError(f"File {path} does not exist")
11
+ super().__init__([str(path)], info=info,
12
+ open_error=open_error or ValueError(f"Could not open video file {path}"))
@@ -0,0 +1,39 @@
1
+ import subprocess
2
+
3
+ from .base_camera import CameraInfo
4
+ from .gstreamer_capture import GStreamerCapture
5
+
6
+
7
+ class GazeboCamera(GStreamerCapture):
8
+ def __init__(self, port=4000, topic_name: str = None, timeout=5, info: CameraInfo = None, open_error=None,
9
+ timeout_error=None):
10
+ self.topic_name = topic_name
11
+ self.port = port
12
+ super().__init__(
13
+ GStreamerCapture.GstPipeline()
14
+ .add("udpsrc", port=self.port)
15
+ .add_caps("application/x-rtp")
16
+ .add("rtph264depay")
17
+ .add("h264parse")
18
+ .add("avdec_h264")
19
+ .add("videoconvert")
20
+ .add("appsink", sync="false"),
21
+ timeout=timeout, info=info,
22
+ open_error=open_error or ValueError(f"Could not open Gazebo camera stream on port {self.port}"),
23
+ timeout_error=timeout_error or TimeoutError(
24
+ f"Could not open Gazebo camera stream on port {self.port} within {timeout} seconds")
25
+ )
26
+
27
+ def _open(self):
28
+ if self.topic_name is not None:
29
+ GazeboCamera.start_gz_stream(self.topic_name)
30
+ super()._open()
31
+
32
+ @staticmethod
33
+ def start_gz_stream(topic_name):
34
+ subprocess.run([
35
+ "gz", "topic",
36
+ "-t", f"/{topic_name}/image/enable_streaming",
37
+ "-m", "gz.msgs.Boolean",
38
+ "-p", "data: true"
39
+ ])
@@ -0,0 +1,42 @@
1
+ from .base_camera import CameraInfo
2
+ from .cv_camera_base import CVCameraBase
3
+
4
+
5
+ class GStreamerCapture(CVCameraBase):
6
+ class GstPipeline:
7
+ def __init__(self):
8
+ self.elements = []
9
+
10
+ def add(self, element_name, **properties):
11
+ if not properties:
12
+ self.elements.append(element_name)
13
+ return self
14
+ props_str = " ".join(f"{k}={v}" for k, v in properties.items())
15
+ element = f"{element_name} {props_str}".strip()
16
+ self.elements.append(element)
17
+ return self
18
+
19
+ def add_caps(self, caps: str):
20
+ self.elements.append(caps)
21
+ return self
22
+
23
+ def __str__(self):
24
+ return " ! ".join(self.elements)
25
+
26
+ def __init__(self, gstreamer_string: "GstPipeline | str", timeout=5, info: CameraInfo = None, open_error=None,
27
+ timeout_error=None):
28
+ try:
29
+ import cv2
30
+ except Exception as exc:
31
+ raise ImportError(
32
+ "OpenCV is required for this feature. Install with 'pip install omnicam[opencv]'."
33
+ ) from exc
34
+ gstreamer_string = str(gstreamer_string)
35
+ self.gstreamer_string = gstreamer_string
36
+ super().__init__(
37
+ [gstreamer_string, cv2.CAP_GSTREAMER],
38
+ timeout=timeout, info=info,
39
+ open_error=open_error or RuntimeError(f"Could not open GStreamer stream with \"{gstreamer_string}\""),
40
+ timeout_error=timeout_error or TimeoutError(
41
+ f"Could not open GStreamer stream with \"{gstreamer_string}\" within {timeout} seconds")
42
+ )
@@ -0,0 +1,20 @@
1
+ from .base_camera import CameraInfo
2
+ from .cv_camera_base import CVCameraBase
3
+
4
+
5
+ class InternetCapture(CVCameraBase):
6
+ def __init__(self, url: str, timeout=5, info: CameraInfo = None, args: list = None,
7
+ open_error=None, timeout_error=None):
8
+ if not url.startswith("http://") and not url.startswith("https://") and not url.startswith("rtsp://"):
9
+ url = "http://" + url
10
+ domain = url.split("//")[1].split("/")[0]
11
+ if ":" not in domain:
12
+ url = url.replace(domain, domain + ":8080")
13
+ if "/" not in url.split("//")[1]:
14
+ url += "/video"
15
+ super().__init__(
16
+ [url, *(args or [])], timeout=timeout, info=info,
17
+ open_error=open_error or ValueError(f"Could not open video stream from URL {url}"),
18
+ timeout_error=timeout_error or TimeoutError(
19
+ f"Could not open video stream from URL {url} within {timeout} seconds")
20
+ )
@@ -0,0 +1,138 @@
1
+ from typing import Union
2
+
3
+ from .base_camera import BaseCamera, resolutions, CameraInfo
4
+
5
+
6
+ class PiCamera(BaseCamera):
7
+ rpi_camera_definitions = [
8
+ CameraInfo(
9
+ name="Camera Module 1", short_name="OV5647",
10
+ focal_length_mm=(3.60, 3.60), pixel_size_um=1.40, max_resolution=(2592, 1944),
11
+ aperture_f=2.9, hfov_deg=53.5, focus_type="Fixed", has_ir_filter=True
12
+ ),
13
+ CameraInfo(
14
+ name="Camera Module 1 NoIR", short_name="OV5647 NoIR",
15
+ focal_length_mm=(3.60, 3.60), pixel_size_um=1.40, max_resolution=(2592, 1944),
16
+ aperture_f=2.9, hfov_deg=53.5, focus_type="Fixed", has_ir_filter=False
17
+ ),
18
+ CameraInfo(
19
+ name="Camera Module 2", short_name="IMX219",
20
+ focal_length_mm=(3.04, 3.04), pixel_size_um=1.12, max_resolution=(3280, 2464),
21
+ aperture_f=2.0, hfov_deg=62.2, focus_type="Fixed", has_ir_filter=True
22
+ ),
23
+ CameraInfo(
24
+ name="Camera Module 2 NoIR", short_name="IMX219 NoIR",
25
+ focal_length_mm=(3.04, 3.04), pixel_size_um=1.12, max_resolution=(3280, 2464),
26
+ aperture_f=2.0, hfov_deg=62.2, focus_type="Fixed", has_ir_filter=False
27
+ ),
28
+ CameraInfo(
29
+ name="Camera Module 3 - Standard", short_name="IMX708 Standard",
30
+ focal_length_mm=(4.74, 4.74), pixel_size_um=1.40, max_resolution=(4608, 2592),
31
+ aperture_f=1.8, hfov_deg=66.0, focus_type="Autofocus", has_ir_filter=True
32
+ ),
33
+ CameraInfo(
34
+ name="Camera Module 3 - Standard NoIR", short_name="IMX708 NoIR",
35
+ focal_length_mm=(4.74, 4.74), pixel_size_um=1.40, max_resolution=(4608, 2592),
36
+ aperture_f=1.8, hfov_deg=66.0, focus_type="Autofocus", has_ir_filter=False
37
+ ),
38
+ CameraInfo(
39
+ name="Camera Module 3 - Wide", short_name="IMX708 Wide",
40
+ focal_length_mm=(2.75, 2.75), pixel_size_um=1.40, max_resolution=(4608, 2592),
41
+ aperture_f=2.2, hfov_deg=102.0, focus_type="Autofocus", has_ir_filter=True
42
+ ),
43
+ CameraInfo(
44
+ name="Camera Module 3 - Wide NoIR", short_name="IMX708 Wide NoIR",
45
+ focal_length_mm=(2.75, 2.75), pixel_size_um=1.40, max_resolution=(4608, 2592),
46
+ aperture_f=2.2, hfov_deg=102.0, focus_type="Autofocus", has_ir_filter=False
47
+ ),
48
+ CameraInfo(
49
+ name="High Quality Camera w/ 6mm Lens", short_name="IMX477 6mm",
50
+ focal_length_mm=(6.00, 6.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
51
+ aperture_f=1.2, hfov_deg=55.0, focus_type="Manual", has_ir_filter=True
52
+ ),
53
+ CameraInfo(
54
+ name="High Quality Camera w/ 16mm Lens", short_name="IMX477 16mm",
55
+ focal_length_mm=(16.00, 16.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
56
+ aperture_f=1.4, hfov_deg=22.2, focus_type="Manual", has_ir_filter=True
57
+ ),
58
+ CameraInfo(
59
+ name="High Quality Camera w/ 35mm Lens", short_name="IMX477 35mm",
60
+ focal_length_mm=(35.00, 35.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
61
+ aperture_f=1.7, hfov_deg=10.1, focus_type="Manual", has_ir_filter=True
62
+ ),
63
+ CameraInfo(
64
+ name="High Quality Camera w/ 8mm M12 Lens", short_name="IMX477 M12-8mm",
65
+ focal_length_mm=(8.00, 8.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
66
+ aperture_f=1.8, hfov_deg=49.0, focus_type="Manual", has_ir_filter=True
67
+ ),
68
+ CameraInfo(
69
+ name="High Quality Camera w/ 25mm M12 Lens", short_name="IMX477 M12-25mm",
70
+ focal_length_mm=(25.00, 25.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
71
+ aperture_f=2.4, hfov_deg=14.4, focus_type="Manual", has_ir_filter=True
72
+ ),
73
+ CameraInfo(
74
+ name="High Quality Camera w/ 2.7mm M12 Fisheye", short_name="IMX477 M12-Fish",
75
+ focal_length_mm=(2.70, 2.70), pixel_size_um=1.55, max_resolution=(4056, 3040),
76
+ aperture_f=2.5, hfov_deg=140.0, focus_type="Manual", has_ir_filter=True
77
+ ),
78
+ CameraInfo(
79
+ name="Global Shutter Camera w/ 6mm Lens", short_name="IMX296 6mm",
80
+ focal_length_mm=(6.00, 6.00), pixel_size_um=3.45, max_resolution=(1456, 1088),
81
+ aperture_f=1.2, hfov_deg=45.0, focus_type="Manual", has_ir_filter=True
82
+ ),
83
+ CameraInfo(
84
+ name="Global Shutter Camera w/ 16mm Lens", short_name="IMX296 16mm",
85
+ focal_length_mm=(16.00, 16.00), pixel_size_um=3.45, max_resolution=(1456, 1088),
86
+ aperture_f=1.4, hfov_deg=17.8, focus_type="Manual", has_ir_filter=True
87
+ ),
88
+ CameraInfo(
89
+ name="Raspberry Pi AI Camera", short_name="IMX500",
90
+ focal_length_mm=(4.74, 4.74), pixel_size_um=1.55, max_resolution=(4056, 3040),
91
+ aperture_f=1.79, hfov_deg=66.3, focus_type="Manual", has_ir_filter=True
92
+ )
93
+ ]
94
+
95
+ rpi_cameras = {}
96
+ for cam_info in rpi_camera_definitions:
97
+ rpi_cameras[cam_info.name] = cam_info
98
+ rpi_cameras[cam_info.short_name] = cam_info
99
+
100
+ def __init__(self, model: Union[str, CameraInfo], resolution: "tuple[int, int] | str" = "720p"):
101
+ super().__init__()
102
+ if not isinstance(model, CameraInfo):
103
+ if model not in PiCamera.rpi_cameras:
104
+ raise ValueError(f"Unsupported: {model}. Supported models: {', '.join(PiCamera.rpi_cameras.keys())}")
105
+ if isinstance(resolution, str) and resolution not in resolutions and resolution not in resolutions.values():
106
+ raise ValueError(
107
+ f"Unsupported resolution: {resolution}. Supported resolutions: {', '.join(str(r) for r in resolutions.values())}")
108
+ model = PiCamera.rpi_cameras[model]
109
+ if isinstance(resolution, str):
110
+ resolution = resolutions[resolution]
111
+ self.resolution = resolution
112
+ self.model = model
113
+
114
+ def _open(self):
115
+ try:
116
+ from picamera2 import Picamera2
117
+ except Exception as e:
118
+ raise ImportError("PiCamera could not be initialized. Is picamera2 installed?") from e
119
+ self.cam = Picamera2()
120
+ self.cam.preview_configuration.main.format = "RGB888"
121
+ self.cam.preview_configuration.main.size = self.resolution
122
+ self.cam.configure("preview")
123
+ self.cam.start()
124
+
125
+ def _read(self):
126
+ return self.cam.capture_array()
127
+
128
+ def _release(self):
129
+ if not self.closed:
130
+ self.closed = True
131
+ self.cam.stop()
132
+ self.cam.close()
133
+
134
+ def _size(self):
135
+ return self.resolution
136
+
137
+ def focal_length(self):
138
+ return self.model.focal_length(self.size())
@@ -0,0 +1,25 @@
1
+ from .base_camera import BaseCamera
2
+
3
+
4
+ class ReadonlyCamera(BaseCamera):
5
+ def __init__(self, cam: BaseCamera):
6
+ super().__init__()
7
+ self.cam = cam
8
+
9
+ def _open(self):
10
+ self.cam._open()
11
+
12
+ def _read(self):
13
+ return self.cam._read()
14
+
15
+ def _release(self):
16
+ pass
17
+
18
+ def _size(self):
19
+ return self.cam._size()
20
+
21
+ def _focus(self, rectangle: tuple[int, int, int, int]):
22
+ self.cam._focus(rectangle)
23
+
24
+ def focal_length(self):
25
+ return self.cam.focal_length()
@@ -0,0 +1,45 @@
1
+ import numpy as np
2
+
3
+ from .base_camera import BaseCamera
4
+
5
+
6
+ class ScreenCapture(BaseCamera):
7
+ # region: {"top": int, "left": int, "width": int, "height": int}
8
+ def __init__(self, index=1, region: "dict[str, int] | None" = None,
9
+ focal_length: "tuple[float, float] | None" = None):
10
+ try:
11
+ import mss
12
+ self.mss = mss
13
+ except Exception as e:
14
+ raise ImportError("mss is required for this feature. Install with 'pip install omnicam[screen]'.") from e
15
+ super().__init__()
16
+ self.index = index
17
+ self.region = region
18
+ self._focal_length = focal_length
19
+
20
+ def _open(self):
21
+ self.cap = self.mss.mss()
22
+ self.monitor = self.cap.monitors[self.index]
23
+ self.region = self.region if self.region is not None else self.monitor
24
+
25
+ def _read(self):
26
+ return np.array(self.cap.grab(
27
+ {
28
+ "top": self.monitor["top"] + self.region["top"],
29
+ "left": self.monitor["left"] + self.region["left"],
30
+ "width": self.region["width"],
31
+ "height": self.region["height"]
32
+ } if self.region is not None else self.monitor))
33
+
34
+ def _release(self):
35
+ self.cap.close()
36
+
37
+ def _size(self):
38
+ if self.region is not None:
39
+ return self.region["width"], self.region["height"]
40
+ return self.monitor["width"], self.monitor["height"]
41
+
42
+ def focal_length(self):
43
+ if self._focal_length is None:
44
+ raise ValueError("Focal length not set for ScreenCapture")
45
+ return self._focal_length
@@ -0,0 +1,20 @@
1
+ import platform
2
+
3
+ from .base_camera import CameraInfo
4
+ from .cv_camera_base import CVCameraBase
5
+
6
+
7
+ class SimpleCamera(CVCameraBase):
8
+ def __init__(self, index=0, info: CameraInfo = None):
9
+ try:
10
+ import cv2
11
+ except Exception as exc:
12
+ raise ImportError(
13
+ "OpenCV is required for this feature. Install with 'pip install omnicam[opencv]'."
14
+ ) from exc
15
+ args = [index, cv2.CAP_DSHOW] if platform.system() == "Windows" else [index, cv2.CAP_V4L2]
16
+ try:
17
+ super().__init__(args, info=info)
18
+ except Exception as e:
19
+ raise RuntimeError(f"Camera with index {index} could not be opened: {e}") from e
20
+ self.index = index
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: omnicam
3
+ Version: 0.1.0
4
+ Summary: A Python package for camera utilities
5
+ Author-email: OguzhanUmutlu <contact@oguzhanumutlu.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Oğuzhan
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/OguzhanUmutlu/omnicam
29
+ Project-URL: Issues, https://github.com/OguzhanUmutlu/omnicam/issues
30
+ Project-URL: Documentation, https://github.com/OguzhanUmutlu/omnicam#readme
31
+ Keywords: camera,opencv,vision,stream,capture
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.8
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Topic :: Multimedia :: Video
43
+ Classifier: Topic :: Software Development :: Libraries
44
+ Requires-Python: >=3.8
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Requires-Dist: numpy>=1.21
48
+ Provides-Extra: opencv
49
+ Requires-Dist: opencv-python>=4.7; extra == "opencv"
50
+ Provides-Extra: screen
51
+ Requires-Dist: mss>=9.0; extra == "screen"
52
+ Provides-Extra: pi
53
+ Requires-Dist: picamera2>=0.3; extra == "pi"
54
+ Provides-Extra: dev
55
+ Requires-Dist: pytest>=7.0; extra == "dev"
56
+ Dynamic: license-file
57
+
58
+ # omnicam
59
+
60
+ A small, unified API for reading frames from USB cameras, IP streams, video files, screen capture, and Raspberry Pi
61
+ cameras.
62
+
63
+ ## Features
64
+
65
+ - Unified `BaseCamera` interface across backends
66
+ - OpenCV-based capture for USB webcams, video files, RTSP/HTTP streams, and GStreamer pipelines
67
+ - Optional Raspberry Pi Camera support (Picamera2)
68
+ - Optional screen capture via `mss`
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install omnicam
74
+ ```
75
+
76
+ Optional extras:
77
+
78
+ ```bash
79
+ pip install omnicam[opencv]
80
+ pip install omnicam[screen]
81
+ pip install omnicam[pi]
82
+ ```
83
+
84
+ ## Quickstart
85
+
86
+ ```python
87
+ from omnicam import SimpleCamera
88
+
89
+ with SimpleCamera(index=0) as cam:
90
+ cam.open()
91
+ frame = cam.read()
92
+ if frame is not None:
93
+ print(frame.shape)
94
+ ```
95
+
96
+ ## Camera types
97
+
98
+ ```python
99
+ from omnicam import FileCapture, InternetCapture, ScreenCapture
100
+
101
+ # Video file
102
+ cam = FileCapture("/path/to/video.mp4")
103
+
104
+ # IP camera or MJPEG stream
105
+ cam = InternetCapture("http://192.168.1.10:8080/video")
106
+
107
+ # Screen capture (requires omnicam[screen])
108
+ cam = ScreenCapture(index=1)
109
+ ```
110
+
111
+ ## Raspberry Pi camera
112
+
113
+ ```python
114
+ from omnicam import PiCamera
115
+
116
+ cam = PiCamera(model="IMX219", resolution="720p")
117
+ ```
118
+
119
+ ## GStreamer and Gazebo
120
+
121
+ ```python
122
+ from omnicam import GStreamerCapture, GazeboCamera
123
+
124
+ pipeline = (
125
+ GStreamerCapture.GstPipeline()
126
+ .add("v4l2src", device="/dev/video0")
127
+ .add("videoconvert")
128
+ .add("appsink")
129
+ )
130
+ cam = GStreamerCapture(pipeline)
131
+
132
+ # Gazebo (requires gz tools available on PATH)
133
+ cam = GazeboCamera(topic_name="my_camera")
134
+ ```
135
+
136
+ ## Notes
137
+
138
+ - `numpy` is required. OpenCV-based backends need `omnicam[opencv]`.
139
+ - GStreamer and Gazebo features rely on system-level tooling.
140
+
141
+ ## License
142
+
143
+ MIT. See `LICENSE`.
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/camerapy/__init__.py
5
+ src/camerapy/base_camera.py
6
+ src/camerapy/cv_camera_base.py
7
+ src/camerapy/file_capture.py
8
+ src/camerapy/gazebo_camera.py
9
+ src/camerapy/gstreamer_capture.py
10
+ src/camerapy/internet_capture.py
11
+ src/camerapy/pi_camera.py
12
+ src/camerapy/readonly_camera.py
13
+ src/camerapy/screen_capture.py
14
+ src/camerapy/simple_camera.py
15
+ src/omnicam.egg-info/PKG-INFO
16
+ src/omnicam.egg-info/SOURCES.txt
17
+ src/omnicam.egg-info/dependency_links.txt
18
+ src/omnicam.egg-info/requires.txt
19
+ src/omnicam.egg-info/top_level.txt
@@ -0,0 +1,13 @@
1
+ numpy>=1.21
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+
6
+ [opencv]
7
+ opencv-python>=4.7
8
+
9
+ [pi]
10
+ picamera2>=0.3
11
+
12
+ [screen]
13
+ mss>=9.0
@@ -0,0 +1 @@
1
+ camerapy