Files
kurwa-strona/Lib/site-packages/ffpyplayer/pic.pyx
Bartłomiej Patyk e5e64b6dc8 quick fix 2
2025-10-22 19:05:25 +02:00

1117 lines
41 KiB
Cython

'''
FFmpeg based image storage and conversion tools
===============================================
FFmpeg based classes to store and convert images from / to many different pixel
formats. See :class:`Image` and :class:`SWScale` for details.
Create an image in rgb24 format:
.. code-block:: python
>>> w, h = 500, 100
>>> size = w * h * 3
>>> buf = bytearray([int(x * 255 / size) for x in range(size)])
>>> img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h))
Convert the image to a different size:
.. code-block:: python
>>> sws = SWScale(w, h, img.get_pixel_format(), ow=w/2, oh=h/3)
>>> img2 = sws.scale(img)
>>> img2.get_size()
(250, 33)
Convert the image to YUV420P and get the resulting plane buffers as bytearrays:
.. code-block:: python
>>> sws = SWScale(w, h, img.get_pixel_format(), ofmt='yuv420p')
>>> img2 = sws.scale(img)
>>> img2.get_pixel_format()
'yuv420p'
>>> planes = img2.to_bytearray()
>>> map(len, planes)
[50000, 12500, 12500, 0]
Create an Image using default FFmpeg buffers:
.. code-block:: python
>>> img = Image(pix_fmt='rgb24', size=(w, h))
Copy the image:
.. code-block:: python
>>> import copy
>>> # copy reference without actually copying the buffers
>>> img2 = copy.copy(img)
>>> # do deep copy
>>> img2 = copy.deepcopy(img)
'''
__all__ = ('Image', 'SWScale', 'get_image_size', 'ImageLoader')
include "includes/inline_funcs.pxi"
from cpython.ref cimport PyObject
from cython cimport view as cyview
cdef extern from "string.h" nogil:
void *memset(void *, int, size_t)
void *memcpy(void *, const void *, size_t)
cdef extern from "Python.h":
PyObject* PyString_FromStringAndSize(const char *, Py_ssize_t)
void Py_DECREF(PyObject *)
import ffpyplayer.tools # for initialization purposes
def get_image_size(pix_fmt, width, height):
'''Returns the size in bytes of the buffers of each plane of an image with a
given pixel format, width, and height.
:Parameters:
`pix_fmt`: str
The pixel format in which the image is represented. Can be one of
:attr:`~ffpyplayer.tools.pix_fmts`.
`width`: int
The width of the image.
`height`: int
The height of the image.
:returns:
`4-tuple of ints`:
A tuple of buffer sizes in bytes for each plane of this pixel format
required to store the image. Unused planes are zero.
:
.. code-block:: python
>>> print get_image_size('rgb24', 100, 100)
(30000, 0, 0, 0)
>>> print get_image_size('yuv420p', 100, 100)
(10000, 2500, 2500, 0)
>>> print get_image_size('gray', 100, 100)
(10000, 1024, 0, 0)
'''
cdef AVPixelFormat fmt
cdef int res, w = width, h = height
cdef int size[4]
cdef int ls[4]
cdef int req[4]
cdef char msg[256]
cdef bytes fmtb
if not pix_fmt or not width or not height:
return 0
fmtb = pix_fmt.encode('utf8')
fmt = av_get_pix_fmt(fmtb)
if fmt == AV_PIX_FMT_NONE:
raise Exception('Pixel format %s not found.' % pix_fmt)
res = av_image_fill_linesizes(ls, fmt, w)
if res < 0:
raise Exception('Failed to initialize linesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
res = get_plane_sizes(size, req, fmt, h, ls)
if res < 0:
raise Exception('Failed to get planesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
return (size[0], size[1], size[2], size[3])
cdef class SWScale(object):
'''Converts Images from one format and size to another format and size.
The class accepts an Image of a given pixel format and size and converts it
to another Image with a different pixel format and size. Each SWScale instance
converts only images with parameters specified when creating the instance.
:Parameters:
`iw, ih`: int
The width and height of the source image.
`ifmt`: str
The pixel format of the source image. Can be one of
:attr:`ffpyplayer.tools.pix_fmts`.
`ow, oh`: int
The width and height of the output image after converting from the
source image. A value of 0 will set that parameter to the source
height/width. A value of -1 for one of the parameters, will result in
a value of that parameter that maintains the original aspect ratio.
Defaults to -1.
`ofmt`: str
The pixel format of the output image. Can be one of
:attr:`ffpyplayer.tools.pix_fmts`. If empty, the source pixel format
will be used. Defaults to empty string.
:
.. code-block:: python
>>> w, h = 500, 100
>>> size = w * h * 3
>>> buf = bytearray([int(x * 255 / size) for x in range(size)])
>>> img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h))
>>> # specify output w,h
>>> sws = SWScale(w, h, img.get_pixel_format(), ow=w/2, oh=h/3)
>>> img2 = sws.scale(img)
>>> img2.get_size()
(250, 33)
>>> # use input height
>>> sws = SWScale(w, h, img.get_pixel_format(), ow=w/2, oh=0)
>>> img2 = sws.scale(img)
>>> img2.get_size()
(250, 100)
>>> # keep aspect ratio
>>> sws = SWScale(w, h, img.get_pixel_format(), ow=w/2)
>>> img2 = sws.scale(img)
>>> img2.get_size()
(250, 50)
>>> # convert rgb24 to yuv420p
>>> sws = SWScale(w, h, img.get_pixel_format(), ofmt='yuv420p')
>>> img2 = sws.scale(img)
>>> img2.get_pixel_format()
'yuv420p'
>>> # convert into a previously allocated and aligned image
>>> import math
>>> align = lambda x: int(math.ceil(x / 32.) * 32)
>>> img2 = Image(pix_fmt=img.get_pixel_format(), size=(w/2, h/2))
>>> img2.get_linesizes(keep_align=True)
(750, 0, 0, 0)
>>> linesize = map(align, img2.get_linesizes())
>>> linesize
[768, 0, 0, 0]
>>> img2 = Image(pix_fmt=img2.get_pixel_format(), size=img2.get_size(), linesize=linesize)
>>> img2.get_linesizes(keep_align=True)
(768, 0, 0, 0)
>>> sws.scale(img, dst=img2)
<ffpyplayer.pic.Image object at 0x02B44440>
>>> img2
<ffpyplayer.pic.Image object at 0x02B44440>
'''
def __cinit__(self, int iw, int ih, ifmt, int ow=-1, int oh=-1, ofmt='', **kargs):
cdef AVPixelFormat src_pix_fmt, dst_pix_fmt
self.dst_pix_fmt = ifmt.encode('utf8')
self.dst_pix_fmt_s = ifmt
self.sws_ctx = NULL
src_pix_fmt = av_get_pix_fmt(self.dst_pix_fmt)
if src_pix_fmt == AV_PIX_FMT_NONE:
raise Exception('Pixel format %s not found.' % ifmt)
dst_pix_fmt = src_pix_fmt
if ofmt:
self.dst_pix_fmt = ofmt.encode('utf8')
self.dst_pix_fmt_s = ofmt
dst_pix_fmt = av_get_pix_fmt(self.dst_pix_fmt)
if dst_pix_fmt == AV_PIX_FMT_NONE:
raise Exception('Pixel format %s not found.' % ofmt)
if ow == -1 and oh == -1:
ow = oh = 0
if not oh:
oh = ih
if not ow:
ow = iw
if ow == -1:
ow = <int>(oh / <double>ih * iw)
if oh == -1:
oh = <int>(ow / <double>iw * ih)
self.dst_w = ow
self.dst_h = oh
self.src_pix_fmt = src_pix_fmt
self.src_w = iw
self.src_h = ih
self.sws_ctx = sws_getCachedContext(NULL, iw, ih, src_pix_fmt, ow, oh,
dst_pix_fmt, SWS_BICUBIC, NULL, NULL, NULL)
if self.sws_ctx == NULL:
raise Exception('Cannot initialize the conversion context.')
def __dealloc__(self):
if self.sws_ctx != NULL:
sws_freeContext(self.sws_ctx)
def scale(self, Image src, Image dst=None, int _flip=False):
'''Scales a image into another image format and/or size as specified by the
instance initialization parameters.
:Parameters:
`src`: :class:`Image`
A image instance with values matching the source image specification
of this instance. An exception is raised if the Image doesn't match.
It will be used as the source image.
`dst`: :class:`Image` or None
A image instance with values matching the output image specification
of this instance. An exception is raised if the Image doesn't match.
If specified, the output image will be converted directly into this Image.
If not specified, a new Image will be created and returned.
`_flip`: bool, defaults to False
Whether the image will be flipped before scaling. This only works
for pixel formats whose color planes are the same size (e.g. rgb), so
use with caution.
:returns:
:class:`Image`:
The output image. If ``dst`` was not None ``dst`` will be returned,
otherwise a new image containing the converted image will be returned.
'''
if (<AVPixelFormat>src.frame.format != self.src_pix_fmt or
self.src_w != src.frame.width or self.src_h != src.frame.height):
raise Exception("Source image doesn't match the specified input parameters.")
if not dst:
dst = Image.__new__(Image, pix_fmt=self.dst_pix_fmt_s,
size=(self.dst_w, self.dst_h))
with nogil:
if _flip:
for i in range(4):
(<uint8_t * *>src.frame.data)[i] += src.frame.linesize[i] * (src.frame.height - 1)
src.frame.linesize[i] = -src.frame.linesize[i]
sws_scale(self.sws_ctx, <const uint8_t *const *>src.frame.data, src.frame.linesize,
0, src.frame.height, dst.frame.data, dst.frame.linesize)
if _flip:
for i in range(4):
src.frame.linesize[i] = -src.frame.linesize[i]
(<uint8_t * *>src.frame.data)[i] -= src.frame.linesize[i] * (src.frame.height - 1)
return dst
cdef int raise_exec(object ecls) nogil except 1:
with gil:
raise ecls()
cdef class Image(object):
'''Stores a image using a specified pixel format.
An Image can be represented by many different pixel formats, which determines
how the buffer representing it is stored. We store the buffers as one to
four arrays of bytes representing the one to four planes. For example,
RGB23 has all the data in the first plane in the form of RGBRGB... while
YUV420P uses the first three planes.
The Image can be initialized with a list of the plane buffers, or internal
buffers can be created when none are provided. Depending on how it's initialized
one or more params need to be specified.
:Paramters:
`plane_buffers`: list
A list of bytes or bytearray type objects representing the 1-4 planes.
The number of planes is determined by ``pix_fmt`` (e.g. 1 for RGB24,
3 for yuv). The length of the bytes object in each plane is a function
of ``size``, and if provided, also ``linesize``. See ``linesize`` for details.
The buffers are used directly without making any copies therefore, the
bytes objects are kept alive internally as long as this instance is alive.
If empty, internal buffers for the image will be created for the image.
`pix_fmt`: str
The pixel format of the image. Can be one of :attr:`ffpyplayer.tools.pix_fmts`.
Must be provided when using ``plane_buffers``.
`size`: 2-tuple of ints
The size of the frame in the form of (width, height).
Must be provided when using ``plane_buffers``.
`linesize`: list of ints
The linesize of each provided plane. In addition to the width of the frame,
a linesize can be provided. The ``linesize`` represent the actual number of
bytes in each line, and may be padded at the end to satisfy some alignment
requirement. For example, a RGB24 frame of size ``(100, 10)`` will have
``3 * 100 = 300`` bytes in each horizontal line and will be 3000 bytes large.
But, when 32 bit alignment is required, the buffer will have to padded at the
end so that each line is 320 bytes, and the total buffer length is 3200 bytes.
If ``linesize`` is provided, it must be provided for every valid plane.
If it's not provided, an alignment of 1 (i.e. no alignment) is assumed.
See :meth:`get_buffer_size` for more details.
`no_create`: bool
A optional argument, which if provided with True will just create the instance
and not initialize anything. All other parameters are ignored when True.
This is useful when instantiating later from cython with the ``cython_init`` method.
**Copying**
FFmpeg has an internal ref counting system where when used, it frees buffers
it allocated only when there's no reference to it remaining thereby allowing
multiple images to use the same buffer without making copies. When the
Image class allocates the image buffers, e.g. when ``plane_buffers`` is empty
such reference buffers are created. As a consequence, when copying the Image
object, the buffers will not have to be copied.
Using the python copy module you can do a **shallow** or a **deep** copy of
the object. When doing a **shallow** copy, new buffers will be created if the
original buffers were not FFmpeg created and referenced, e.g. if provided
using ``plane_buffers``. This is to ensure the buffers won't
go out of memory while in use.
After the copy, the buffers will be "referenced" and additional copies will
create more references without copying the buffers.
A **deep** copy, however, will always create a new referenced buffer.
The function :meth:`is_ref` indicates whether the image buffer is such a
FFmpeg referenced buffer.
:
.. code-block:: python
>>> w, h = 640, 480
>>> size = w * h * 3
>>> buf = bytearray([int(x * 255 / size) for x in range(size)])
>>> img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h))
>>> img2 = Image(pix_fmt='rgb24', size=(w, h))
'''
def __cinit__(self, plane_buffers=[], pix_fmt='', size=(), linesize=[], **kwargs):
cdef int i, w, h, res
cdef object plane = None
cdef char msg[256]
cdef AVFrame *avframe
cdef int buff_size[4]
cdef int ls[4]
cdef int req[4]
cdef bytes fmt_b
self.frame = NULL
self.byte_planes = None
if kwargs.get('no_create', False):
return
fmt_b = pix_fmt.encode('utf8')
self.pix_fmt = av_get_pix_fmt(fmt_b)
if self.pix_fmt == AV_PIX_FMT_NONE:
raise Exception('Pixel format %s not found.' % pix_fmt)
w, h = size
self.frame = av_frame_alloc()
if self.frame == NULL:
raise MemoryError()
self.frame.format = self.pix_fmt
self.frame.width = w
self.frame.height = h
if linesize:
for i in range(min(len(linesize), 4)):
self.frame.linesize[i] = linesize[i]
else:
res = av_image_fill_linesizes(self.frame.linesize, self.pix_fmt, w)
if res < 0:
raise Exception('Failed to initialize linesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
av_image_fill_linesizes(ls, self.pix_fmt, w)
for i in range(4):
if ls[i] and not self.frame.linesize[i]:
raise Exception('Incorrect linesize provided.')
if plane_buffers:
self.byte_planes = []
res = get_plane_sizes(buff_size, req, self.pix_fmt, self.frame.height, self.frame.linesize)
if res < 0:
raise Exception('Failed to get plane sizes: ' + tcode(emsg(res, msg, sizeof(msg))))
for i in range(4):
if req[i] and buff_size[i] and (len(plane_buffers) <= i or not plane_buffers[i]):
raise Exception('Required plane %d not provided for %s' % (i, pix_fmt))
if len(plane_buffers) > i and plane_buffers[i] and not buff_size[i]:
raise Exception('Unused plane %d provided for %s' % (i, pix_fmt))
for i in range(4):
if len(plane_buffers) == i:
break
if not plane_buffers[i]:
continue
plane = plane_buffers[i]
if len(plane) < buff_size[i]:
raise Exception('Buffer for plane %d is too small, required buffer size is %d.'\
% (i, buff_size[i]))
self.byte_planes.append(plane)
self.frame.data[i] = plane
else:
with nogil:
res = av_frame_get_buffer(self.frame, 32)
if res < 0:
raise Exception('Could not allocate avframe buffer of size %dx%d: %s'\
% (w, h, tcode(emsg(res, msg, sizeof(msg)))))
def __dealloc__(self):
av_frame_free(&self.frame)
cdef int cython_init(self, AVFrame *frame) nogil except 1:
'''Can be called only once after object creation and it creates a internal
reference to ``frame``.
'''
self.frame = av_frame_clone(frame)
if self.frame == NULL:
raise_exec(MemoryError)
self.pix_fmt = <AVPixelFormat>self.frame.format
return 0
def __copy__(self):
cdef Image img = Image.__new__(Image, no_create=True)
with nogil:
img.cython_init(self.frame)
return img
def __deepcopy__(self, memo):
cdef AVFrame *frame = av_frame_alloc()
cdef Image img
if frame == NULL:
raise MemoryError()
frame.format = self.frame.format
frame.width = self.frame.width
frame.height = self.frame.height
if av_frame_copy_props(frame, self.frame) < 0:
av_frame_free(&frame)
raise Exception('Cannot copy frame properties.')
if av_frame_get_buffer(frame, 32) < 0:
av_frame_free(&frame)
raise Exception('Cannot allocate frame buffers.')
img = Image.__new__(Image, no_create=True)
with nogil:
av_image_copy(frame.data, frame.linesize, <const uint8_t **>self.frame.data,
self.frame.linesize, <AVPixelFormat>frame.format,
frame.width, frame.height)
img.cython_init(frame)
av_frame_free(&frame)
return img
cpdef is_ref(Image self):
'''Returns whether the image buffer is FFmpeg referenced. This can only be
True when the buffers were allocated internally or by FFmpeg bit not when
``plane_buffers`` is provided. See :class:`Image` for details. After a copy,
it will always returns True.
:returns:
bool: True if the buffer is FFmpeg referenced.
For example:
.. code-block:: python
>>> w, h = 640, 480
>>> img = Image(plane_buffers=[bytes(' ') * (w * h * 3)], pix_fmt='rgb24', size=(w, h))
>>> img.is_ref()
False
>>> import copy
>>> img2 = copy.copy(img)
>>> img2.is_ref()
True
Or if directly allocated internally:
.. code-block:: python
>>> img = Image(pix_fmt='rgb24', size=(w, h))
>>> img.is_ref()
True
'''
return self.frame.buf[0] != NULL
cpdef is_key_frame(Image self):
'''Returns whether the image is a key frame.
:returns:
bool: True if the image was a key frame.
'''
return self.frame.key_frame == 1
cpdef get_linesizes(Image self, keep_align=False):
'''Returns the linesize of each plane.
The linesize is the actual number of bytes in each horizontal line for a given plane,
which may be padded at the end to satisfy some alignment requirement.
For example, a RGB24 frame of size ``(100, 10)`` will have ``3 * 100 = 300``
bytes in each line and will be 3000 bytes large. But, when 32 bit
alignment is required, the buffer will have to padded at the end so
that each line is 320 bytes, and the total buffer length is 3200 bytes.
:Parameters:
`keep_align`: bool
If True, the original linesize alignments of the image will be returned for
every plane. If False, linesize with an alignment of 1 (i.e. no alignment)
will be used, returning the minimal linesize required to for the image.
Defaults to False.
:returns:
4-tuple of ints:
A 4 tuple with the linesizes of each plane. If the plane isn't used
it'll be 0.
By defaults there's no alignment:
.. code-block:: python
>>> w, h = 100, 10
>>> img = Image(plane_buffers=[bytes(' ') * (w * h * 3)],
... pix_fmt='rgb24', size=(w, h))
>>> img.get_linesizes(keep_align=True)
(300, 0, 0, 0)
You can force alignment e.g. 32 bits alignment:
.. code-block:: python
>>> import math
>>> linesize = [int(math.ceil(w * 3 / 32.) * 32)]
>>> linesize
[320]
>>> img = Image(plane_buffers=[bytes(' ') * (h * linesize[0])],
... pix_fmt='rgb24', size=(w, h), linesize=linesize)
>>> img.get_linesizes(keep_align=True)
(320, 0, 0, 0)
>>> img.get_size()
(100, 10)
The linesizes of an unaligned and 32 bit aligned yuv420p image:
.. code-block:: python
>>> img = Image(pix_fmt='yuv420p', size=(w, h))
>>> img.get_linesizes(keep_align=True)
(100, 50, 50, 0)
>>> img.get_size()
(100, 10)
>>> # now try align to 32 bit
>>> linesize = img.get_linesizes(keep_align=True)
>>> align = lambda x: int(math.ceil(x / 32.) * 32)
>>> linesize = map(align, linesize)
>>> linesize
[128, 64, 64, 0]
>>> img = Image(pix_fmt='yuv420p', size=(w, h), linesize=linesize)
>>> img.get_linesizes(keep_align=True)
(128, 64, 64, 0)
>>> img.get_linesizes()
(100, 50, 50, 0)
>>> img.get_size()
(100, 10)
'''
cdef int lsl[4]
cdef int *ls = self.frame.linesize
if not keep_align:
av_image_fill_linesizes(lsl, self.pix_fmt, self.frame.width)
ls = lsl
return (ls[0], ls[1], ls[2], ls[3])
cpdef get_size(Image self):
'''Returns the size of the frame.
:returns:
2-tuple of ints: The size of the frame as ``(width, height)``.
::
>>> img.get_size()
(640, 480)
'''
return (self.frame.width, self.frame.height)
cpdef get_pixel_format(Image self):
'''Returns the pixel format of the image. Can be one of
:attr:`ffpyplayer.tools.pix_fmts`.
:returns:
str: The pixel format of the image.
::
>>> img.get_pixel_format()
'rgb24'
'''
return tcode(av_get_pix_fmt_name(self.pix_fmt))
cpdef get_buffer_size(Image self, keep_align=False):
'''Returns the size of the buffers of each plane.
:Parameters:
`keep_align`: bool
If True, the linesize alignments of the actual image will be used to
calculate the buffer size for each plane. If False, an alignment of 1
(i.e. no alignment) will be used, returning the minimal buffer size
required to store the image. Defaults to False.
:returns:
4-tuple of ints:
A list of buffer sizes for each plane of this pixel format.
A (unaligned) yuv420p image has 3 planes:
.. code-block:: python
>>> w, h = 100, 10
>>> img = Image(pix_fmt='yuv420p', size=(w, h))
>>> img.get_linesizes(keep_align=True)
(100, 50, 50, 0)
>>> img.get_buffer_size()
(1000, 250, 250, 0)
>>> # align to 32 bits
>>> linesize = img.get_linesizes(keep_align=True)
>>> align = lambda x: int(math.ceil(x / 32.) * 32)
>>> linesize = map(align, linesize)
>>> linesize
[128, 64, 64, 0]
>>> img = Image(pix_fmt='yuv420p', size=(w, h), linesize=linesize)
>>> img.get_linesizes(keep_align=True)
(128, 64, 64, 0)
>>> img.get_buffer_size(keep_align=True)
(1280, 320, 320, 0)
>>> img.get_buffer_size()
(1000, 250, 250, 0)
'''
cdef int res
cdef int size[4]
cdef int ls[4]
cdef int req[4]
cdef char msg[256]
if keep_align:
memcpy(ls, self.frame.linesize, sizeof(ls))
else:
res = av_image_fill_linesizes(ls, self.pix_fmt, self.frame.width)
if res < 0:
raise Exception('Failed to initialize linesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
res = get_plane_sizes(size, req, <AVPixelFormat>self.frame.format, self.frame.height, ls)
if res < 0:
raise Exception('Failed to get planesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
return (size[0], size[1], size[2], size[3])
cpdef get_required_buffers(Image self):
'''Returns a 4 tuple of booleans indicating which of the 4 planes are required
(i.e. even if get_buffer_size is non-zero for that plane it may still be
optional).
'''
cdef int res
cdef int size[4]
cdef int ls[4]
cdef int req[4]
cdef char msg[256]
memcpy(ls, self.frame.linesize, sizeof(ls))
res = get_plane_sizes(size, req, <AVPixelFormat>self.frame.format, self.frame.height, ls)
if res < 0:
raise Exception('Failed to get planesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
return (req[0], req[1], req[2], req[3])
cpdef to_bytearray(Image self, keep_align=False):
'''Returns a copy of the plane buffers as bytearrays.
:Parameters:
`keep_align`: bool
If True, the buffer for each plane will be padded after each horizontal
line to match the linesize of its plane in this image. If False, an
alignment of 1 (i.e. no alignment) will be used, returning the
maximially packed buffer of this plane. Defaults to False.
:returns:
4-element list: A list of bytearray buffers for each plane of this
pixel format. An empty bytearray is returned for unused planes.
Get the buffer of an RGB image:
.. code-block:: python
>>> w, h = 100, 10
>>> img = Image(pix_fmt='rgb24', size=(w, h))
>>> img.get_linesizes(keep_align=True)
(300, 0, 0, 0)
>>> map(len, img.to_bytearray())
[3000, 0, 0, 0]
Get the buffers of a YUV420P image:
.. code-block:: python
>>> img = Image(pix_fmt='yuv420p', size=(w, h))
>>> linesize = img.get_linesizes(keep_align=True)
>>> linesize
(100, 50, 50, 0)
>>> align = lambda x: int(math.ceil(x / 32.) * 32)
>>> linesize = map(align, linesize)
>>> linesize
[128, 64, 64, 0]
>>> img = Image(pix_fmt='yuv420p', size=(w, h), linesize=linesize)
>>> map(len, img.to_bytearray())
[1000, 250, 250, 0]
>>> map(len, img.to_bytearray(keep_align=True))
[1280, 320, 320, 0]
>>> # now initialize a new Image with it
>>> img2 = Image(plane_buffers=img.to_bytearray(),
... pix_fmt=img.get_pixel_format(), size=img.get_size())
>>> img2.get_buffer_size(keep_align=True)
(1000, 250, 250, 0)
>>> # keep alignment
>>> img2 = Image(plane_buffers=img.to_bytearray(keep_align=True),
... pix_fmt=img.get_pixel_format(), size=img.get_size(),
... linesize=img.get_linesizes(keep_align=True))
>>> img2.get_buffer_size(keep_align=True)
(1280, 320, 320, 0)
'''
cdef list planes = [None, None, None, None]
cdef int i, res
cdef uint8_t *data[4]
cdef int size[4]
cdef int ls[4]
cdef int req[4]
cdef char msg[256]
memset(data, 0, sizeof(data))
if keep_align:
memcpy(ls, self.frame.linesize, sizeof(ls))
else:
res = av_image_fill_linesizes(ls, self.pix_fmt, self.frame.width)
if res < 0:
raise Exception('Failed to initialize linesizes: ' + tcode(emsg(res, msg, sizeof(msg))))
res = get_plane_sizes(size, req, <AVPixelFormat>self.frame.format, self.frame.height, ls)
if res < 0:
raise Exception('Failed to get plane sizes: ' + tcode(emsg(res, msg, sizeof(msg))))
for i in range(4):
planes[i] = bytearray(b'\0') * size[i]
if size[i]:
data[i] = planes[i]
with nogil:
av_image_copy(data, ls, <const uint8_t **>self.frame.data, self.frame.linesize,
<AVPixelFormat>self.frame.format, self.frame.width, self.frame.height)
return planes
cpdef to_memoryview(Image self, keep_align=False):
'''Returns a memoryviews of the buffers of the image.
:Parameters:
`keep_align`: bool
If True, the buffers of the original image will be returned
without making any additional copies. If False, then if the
image alignment is already 1, the original buffers will be
returned, otherwise, new buffers will be created with an
alignment of 1 and the buffers will be copied into them
and returned. See :meth:`to_bytearray`.
:Returns:
4-element list:
A list of cython arrays for each plane of this
image's pixel format. If the data didn't have to be copied, the
arrays point directly to the original image data. The arrays
can be used where memoryviews are accepted, since cython arrays
implement the memoryview interface.
Unused planes are set to None.
.. warning::
If the data points to the original image data, you must ensure
that this :class:`Image` instance does not go out of memory
while the returned memoryviews of the arrays are in use. Otherwise when
the :class:`Image` goes out of memory, the original data will become
invalid and usage of the returned memoryviews of them will crash python.
Get the buffer of an RGB image:
.. code-block:: python
>>> w, h = 100, 10
>>> img = Image(pix_fmt='rgb24', size=(w, h))
>>> img.get_linesizes(keep_align=True)
(300, 0, 0, 0)
>>> img.to_memoryview()
[<ffpyplayer.pic.array object at 0x055DCE58>, None, None, None]
>>> arr = img.to_memoryview()[0]
>>> # memview is the only attribute of cython arrays
>>> arr.memview
<MemoryView of 'array' at 0x55d1468>
>>> arr.memview.size
3000
'''
cdef list planes = [None, None, None, None]
cdef cyview.array cyarr
cdef int i, res
cdef int size[4]
cdef char *data[4]
cdef int ls[4]
cdef int req[4]
cdef int *cls = self.frame.linesize
cdef char msg[256]
memset(data, 0, sizeof(data))
res = av_image_fill_linesizes(ls, self.pix_fmt, self.frame.width)
if res < 0:
raise Exception('Failed to initialize linesizes: ' +
tcode(emsg(res, msg, sizeof(msg))))
if keep_align or (cls[0] == ls[0] and cls[1] == ls[1] and
cls[2] == ls[2] and cls[3] == ls[3]):
res = get_plane_sizes(size, req, <AVPixelFormat>self.frame.format,
self.frame.height, self.frame.linesize)
if res < 0:
raise Exception('Failed to get plane sizes: ' + tcode(emsg(res, msg, sizeof(msg))))
for i in range(4):
if not size[i]:
continue
planes[i] = cyarr = cyview.array(shape=(size[i], ), itemsize=sizeof(char),
format="B", mode="c", allocate_buffer=False)
cyarr.data = <char *>self.frame.data[i]
return planes
res = get_plane_sizes(size, req, <AVPixelFormat>self.frame.format, self.frame.height, ls)
if res < 0:
raise Exception('Failed to get plane sizes: ' + tcode(emsg(res, msg, sizeof(msg))))
for i in range(4):
if not size[i]:
continue
planes[i] = cyarr = cyview.array(shape=(size[i], ), itemsize=sizeof(char),
format="B", mode="c", allocate_buffer=True)
data[i] = cyarr.data
with nogil:
av_image_copy(<uint8_t **>data, ls, <const uint8_t **>self.frame.data, self.frame.linesize,
<AVPixelFormat>self.frame.format, self.frame.width, self.frame.height)
return planes
cdef class ImageLoader(object):
'''Class that reads one or more images from a file and returns them.
:Parameters:
`filename`: string type
The full path to the image file. The string will first be encoded
using utf8 before passing to FFmpeg.
For example, reading a simple png using the iterator syntax:
.. code-block:: python
>>> img = ImageLoader('file.png')
>>> images = [m for m in img]
>>> images
[(<ffpyplayer.pic.Image object at 0x02B5F5D0>, 0.0)]
Or reading it directly:
.. code-block:: python
>>> img = ImageLoader('file.png')
>>> img.next_frame()
(<ffpyplayer.pic.Image object at 0x02B74850>, 0.0)
>>> img.next_frame()
(None, 0)
>>> img.next_frame()
(None, 0)
Or reading a gif using the iterator syntax:
.. code-block:: python
>>> img = ImageLoader('sapo11.gif')
>>> images = [m for m in img]
>>> images
[(<ffpyplayer.pic.Image object at 0x02B749B8>, 0.0),
(<ffpyplayer.pic.Image object at 0x02B74918>, 0.08),
(<ffpyplayer.pic.Image object at 0x02B74990>, 0.22),
(<ffpyplayer.pic.Image object at 0x02B749E0>, 0.36),
(<ffpyplayer.pic.Image object at 0x02B74A08>, 0.41000000000000003),
(<ffpyplayer.pic.Image object at 0x02B74A30>, 0.46),
(<ffpyplayer.pic.Image object at 0x02B74A58>, 0.51)]
Or reading it directly:
.. code-block:: python
>>> img = ImageLoader('sapo11.gif')
>>> img.next_frame()
(<ffpyplayer.pic.Image object at 0x02B74B70>, 0.0)
>>> img.next_frame()
(<ffpyplayer.pic.Image object at 0x02B74C60>, 0.08)
...
>>> img.next_frame()
(<ffpyplayer.pic.Image object at 0x02B74B70>, 0.51)
>>> img.next_frame()
(None, 0)
>>> img.next_frame()
(None, 0)
'''
def __cinit__(self, filename, **kwargs):
cdef AVDictionary *opts = NULL
cdef const AVDictionaryEntry *t = NULL
cdef int ret = 0
cdef char *fname
fname = self.filename = filename.encode('utf8')
self.format_ctx = NULL
self.codec = NULL
self.codec_ctx = avcodec_alloc_context3(NULL)
if self.codec_ctx == NULL:
raise MemoryError()
self.frame = NULL
self.eof = 0
av_init_packet(&self.pkt)
with nogil:
ret = avformat_open_input(&self.format_ctx, fname, NULL, NULL)
if ret < 0:
raise Exception("Failed to open input file {}: {}".format(filename,
tcode(emsg(ret, self.msg, sizeof(self.msg)))))
ret = avcodec_parameters_to_context(self.codec_ctx, self.format_ctx.streams[0].codecpar)
if ret < 0:
raise Exception("Failed to open input file {}: {}".format(filename,
tcode(emsg(ret, self.msg, sizeof(self.msg)))))
self.codec = avcodec_find_decoder(self.codec_ctx.codec_id)
if self.codec is NULL:
raise Exception("Failed to find supported codec for file {}"
.format(filename))
with nogil:
ret = avcodec_open2(self.codec_ctx, self.codec, &opts)
if ret < 0:
raise Exception("Failed to open codec for {}: {}".format(filename,
tcode(emsg(ret, self.msg, sizeof(self.msg)))))
t = av_dict_get(opts, "", NULL, AV_DICT_IGNORE_SUFFIX)
if t != NULL:
raise Exception("Option {} not found.".format(t.key))
def __dealloc__(self):
with nogil:
av_packet_unref(&self.pkt)
av_frame_free(&self.frame)
avformat_close_input(&self.format_ctx)
if self.codec_ctx != NULL:
avcodec_free_context(&self.codec_ctx)
def __iter__(self):
while True:
res = self.next_frame()
if res == (None, 0):
break
yield res
cpdef next_frame(self):
''' Returns the next available frame, or `(None, 0)` if there are no
more frames available.
:returns:
a 2-tuple of `(:class:`Image`, pts)`:
Where the first element is the next image to be displayed and `pts`
is the time, relative to the first frame, when to display it e.g. in
the case of a gif.
If we reached the eof of the file and there are no more frames
to be returned, it returns `(None, 0)`.
.. warning::
Both :meth:`next_frame` and the iterator syntax read the frames
identically. Consequently, calling one, will also advance the frame
for the other.
'''
cdef int frame_decoded, ret = 0
cdef Image image
cdef double t = 0
if self.eof:
return self.eof_frame()
with nogil:
ret = av_read_frame(self.format_ctx, &self.pkt)
if ret < 0:
if ret == AVERROR_EOF:
self.eof = 1
self.pkt.data = NULL
return self.eof_frame()
raise Exception("Failed to read frame: {}",
tcode(emsg(ret, self.msg, sizeof(self.msg))))
with nogil:
self.frame = av_frame_alloc()
if self.frame is NULL:
raise MemoryError("Failed to alloc frame")
with nogil:
ret = avcodec_send_packet(self.codec_ctx, &self.pkt)
if ret >= 0:
ret = avcodec_receive_frame(self.codec_ctx, self.frame)
if ret < 0:
if ret == AVERROR_EOF:
self.eof = 1
self.pkt.data = NULL
return self.eof_frame()
raise Exception("Failed to decode image from file")
self.frame.pts = self.frame.best_effort_timestamp
if self.frame.pts == AV_NOPTS_VALUE:
t = 0.
else:
t = av_q2d(self.format_ctx.streams[0].time_base) * self.frame.pts
image = Image(no_create=True)
image.cython_init(self.frame)
av_packet_unref(&self.pkt)
av_frame_free(&self.frame)
return image, t
cdef inline object eof_frame(self):
'''Used to flush the remaining frames until no more cached.
'''
cdef int ret = 0
cdef Image image
cdef double t = 0
if self.eof == 2:
return None, 0
with nogil:
self.frame = av_frame_alloc()
if self.frame is NULL:
raise MemoryError("Failed to alloc frame")
with nogil:
ret = avcodec_send_packet(self.codec_ctx, &self.pkt)
if ret >= 0:
ret = avcodec_receive_frame(self.codec_ctx, self.frame)
if ret < 0:
self.eof = 2
av_frame_free(&self.frame)
return None, 0
self.frame.pts = self.frame.best_effort_timestamp
if self.frame.pts == AV_NOPTS_VALUE:
t = 0.
else:
t = av_q2d(self.format_ctx.streams[0].time_base) * self.frame.pts
image = Image(no_create=True)
image.cython_init(self.frame)
av_frame_free(&self.frame)
return image, t