import os
import cv2 as cv
import zipfile
import numpy as np
import fnmatch
from numpy.core.numeric import _rollaxis_dispatcher
# from machinevisiontoolbox.ImageCore import ImageCoreMixin
# from machinevisiontoolbox.ImageIO import ImageIOMixin
# from machinevisiontoolbox.ImageConstants import ImageConstantsMixin
# from machinevisiontoolbox.ImageProcessing import ImageProcessingMixin
# from machinevisiontoolbox.ImageMorph import ImageMorphMixin
# from machinevisiontoolbox.ImageSpatial import ImageSpatialMixin
# from machinevisiontoolbox.ImageColor import ImageColorMixin
# from machinevisiontoolbox.ImageReshape import ImageReshapeMixin
# from machinevisiontoolbox.ImageFeatures import ImageFeaturesMixin
# from machinevisiontoolbox.ImageBlobs import ImageBlobsMixin
# from machinevisiontoolbox.ImageLineFeatures import ImageLineFeaturesMixin
# from machinevisiontoolbox.ImagePointFeatures import ImagePointFeaturesMixin
from machinevisiontoolbox.base import mvtb_path_to_datafile, iread, convert
from machinevisiontoolbox import Image
from numpy.lib.arraysetops import isin
from abc import ABC, abstractmethod
# class Image(
# ImageCoreMixin,
# ImageIOMixin,
# ImageConstantsMixin,
# ImageProcessingMixin,
# ImageMorphMixin,
# ImageSpatialMixin,
# ImageColorMixin,
# ImageReshapeMixin,
# ImageBlobsMixin,
# ImageFeaturesMixin,
# ImageLineFeaturesMixin,
# ImagePointFeaturesMixin
# ):
# pass
class ImageSource(ABC):
@abstractmethod
def __init__():
pass
[docs]
class VideoFile(ImageSource):
"""
Iterate images from a video file
:param filename: Path to video file
:type filename: str
:param kwargs: options applied to image frames, see :func:`~machinevisiontoolbox.base.imageio.convert`
The resulting object is an iterator over the frames of the video file.
The iterator returns :class:`Image` objects where:
- the ``name`` attribute is the name of the video file
- the ``id`` attribute is the frame number within the file
If the path is not absolute, the video file is first searched for
relative to the current directory, and if not found, it is searched for
in the ``images`` folder of the ``mvtb-data`` package, installed as a
Toolbox dependency.
Example::
>>> from machinevisiontoolbox import VideoFile
>>> video = VideoFile("traffic_sequence.mpg")
>>> len(video)
>>> for im in video:
>>> # process image
:references:
- Robotics, Vision & Control for Python, Section 11.1.4, P. Corke, Springer 2023.
:seealso: :func:`~machinevisiontoolbox.base.imageio.convert`
`opencv.VideoCapture <https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80>`_
"""
[docs]
def __init__(self, filename, **kwargs):
self.filename = str(mvtb_path_to_datafile("images", filename))
# get the number of frames in the video
# not sure it's always correct
cap = cv.VideoCapture(self.filename)
ret, frame = cap.read()
self.nframes = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
self.shape = frame.shape
self.fps = int(cap.get(cv.CAP_PROP_FPS))
self.args = kwargs
cap.release()
self.cap = None
self.i = 0
def __iter__(self):
self.i = 0
if self.cap is not None:
self.cap.release()
self.cap = cv.VideoCapture(self.filename)
return self
def __next__(self):
ret, frame = self.cap.read()
if ret is False:
self.cap.release()
raise StopIteration
else:
im = convert(frame, **self.args)
if im.ndim == 3:
im = Image(im, id=self.i, name=self.filename, colororder="RGB")
else:
im = Image(im, id=self.i, name=self.filename)
self.i += 1
return im
def __len__(self):
return self.nframes
def __repr__(self):
return f"VideoFile({os.path.basename(self.filename)}) {self.shape[1]} x {self.shape[0]}, {self.nframes} frames @ {self.fps}fps"
[docs]
class VideoCamera(ImageSource):
"""
Iterate images from a local video camera
:param id: Identity of local camera
:type id: int
:param kwargs: options applied to image frames, see :func:`~machinevisiontoolbox.base.imageio.convert`
Connect to a local video camera. For some cameras this will cause
the recording light to come on.
The resulting object is an iterator over the frames from the video
camera. The iterator returns :class:`Image` objects.
Example::
>>> from machinevisiontoolbox import VideoCamera
>>> video = VideoCamera(0)
>>> for im in video:
>>> # process image
alternatively::
>>> img = video.grab()
:note: The value of ``id`` is system specific but generally 0 is the
first attached video camera.
:references:
- Robotics, Vision & Control for Python, Section 11.1.3, P. Corke, Springer 2023.
:seealso: :func:`~machinevisiontoolbox.base.imageio.convert`
`cv2.VideoCapture <https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80>`_,
"""
[docs]
def __init__(self, id=0, rgb=True, **kwargs):
self.id = id
self.cap = None
self.args = kwargs
self.rgb = rgb
self.cap = cv.VideoCapture(self.id)
def __iter__(self):
self.i = 0
if self.cap is not None:
self.cap.release()
self.cap = cv.VideoCapture(self.id)
return self
def __next__(self):
ret, frame = self.cap.read() # frame will be in BGR order
if ret is False:
print("camera read fail, camera is released")
self.cap.release()
raise StopIteration
else:
if self.rgb:
# RGB required, invert the planes
im = convert(frame, rgb=True, copy=True, **self.args)
img = Image(im, id=self.i, colororder="RGB")
else:
# BGR required
im = convert(frame, rgb=False, colororder="BGR", copy=True, **self.args)
img = Image(im, id=self.i, colororder="BGR")
self.i += 1
return img
[docs]
def grab(self):
"""
Grab single frame from camera
:return: next frame from the camera
:rtype: :class:`Image`
This is an alternative interface to the class iterator.
"""
stream = iter(self)
return next(stream)
[docs]
def release(self):
"""
Release the camera
Disconnect from the local camera, and for cameras with a recording
light, turn off that light.
"""
self.cap.release()
def __repr__(self):
backend = self.cap.getBackendName()
return f"VideoCamera({self.id}) {self.width} x {self.height} @ {self.framerate}fps using {backend}"
# see https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d
properties = {
"brightness": cv.CAP_PROP_BRIGHTNESS,
"contrast": cv.CAP_PROP_CONTRAST,
"saturation": cv.CAP_PROP_SATURATION,
"hue": cv.CAP_PROP_HUE,
"gain": cv.CAP_PROP_GAIN,
"exposure": cv.CAP_PROP_EXPOSURE,
"auto-exposure": cv.CAP_PROP_AUTO_EXPOSURE,
"gamma": cv.CAP_PROP_GAMMA,
"temperature": cv.CAP_PROP_TEMPERATURE,
"auto-whitebalance": cv.CAP_PROP_AUTO_WB,
"whitebalance-temperature": cv.CAP_PROP_WB_TEMPERATURE,
"ios:exposure": cv.CAP_PROP_IOS_DEVICE_EXPOSURE,
"ios:whitebalance": cv.CAP_PROP_IOS_DEVICE_WHITEBALANCE,
}
[docs]
def get(self, property=None):
"""
Get camera property
:param prop: camera property name
:type prop: str
:return: parameter value
:rtype: float
Get value for the specified property. Value 0 is returned when querying a property that is not supported by the backend used by the VideoCapture instance.
============================== =========================================================
Property description
============================== =========================================================
``"brightness"`` image brightness (offset)
``"contrast"`` contrast of the image
``"saturation"`` saturation of the image
``"hue"`` hue of the image
``"gain"`` gain of the image
``"exposure"`` exposure of image
``"auto-exposure"`` exposure control by camera
``"gamma"`` gamma of image
``"temperature"`` color temperature
``"auto-whitebalance"`` enable/ disable auto white-balance
``"whitebalance-temperature"`` white-balance color temperature
``"ios:exposure"`` exposure of image for Apple AVFOUNDATION backend
``"ios:whitebalance"`` white balance of image for Apple AVFOUNDATION backend
============================== =========================================================
:seealso: :meth:`set`
"""
if property is not None:
return self.cap.get(self.properties[property])
else:
return {
property: self.cap.get(self.properties[property])
for property in self.properties
}
[docs]
def set(self, property, value):
"""
Set camera property
:param prop: camera property name
:type prop: str
:param value: new property value
:type value: float
:return: parameter value
:rtype: float
Set new value for the specified property. Value 0 is returned when querying a property that is not supported by the backend used by the VideoCapture instance.
============================== =========================================================
Property description
============================== =========================================================
``"brightness"`` image brightness (offset)
``"contrast"`` contrast of the image
``"saturation"`` saturation of the image
``"hue"`` hue of the image
``"gain"`` gain of the image
``"exposure"`` exposure of image
``"auto-exposure"`` exposure control by camera
``"gamma"`` gamma of image
``"temperature"`` color temperature
``"auto-whitebalance"`` enable/ disable auto white-balance
``"whitebalance-temperature"`` white-balance color temperature
``"ios:exposure"`` exposure of image for Apple AVFOUNDATION backend
``"ios:whitebalance"`` white balance of image for Apple AVFOUNDATION backend
============================== =========================================================
:seealso: :meth:`get`
"""
return self.cap.set(self.properties[property], value)
@property
def width(self):
"""
Width of video frame
:return: width of video frame in pixels
:rtype: int
:seealso: :meth:`height` :meth:`shape`
"""
return int(self.cap.get(cv.CAP_PROP_FRAME_WIDTH))
@property
def height(self):
"""
Height of video frame
:return: height of video frame in pixels
:rtype: int
:seealso: :meth:`width` :meth:`shape`
"""
return int(self.cap.get(cv.CAP_PROP_FRAME_HEIGHT))
@property
def framerate(self):
"""
Camera frame rate
:return: camera frame rate in frames per second
:rtype: int
:note: If frame rate cannot be determined return -1
"""
try:
fps = int(self.cap.get(cv.CAP_PROP_FPS))
except ValueError:
fps = -1
return fps
@property
def shape(self):
"""
Shape of video frame
:return: height and width of video frame in pixels
:rtype: int, int
:seealso: :meth:`height` :meth:`width`
"""
return (self.height, self.width)
[docs]
class ImageCollection(ImageSource):
"""
Iterate images from a collection of files
:param filename: wildcard path to image files
:type filename: str
:param loop: Endlessly loop over the files, defaults to False
:type loop: bool, optional
:param kwargs: options applied to image frames, see :func:`~machinevisiontoolbox.base.imageio.convert`
The resulting object is an iterator over the image files that match the
wildcard description. The iterator returns :class:`Image` objects where
the ``name`` attribute is the name of the image file
If the path is not absolute, the video file is first searched for
relative to the current directory, and if not found, it is searched for
in the ``images`` folder of the ``mvtb-data`` package, installed as a
Toolbox dependency.
Example::
>>> from machinevisiontoolbox import FileColletion
>>> images = FileCollection('campus/*.png')
>>> len(images)
>>> for image in images: # iterate over images
>>> # process image
alternatively::
>>> img = files[i] # load i'th file from the collection
:references:
- Robotics, Vision & Control for Python, Section 11.1.2, P. Corke, Springer 2023.
:seealso: :func:`~machinevisiontoolbox.base.imageio.convert`
`cv2.imread <https://docs.opencv.org/master/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
"""
[docs]
def __init__(self, filename=None, loop=False, **kwargs):
if filename is not None:
self.images, self.names = iread(filename)
self.args = kwargs
self.loop = loop
def __getitem__(self, i):
if isinstance(i, slice):
# slice of a collection -> ImageCollection
new = self.__class__()
new.images = self.images[i]
new.names = self.names[i]
new.args = self.args
return new
else:
# element of a collection -> Image
data = self.images[i]
im = convert(data, **self.args)
if im.ndim == 3:
return Image(im, name=self.names[i], id=i, colororder="RGB")
else:
return Image(im, id=i, name=self.names[i])
def __iter__(self):
self.i = 0
return self
def __str__(self):
return "\n".join([str(f) for f in self.names])
def __repr__(self):
return str(self)
def __next__(self):
if self.i >= len(self.names):
if self.loop:
self.i = 0
else:
raise StopIteration
data = self.images[self.i]
im = convert(data, **self.args)
if im.ndim == 3:
im = Image(im, id=self.i, name=self.names[self.i], colororder="BGR")
else:
im = Image(im, id=self.i, name=self.names[self.i])
self.i += 1
return im
def __len__(self):
return len(self.images)
[docs]
class ZipArchive(ImageSource):
"""
Iterate images from a zip archive
:param filename: path to zipfile
:type filename: str
:param filter: a Unix shell-style wildcard that specified which files
to include when iterating over the archive
:type filter: str
:param kwargs: options applied to image frames, see :func:`~machinevisiontoolbox.base.imageio.convert`
The resulting object is an iterator over the image files within the
zip archive. The iterator returns :class:`Image` objects and the name of
the file, within the archive, is given by its ``name`` attribute.
If the path is not absolute, the video file is first searched for
relative to the current directory, and if not found, it is searched for
in the ``images`` folder of the ``mvtb-data`` package, installed as a
Toolbox dependency.
Example::
>>> from machinevisiontoolbox import ZipArchive
>>> images = ZipArchive('bridge-l.zip')
>>> len(images)
>>> for image in images: # iterate over files
>>> # process image
alternatively::
>>> image = images[i] # load i'th file from the archive
:references:
- Robotics, Vision & Control for Python, Section 11.1.2, P. Corke, Springer 2023.
:note: ``filter`` is a Unix style wildcard expression, not a Python
regexp, so expressions like ``*.png`` would select all PNG files in
the archive for iteration.
:seealso: :meth:`open` :func:`~machinevisiontoolbox.base.imageio.convert`
`cv2.imread <https://docs.opencv.org/master/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
"""
[docs]
def __init__(self, filename, filter=None, loop=False, **kwargs):
filename = mvtb_path_to_datafile("images", filename)
self.zipfile = zipfile.ZipFile(filename, "r")
if filter is None:
files = self.zipfile.namelist()
else:
files = fnmatch.filter(self.zipfile.namelist(), filter)
self.files = sorted(files)
self.args = kwargs
self.loop = loop
[docs]
def open(self, name):
"""
Open a file from the archive
:param name: file name
:type name: str
:return: read-only handle to the named file
:rtype: file object
Opens the specified file within the archive. Typically the
``ZipArchive`` instance is used as an iterator over the image files
within, but this method can be used to access non-image data such as
camera calibration data etc. that might also be contained within the
archive and is excluded by the ``filter``.
"""
return self.zipfile.open(name)
[docs]
def ls(self):
"""
List all files within the archive to stdout.
"""
for name in self.zipfile.namelist():
print(name)
def __getitem__(self, i):
im = self._read(i)
if im.ndim == 3:
return Image(im, name=self.files[i], id=i, colororder="BGR")
else:
return Image(im, id=i, name=self.files[i])
def __iter__(self):
self.i = 0
return self
def __repr__(self):
return "\n".join(self.files)
def __next__(self):
if self.i >= len(self.files):
if self.loop:
self.i = 0
else:
raise StopIteration
im = self._read(self.i)
if im.ndim == 3:
im = Image(im, id=self.i, name=self.files[self.i], colororder="BGR")
else:
im = Image(im, id=self.i, name=self.files[self.i])
self.i += 1
return im
def __len__(self):
return len(self.files)
def _read(self, i):
data = self.zipfile.read(self.files[i])
img = cv.imdecode(
np.frombuffer(data, np.uint8), cv.IMREAD_ANYDEPTH | cv.IMREAD_UNCHANGED
)
return convert(img, **self.args)
[docs]
class WebCam(ImageSource):
"""
Iterate images from an internet web camera
:param url: URL of the camera
:type url: str
:param kwargs: options applied to image frames, see :func:`~machinevisiontoolbox.base.imageio.convert`
The resulting object is an iterator over the frames returned from the
remote camera. The iterator returns :class:`Image` objects.
Example::
>>> from machinevisiontoolbox import WebCam
>>> webcam = WebCam('https://webcam.dartmouth.edu/webcam/image.jpg')
>>> for image in webcam: # iterate over frames
>>> # process image
alternatively::
>>> img = webcam.grab() # grab next frame
:note: Manu webcameras accept a query string in the URL to specify
image resolution, image format, codec and other parameters. There
is no common standard for this, see the manufacturer's datasheet
for details.
:references:
- Robotics, Vision & Control for Python, Section 11.1.5, P. Corke, Springer 2023.
:seealso: :func:`~machinevisiontoolbox.base.imageio.convert`
`cv2.VideoCapture <https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80>`_
"""
[docs]
def __init__(self, url, **kwargs):
self.url = url
self.args = kwargs
self.cap = None
def __iter__(self):
if self.cap is not None:
self.cap.release()
self.cap = cv.VideoCapture(self.url)
return self
def __next__(self):
ret, frame = self.cap.read()
if ret is False:
self.cap.release()
raise StopIteration
else:
im = convert(frame, **self.args)
if im.ndim == 3:
return Image(im, colororder="RGB")
else:
return Image(im)
[docs]
def grab(self):
"""
Grab frame from web camera
:return: next frame from the web camera
:rtype: :class:`Image`
This is an alternative interface to the class iterator.
"""
stream = iter(self)
return next(stream)
# dartmouth = WebCam('https://webcam.dartmouth.edu/webcam/image.jpg')
[docs]
class EarthView(ImageSource):
"""
Iterate images from GoogleEarth
:param key: Google API key, defaults to None
:type key: str
:param type: type of map (API ``maptype``): 'satellite' [default], 'map', 'roads', 'hybrid', and 'terrain'.
:type type: str, optional
:param zoom: map zoom, defaults to 18
:type zoom: int, optional
:param scale: image scale factor: 1 [default] or 2
:type scale: int, optional
:param shape: image size (API ``size``), defaults to (500, 500)
:type shape: tuple, optional
:param kwargs: options applied to image frames, see :func:`~machinevisiontoolbox.base.imageio.convert`
The resulting object has a ``grab`` method that returns :class:`Image`
objects for a specified position on the planet.
``zoom`` varies from 1 (whole world) to a maximum of 18.
The ``type`` argument controls the type of map returned:
=============== ========================================================================
``type`` Returned image
=============== ========================================================================
``"satellite"`` satellite color image from space
``"roadmap"`` a standard roadmap image as normally shown on the Google Maps website
``"map"`` synonym for ``"roadmap"``
``"hybrid"`` hybrid of the satellite with overlay of roadmap image
``"terrain"`` specifies a physical relief map image, showing terrain and vegetation
``"roads"`` a binary image which is an occupancy grid, roads are free space
=============== ========================================================================
Example::
>>> from machinevisiontoolbox import EarthView
>>> earth = EarthView() # create an Earth viewer
>>> image = earth(-27.475722, 153.0285, zoom=17 # make a view
>>> # process image
.. warning:: You must have a Google account and a valid key, backed
by a credit card, to access this service.
`Getting started <https://developers.google.com/maps/documentation/maps-static>`_
:note:
- If the key is not passed in, a value is sought from the
environment variable ``GOOGLE_KEY``.
- Uses the `Google Maps Static API <https://developers.google.com/maps/documentation/maps-static/start>`_
:references:
- Robotics, Vision & Control for Python, Section 11.1.6, P. Corke, Springer 2023.
:seealso: :meth:`grab` :func:`~machinevisiontoolbox.base.imageio.convert`
"""
[docs]
def __init__(
self, key=None, type="satellite", zoom=18, scale=1, shape=(500, 500), **kwargs
):
if key is None:
self.key = os.getenv("GOOGLE_KEY")
else:
self.key = key
self.type = type
self.scale = scale
self.zoom = zoom
self.shape = shape
self.args = kwargs
[docs]
def grab(
self,
lat,
lon,
zoom=None,
type=None,
scale=None,
shape=None,
roadnames=False,
placenames=False,
):
"""
Google map view as an image
:param lat: lattitude (degrees)
:type lat: float
:param lon: longitude (degrees)
:type lon: float
:param type: map type, "roadmap", "satellite" [default], "hybrid", and "terrain".
:type type: str, optional
:param zoom: image zoom factor, defaults to None
:type zoom: int, optional
:param scale: image scale factor: 1 [default] or 2
:type scale: int, optional
:param shape: image shape (width, height), defaults to None
:type shape: array_like(2), optional
:param roadnames: show roadnames, defaults to False
:type roadnames: bool, optional
:param placenames: show place names, defaults to False
:type placenames: bool, optional
:return: Google map view
:rtype: :class:`Image`
If parameters are not given the values provided to the constructor
are taken as defaults.
:note: The returned image may have an alpha plane.
"""
if type is None:
type = self.type
if scale is None:
scale = self.scale
if zoom is None:
zoom = self.zoom
if shape is None:
shape = self.shape
# type is one of: satellite map hybrid terrain roadmap roads
occggrid = False
if type == "map":
type = "roadmap"
elif type == "roads":
type = "roadmap"
occggrid = True
# https://developers.google.com/maps/documentation/maps-static/start#URL_Parameters
# now read the map
url = f"https://maps.googleapis.com/maps/api/staticmap?center={lat},{lon}&zoom={zoom}&size={shape[0]}x{shape[1]}&scale={scale}&format=png&maptype={type}&key={self.key}&sensor=false"
opturl = []
if roadnames:
opturl.append("style=feature:road|element:labels|visibility:off")
if placenames:
opturl.append(
"style=feature:administrative|element:labels.text|visibility:off&style=feature:poi|visibility:off"
)
if occggrid:
opturl.extend(
[
"style=feature:landscape|element:geometry.fill|color:0x000000|visibility:on",
"style=feature:landscape|element:labels|visibility:off",
"style=feature:administrative|visibility:off",
"style=feature:road|element:geometry|color:0xffffff|visibility:on",
"style=feature:road|element:labels|visibility:off",
"style=feature:poi|element:all|visibility:off",
"style=feature:transit|element:all|visibility:off",
"style=feature:water|element:all|visibility:off",
]
)
if len(opturl) > 0:
url += "&" + "&".join(opturl)
data = iread(url)
if data[0].shape[2] == 4:
colororder = "RGBA"
elif data[0].shape[2] == 3:
colororder = "RGB"
else:
colororder = None
im = convert(data[0], **self.args)
return Image(im, colororder=colororder)
# if __name__ == "__main__":
# import machinevisiontoolbox as mvtb
# campus = ImageCollection("campus/*.png")
# a = campus[3]
# print(a)
# # campus/*.png
# # traffic_sequence.mpg
# # v = VideoFile("traffic_sequence.mpg")
# # f = FileCollection("campus/*.png")
# # print(f)
# zf = ZipArchive('bridge-l.zip', filter='*02*')
# print(zf)
# print(len(zf))
# # print(zf)
# print(zf[12])
# for im in zf:
# print(im, im.max)
# pass