Now that the lightweight C++ camera SDK is complete for Windows, Linux and macOS, we can integrate it into other high-level programming languages. In this article, we will explore how to build a Python camera SDK based on the C++ camera library and use it for multi-barcode scanning with the Dynamsoft Barcode Reader SDK.
Python Multi-Barcode Scanner Demo Video
Scaffolding a CPython Extension Project
A CPython extension is a shared library (e.g., a DLL on Windows, a .so on Linux, or a .dylib on macOS) that can be loaded into the Python interpreter at runtime and used to extend its functionality. The structure of the lite camera CPython extension project is as follows:
python-lite-camera
│
│── include
│ ├── Camera.h
│ ├── CameraPreview.h
│ ├── stb_image_write.h
│── lib
│ ├── linux
│ │ ├── liblitecam.so
│ ├── macos
│ │ ├── liblitecam.dylib
│ ├── windows
│ │ ├── litecam.dll
│ │ ├── litecam.lib
├── src
│ ├── litecam.cpp
│ ├── pycamera.h
│ ├── pywindow.h
│── litecam
│ ├── __init__.py
│── setup.py
│── MANIFEST.in
│── README.md
Explanation:
-
include
: The header files of the C++ camera library. -
lib
: The shared libraries of the C++ camera library. -
src
: The source code of the Python camera SDK. -
litecam
: The entry point of the Python extension. -
setup.py
: The build script. -
MANIFEST.in
: The manifest file for including non-Python files. -
README.md
: The documentation.
Writing Build Script setup.py
for Python Extension
Add the following content to setup.py
:
from setuptools.command import build_ext
from setuptools import setup, Extension
import sys
import os
import io
from setuptools.command.install import install
import shutil
from pathlib import Path
lib_dir = ''
sources = [
"src/litecam.cpp",
]
include_dirs = [os.path.join(os.path.dirname(__file__), "include")]
libraries = ['litecam']
extra_compile_args = []
if sys.platform == "linux" or sys.platform == "linux2":
lib_dir = 'lib/linux'
extra_compile_args = ['-std=c++11']
extra_link_args = ["-Wl,-rpath=$ORIGIN"]
elif sys.platform == "darwin":
lib_dir = 'lib/macos'
extra_compile_args = ['-std=c++11']
extra_link_args = ["-Wl,-rpath,@loader_path"]
elif sys.platform == "win32":
lib_dir = 'lib/windows'
extra_link_args = []
else:
raise RuntimeError("Unsupported platform")
long_description = io.open("README.md", encoding="utf-8").read()
module_litecam = Extension(
"litecam",
sources=sources,
include_dirs=include_dirs,
library_dirs=[lib_dir],
libraries=libraries,
extra_compile_args=extra_compile_args,
extra_link_args=extra_link_args,
)
def copyfiles(src, dst):
if os.path.isdir(src):
filelist = os.listdir(src)
for file in filelist:
libpath = os.path.join(src, file)
shutil.copy2(libpath, dst)
else:
shutil.copy2(src, dst)
class CustomBuildExt(build_ext.build_ext):
def run(self):
build_ext.build_ext.run(self)
dst = os.path.join(self.build_lib, "litecam")
copyfiles(lib_dir, dst)
filelist = os.listdir(self.build_lib)
for file in filelist:
filePath = os.path.join(self.build_lib, file)
if not os.path.isdir(file):
copyfiles(filePath, dst)
os.remove(filePath)
class CustomBuildExtDev(build_ext.build_ext):
def run(self):
build_ext.build_ext.run(self)
dev_folder = os.path.join(Path(__file__).parent, 'litecam')
copyfiles(lib_dir, dev_folder)
filelist = os.listdir(self.build_lib)
for file in filelist:
filePath = os.path.join(self.build_lib, file)
if not os.path.isdir(file):
copyfiles(filePath, dev_folder)
class CustomInstall(install):
def run(self):
install.run(self)
setup(name='lite-camera',
version='2.0.1',
description='LiteCam is a lightweight, cross-platform library for capturing RGB frames from cameras and displaying them. Designed with simplicity and ease of integration in mind, LiteCam supports Windows, Linux and macOS platforms.',
long_description=long_description,
long_description_content_type="text/markdown",
author='yushulx',
url='https://github.com/yushulx/python-lite-camera',
license='MIT',
packages=['litecam'],
ext_modules=[module_litecam],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Information Technology",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: C++",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Scientific/Engineering",
"Topic :: Software Development",
],
cmdclass={
'install': CustomInstall,
'build_ext': CustomBuildExt,
'develop': CustomBuildExtDev},
)
Explanation:
-
lite-camera
: The name of the Python package. -
ext_modules
: A list of CPython extensions. It specifies the source files, include directories, library directories, libraries, and compile/link flags for different platforms. -
develop
: A custom command for development. It copies the shared libraries to thelitecam
folder for easy testing. -
build_ext
: Customizes the build process for packaging the shared libraries into the wheel package.
Implementing Python Camera SDK API in C++
The pycamera.h
file defines the PyCamera
Python class for capturing frames from the camera, while the pywindow.h
file defines the PyWindow
Python class for displaying the frames in a window. The litecam.cpp
file serves as the entry point for the Python extension, where some global methods are defined, and the PyCamera
and PyWindow
classes are registered.
pycamera.h
Includes
#include <Python.h>
#include <structmember.h>
#include "Camera.h"
-
Python.h
: Included to use the Python C API. -
structmember.h
: Provides macros and helpers for managing object members. -
Camera.h
: Defines theCamera
class, which thePyCamera
extension wraps.
PyCamera Struct Definition
typedef struct
{
PyObject_HEAD Camera *handler;
} PyCamera;
The PyCamera
struct represents the Python object that wraps around the C++ Camera
object. The handler is a pointer to an instance of the Camera
class.
Methods
-
PyCamera_dealloc
static int PyCamera_clear(PyCamera *self) { if (self->handler) { delete self->handler; } return 0; } static void PyCamera_dealloc(PyCamera *self) { PyCamera_clear(self); Py_TYPE(self)->tp_free((PyObject *)self); }
Deallocates the
Camera
object and releases the associated memory. -
PyCamera_new
static PyObject *PyCamera_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyCamera *self; self = (PyCamera *)type->tp_alloc(type, 0); if (self != NULL) { self->handler = new Camera(); } return (PyObject *)self; }
Creates a new instance of
PyCamera
. It allocates memory for the Python object, creates a newCamera
object, and assigns it toself->handler
. -
open
static PyObject *open(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; int index = 0; if (!PyArg_ParseTuple(args, "i", &index)) { return NULL; } bool ret = self->handler->Open(index); return Py_BuildValue("i", ret); }
Opens the camera with the specified index.
-
listMediaTypes
static PyObject *listMediaTypes(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; std::vector<MediaTypeInfo> mediaTypes = self->handler->ListSupportedMediaTypes(); PyObject *pyList = PyList_New(0); for (size_t i = 0; i < mediaTypes.size(); i++) { MediaTypeInfo &mediaType = mediaTypes[i]; #ifdef _WIN32 PyObject *subtypeName = PyUnicode_FromWideChar(mediaType.subtypeName, wcslen(mediaType.subtypeName)); PyObject *pyMediaType = Py_BuildValue("{s:i,s:i,s:O}", "width", mediaType.width, "height", mediaType.height, "subtypeName", subtypeName); if (subtypeName != NULL) { Py_DECREF(subtypeName); } #else PyObject *pyMediaType = Py_BuildValue("{s:i,s:i,s:s}", "width", mediaType.width, "height", mediaType.height, "subtypeName", mediaType.subtypeName); #endif PyList_Append(pyList, pyMediaType); } return pyList; }
Returns a list of supported media types. For Windows, it converts the wide character string to a Python Unicode object.
-
release
static PyObject *release(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; self->handler->Release(); Py_RETURN_NONE; }
Releases the camera.
-
setResolution
static PyObject *setResolution(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; int width = 0, height = 0; if (!PyArg_ParseTuple(args, "ii", &width, &height)) { return NULL; } int ret = self->handler->SetResolution(width, height); return Py_BuildValue("i", ret); }
Sets the resolution of the camera.
-
captureFrame
static PyObject *captureFrame(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; FrameData frame = self->handler->CaptureFrame(); if (frame.rgbData) { PyObject *rgbData = PyByteArray_FromStringAndSize((const char *)frame.rgbData, frame.size); PyObject *pyFrame = Py_BuildValue("iiiO", frame.width, frame.height, frame.size, rgbData); ReleaseFrame(frame); return pyFrame; } else { Py_RETURN_NONE; } }
Captures a frame from the camera and returns the RGB data as a Python byte array.
-
getWidth
andgetHeight
static PyObject *getWidth(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; int width = self->handler->frameWidth; return Py_BuildValue("i", width); } static PyObject *getHeight(PyObject *obj, PyObject *args) { PyCamera *self = (PyCamera *)obj; int height = self->handler->frameHeight; return Py_BuildValue("i", height); }
Returns the width and height of the captured frame.
instance_methods
static PyMethodDef instance_methods[] = {
{"open", open, METH_VARARGS, NULL},
{"listMediaTypes", listMediaTypes, METH_VARARGS, NULL},
{"release", release, METH_VARARGS, NULL},
{"setResolution", setResolution, METH_VARARGS, NULL},
{"captureFrame", captureFrame, METH_VARARGS, NULL},
{"getWidth", getWidth, METH_VARARGS, NULL},
{"getHeight", getHeight, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL}};
Defines the methods that are available on the PyCamera
Python object. These methods are associated with the corresponding C functions defined above.
PyCameraType
static PyTypeObject PyCameraType = {
PyVarObject_HEAD_INIT(NULL, 0) "litecam.PyCamera", /* tp_name */
sizeof(PyCamera), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)PyCamera_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
"PyCamera", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
instance_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
PyCamera_new, /* tp_new */
};
Defines the PyCamera
type, including its methods, memory allocation, deallocation, and other behaviors.
pywindow.h
Includes
#include <Python.h>
#include <structmember.h>
#include "CameraPreview.h"
-
Python.h
: Required to use the Python C API. -
structmember.h
: Provides macros for managing object members. -
CameraPreview.h
: Defines theCameraWindow
class, which is used to display a camera preview and interact with it.
PyWindow Struct Definition
typedef struct
{
PyObject_HEAD CameraWindow *handler;
} PyWindow;
Defines a PyWindow
struct that wraps a C++ CameraWindow
object. The handler member points to an instance of CameraWindow
.
Methods for PyWindow Object
-
PyWindow_dealloc
static int PyWindow_clear(PyWindow *self) { if (self->handler) { delete self->handler; } return 0; } static void PyWindow_dealloc(PyWindow *self) { PyWindow_clear(self); Py_TYPE(self)->tp_free((PyObject *)self); }
Deallocates the
CameraWindow
object and releases the memory. -
PyWindow_new
static PyObject *PyWindow_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyWindow *self; int width = 0, height = 0; char *title = NULL; if (!PyArg_ParseTuple(args, "iis", &width, &height, &title)) { return NULL; } self = (PyWindow *)type->tp_alloc(type, 0); if (self != NULL) { self->handler = new CameraWindow(width, height, title); if (!self->handler->Create()) { std::cerr << "Failed to create window." << std::endl; return NULL; } self->handler->Show(); } return (PyObject *)self; }
Creates a new instance of
PyWindow
. It allocates memory for the Python object, creates a newCameraWindow
object, and assigns it toself->handler
. -
waitKey
static PyObject *waitKey(PyObject *obj, PyObject *args) { PyWindow *self = (PyWindow *)obj; const char *key = NULL; if (!PyArg_ParseTuple(args, "s", &key) || strlen(key) != 1) { PyErr_SetString(PyExc_ValueError, "Expected a single character string"); return NULL; } bool ret = self->handler->WaitKey(key[0]); return Py_BuildValue("i", ret); }
Waits for a key press event and returns
False
if the key matches the specified character. TheFalse
means the application should exit. -
showFrame
static PyObject *showFrame(PyObject *obj, PyObject *args) { PyWindow *self = (PyWindow *)obj; int width = 0, height = 0; PyObject *byteArray = NULL; if (!PyArg_ParseTuple(args, "iiO", &width, &height, &byteArray)) { return NULL; } unsigned char *data = (unsigned char *)PyByteArray_AsString(byteArray); self->handler->ShowFrame(data, width, height); Py_RETURN_NONE; }
Displays a frame in the window.
-
drawContour
static PyObject *drawContour(PyObject *obj, PyObject *args) { PyWindow *self = (PyWindow *)obj; PyObject *pyPoints = NULL; if (!PyArg_ParseTuple(args, "O", &pyPoints)) { return NULL; } std::vector<std::pair<int, int>> points; for (Py_ssize_t i = 0; i < PyList_Size(pyPoints); i++) { PyObject *item = PyList_GetItem(pyPoints, i); int x = PyLong_AsLong(PyTuple_GetItem(item, 0)); int y = PyLong_AsLong(PyTuple_GetItem(item, 1)); points.push_back(std::make_pair(x, y)); } self->handler->DrawContour(points); Py_RETURN_NONE; }
Draws a contour (a series of points) on the frame.
-
drawText
static PyObject *drawText(PyObject *obj, PyObject *args) { PyWindow *self = (PyWindow *)obj; const char *text = NULL; int x = 0, y = 0, fontSize = 0; PyObject *pyColor = NULL; if (!PyArg_ParseTuple(args, "siiiO", &text, &x, &y, &fontSize, &pyColor)) { return NULL; } CameraWindow::Color color; color.r = (unsigned char)PyLong_AsLong(PyTuple_GetItem(pyColor, 0)); color.g = (unsigned char)PyLong_AsLong(PyTuple_GetItem(pyColor, 1)); color.b = (unsigned char)PyLong_AsLong(PyTuple_GetItem(pyColor, 2)); self->handler->DrawText(text, x, y, fontSize, color); Py_RETURN_NONE; }
Draws text on the frame.
window_instance_methods
static PyMethodDef window_instance_methods[] = {
{"waitKey", waitKey, METH_VARARGS, NULL},
{"showFrame", showFrame, METH_VARARGS, NULL},
{"drawContour", drawContour, METH_VARARGS, NULL},
{"drawText", drawText, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL}};
Defines the methods that are available on the PyWindow
Python object.
PyWindowType
static PyTypeObject PyWindowType = {
PyVarObject_HEAD_INIT(NULL, 0) "litecam.PyWindow", /* tp_name */
sizeof(PyWindow), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)PyWindow_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
"PyWindow", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
window_instance_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
PyWindow_new, /* tp_new */
};
Defines the PyWindow
type, including its methods, memory allocation, deallocation, and other behavior.
litecam.cpp
#include <Python.h>
#include <stdio.h>
#include "pycamera.h"
#include "pywindow.h"
#define INITERROR return NULL
static PyObject *getDeviceList(PyObject *obj, PyObject *args)
{
PyObject *pyList = PyList_New(0);
std::vector<CaptureDeviceInfo> devices = ListCaptureDevices();
for (size_t i = 0; i < devices.size(); i++)
{
CaptureDeviceInfo &device = devices[i];
#ifdef _WIN32
PyObject *pyName = PyUnicode_FromWideChar(device.friendlyName, wcslen(device.friendlyName));
if (pyName != NULL)
{
PyList_Append(pyList, pyName);
Py_DECREF(pyName);
}
#else
PyObject *pyName = PyUnicode_FromString(device.friendlyName);
if (pyName != NULL)
{
PyList_Append(pyList, pyName);
Py_DECREF(pyName);
}
#endif
}
return pyList;
}
static PyObject *saveJpeg(PyObject *obj, PyObject *args)
{
const char *filename = NULL;
int width = 0, height = 0;
PyObject *byteArray = NULL;
if (!PyArg_ParseTuple(args, "siiO", &filename, &width, &height, &byteArray))
{
PyErr_SetString(PyExc_TypeError, "Expected arguments: str, int, int, PyByteArray");
return NULL;
}
unsigned char *data = (unsigned char *)PyByteArray_AsString(byteArray);
Py_ssize_t size = PyByteArray_Size(byteArray);
if (size != (Py_ssize_t)(width * height * 3))
{
PyErr_SetString(PyExc_ValueError, "Invalid byte array size for the given width and height.");
return NULL;
}
saveFrameAsJPEG(data, width, height, filename);
Py_RETURN_NONE;
}
static PyMethodDef litecam_methods[] = {
{"getDeviceList", getDeviceList, METH_VARARGS, "Get available cameras"},
{"saveJpeg", saveJpeg, METH_VARARGS, "Get available cameras"},
{NULL, NULL, 0, NULL}};
static struct PyModuleDef litecam_module_def = {
PyModuleDef_HEAD_INIT,
"litecam",
"Internal \"litecam\" module",
-1,
litecam_methods};
PyMODINIT_FUNC PyInit_litecam(void)
{
PyObject *module = PyModule_Create(&litecam_module_def);
if (module == NULL)
INITERROR;
if (PyType_Ready(&PyCameraType) < 0)
{
printf("Failed to initialize PyCameraType\n");
Py_DECREF(module);
return NULL;
}
if (PyModule_AddObject(module, "PyCamera", (PyObject *)&PyCameraType) < 0)
{
printf("Failed to add PyCamera to the module\n");
Py_DECREF(&PyCameraType);
Py_DECREF(module);
INITERROR;
}
if (PyType_Ready(&PyWindowType) < 0)
{
printf("Failed to initialize PyWindowType\n");
Py_DECREF(module);
return NULL;
}
if (PyModule_AddObject(module, "PyWindow", (PyObject *)&PyWindowType) < 0)
{
printf("Failed to add PyWindow to the module\n");
Py_DECREF(&PyWindowType);
Py_DECREF(module);
INITERROR;
}
return module;
}
Explanation:
-
getDeviceList
: Returns a list of available cameras. -
saveJpeg
: Saves a frame as a JPEG image. -
PyInit_litecam
: Initializes thelitecam
module and registers thePyCamera
andPyWindow
types.
Building the Python Camera SDK
-
Development Mode
python setup.py develop
-
Wheel Package
python setup.py bdist_wheel
-
Source Distribution
python setup.py sdist
Steps to Implement a Python Multi-Barcode Scanner
-
Install the Python camera SDK and Dynamsoft Barcode Reader SDK:
pip install lite-camera dynamsoft-capture-vision-bundle
Obtain a 30-day free trial license for Dynamsoft Barcode Reader.
-
Create a Python script for multi-barcode scanning:
import litecam from dynamsoft_capture_vision_bundle import * import queue class FrameFetcher(ImageSourceAdapter): def has_next_image_to_fetch(self) -> bool: return True def add_frame(self, imageData): self.add_image_to_buffer(imageData) class MyCapturedResultReceiver(CapturedResultReceiver): def __init__(self, result_queue): super().__init__() self.result_queue = result_queue def on_captured_result_received(self, captured_result): self.result_queue.put(captured_result) if __name__ == '__main__': errorCode, errorMsg = LicenseManager.init_license( "LICENSE-KEY") if errorCode != EnumErrorCode.EC_OK and errorCode != EnumErrorCode.EC_LICENSE_CACHE_USED: print("License initialization failed: ErrorCode:", errorCode, ", ErrorString:", errorMsg) else: camera = litecam.PyCamera() if camera.open(0): cvr = CaptureVisionRouter() fetcher = FrameFetcher() cvr.set_input(fetcher) result_queue = queue.Queue() receiver = MyCapturedResultReceiver(result_queue) cvr.add_result_receiver(receiver) errorCode, errorMsg = cvr.start_capturing( EnumPresetTemplate.PT_READ_BARCODES.value) if errorCode != EnumErrorCode.EC_OK: print("error:", errorMsg) window = litecam.PyWindow( camera.getWidth(), camera.getHeight(), "Camera Stream") while window.waitKey('q'): frame = camera.captureFrame() if frame is not None: width = frame[0] height = frame[1] size = frame[2] data = frame[3] window.showFrame(width, height, data) imagedata = ImageData( bytes(data), width, height, width * 3, EnumImagePixelFormat.IPF_RGB_888) fetcher.add_frame(imagedata) if not result_queue.empty(): captured_result = result_queue.get_nowait() items = captured_result.get_items() for item in items: if item.get_type() == EnumCapturedResultItemType.CRIT_BARCODE: text = item.get_text() location = item.get_location() x1 = location.points[0].x y1 = location.points[0].y x2 = location.points[1].x y2 = location.points[1].y x3 = location.points[2].x y3 = location.points[2].y x4 = location.points[3].x y4 = location.points[3].y contour_points = [ (x1, y1), (x2, y2), (x3, y3), (x4, y4)] window.drawContour(contour_points) window.drawText(text, x1, y1, 24, (255, 0, 0)) del location camera.release() cvr.stop_capturing()
Replace
LICENSE-KEY
with your Dynamsoft Barcode Reader license key. -
Run the script:
python main.py
Top comments (0)