Skip to content

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

OpenCV Layer for AWS Lambda

Minimal headless OpenCV layers for AWS Lambda, built from source.

Build Release Downloads Python Arch License

  • Small: OpenCV and its codecs are linked statically into a single stripped cv2.abi3.so, without FFmpeg or OpenBLAS.
  • Fast cold start: Precompiled bytecode is included, because Lambda cannot write a bytecode cache to /opt.
  • Runtime-matched: Built inside the official public.ecr.aws/lambda/python images, and every zip is invoked through the Lambda Runtime Interface Emulator before release.
  • Verifiable: Pinned sources and build tools, deterministic zips, SHA256SUMS, a CycloneDX SBOM per zip, and GitHub build provenance attestations.

Quick Start

1. Download a zip from the latest release. The asset URL is stable across releases.

curl -LO https://github.com/076923/aws-lambda-python-opencv-layer/releases/latest/download/opencv-4.14.0-minimal-python3.13-arm64.zip

2. Publish it as a layer. The runtime and architecture must match your function.

aws lambda publish-layer-version \
  --layer-name opencv-minimal \
  --zip-file fileb://opencv-4.14.0-minimal-python3.13-arm64.zip \
  --compatible-runtimes python3.13 \
  --compatible-architectures arm64

Note

Every zip is under the 50 MB limit for direct upload. If your own build is larger, upload it to S3 first and pass --content S3Bucket=<bucket>,S3Key=<key>.

3. Use it in your function.

import base64

import cv2
import numpy as np


def handler(event, context):
    data = np.frombuffer(base64.b64decode(event["image"]), np.uint8)
    img = cv2.imdecode(data, cv2.IMREAD_COLOR)
    thumb = cv2.resize(img, (256, 256), interpolation=cv2.INTER_AREA)
    ok, buf = cv2.imencode(".webp", thumb, [cv2.IMWRITE_WEBP_QUALITY, 80])
    return {"image": base64.b64encode(buf).decode()}

Choose a Variant

Asset name: opencv-{opencv}-{profile}-python{python}-{arch}.zip

Option Values
opencv 4.14.0, 5.1.0
profile minimal, standard, dnn
python 3.12, 3.13, 3.14
arch x86_64, arm64
Profile Modules Use for
minimal core, imgproc, imgcodecs Resize, crop, color conversion, filters, encode and decode
standard minimal + features2d, calib3d, flann, objdetect, photo, video, ml Feature matching, homography, QR codes, inpainting, optical flow
dnn standard + dnn Running ONNX and other models with cv2.dnn

Sizes for Python 3.13 with NumPy 2.5.4 (zipped / unzipped, MB). Other Python versions differ by less than 0.3 MB.

Profile 4.14.0 x86_64 4.14.0 arm64 5.1.0 x86_64 5.1.0 arm64
minimal 12.4 / 33.6 10.5 / 29.2 14.6 / 39.3 13.0 / 35.1
standard 15.7 / 41.7 13.4 / 36.0 17.0 / 44.9 15.0 / 39.8
dnn 18.3 / 48.6 15.7 / 42.0 20.9 / 57.0 18.4 / 50.1

For comparison, the official opencv-python-headless 4.14.0.94 wheel with NumPy is about 72 MB zipped and 215 MB unzipped. Each release lists the size of every asset.

Tip

arm64 (Graviton) is about 20% cheaper per GB-second than x86_64.

Note

OpenCV 5 renamed some modules (features2d → features, calib3d → calib, geometry, stereo). The standard profile includes the equivalent modules for each version.

Limitations

The layers keep only what image processing on Lambda needs.

Not included Impact
FFmpeg, GStreamer, videoio, highgui cv2.VideoCapture, cv2.VideoWriter, and cv2.imshow are not available
TIFF, JPEG 2000, OpenEXR, AVIF, HDR, PNM codecs Only JPEG, PNG, and WebP can be read and written
OpenBLAS (OpenCV and NumPy) NumPy uses its bundled lapack_lite. Large matmul and linalg calls are slower
IPP, OpenCL, TBB, Eigen OpenCV uses its own SIMD kernels and pthreads
Haar cascade XML files Ship the XML files you need with your function. cv2.CascadeClassifier is only available with 4.14.0, because OpenCV 5 removed it from the main repository
python3.11 and older These runtimes run on Amazon Linux 2 and are not supported

Warning

NumPy is built without BLAS. This is fine for image processing, where NumPy mostly holds pixel arrays for OpenCV. If your function uses NumPy as a compute engine (large matrix products, decompositions, solvers), these layers are the wrong choice. Use the official opencv-python-headless and numpy wheels, which bundle OpenBLAS.

Warning

x86_64 binaries require AVX2. cv2 is compiled with CPU_BASELINE=AVX2 and has no fallback path. Every AWS Lambda x86_64 host supports AVX2, but other machines may not, such as older CPUs or some emulators. On a CPU without AVX2, import cv2 stops with an error that the build does not support the current CPU. The arm64 builds use the default NEON baseline.

How It Stays Small

Technique Effect
Static build into one cv2.abi3.so No bundled .libs directories or duplicate runtimes
CPU_BASELINE=AVX2 on x86_64 without runtime dispatch No SSE4, AVX, or AVX-512 copies of the kernels. Every Lambda x86_64 host supports AVX2
-ffunction-sections, --gc-sections, strip Removes unused code and symbols
NumPy without BLAS Removes about 27 MB of OpenBLAS and gfortran runtime
Remove NumPy tests, headers, static libs, f2py, .pyi stubs Removes files that are only needed for development
.pyc compiled with unchecked-hash Zip rounds file times to 2 seconds, so timestamp-checked bytecode would be ignored on every cold start

Testing

Every zip goes through two checks before it can be merged or released. CI fails if either one fails.

Check Where What it verifies
Smoke test Build container, before zipping cv2 and numpy import, and basic image operations work
Lambda invocation Fresh public.ecr.aws/lambda/python container with the zip extracted to a read-only /opt, invoked through the Runtime Interface Emulator with 1024 MB of memory See below

The Lambda invocation runs tests/lambda/handler.py twice, once cold and once warm, and checks:

  • cv2 and numpy load from /opt/python, and the precompiled bytecode is present
  • JPEG, PNG, and WebP round trips
  • ORB matching recovers a known shift through findHomography (standard, dnn)
  • cv2.dnn.blobFromImage (dnn)
  • Each profile has its modules and lacks the ones it excludes, such as cv2.dnn in standard and cv2.VideoCapture everywhere
  • Import time, a 12 MP decode → resize → blur → WebP pipeline, and peak memory, which are recorded in each zip's .json

Note

The emulator runs on GitHub-hosted runners, so the timings are indicative only. Cold start, memory, and latency on AWS Lambda itself are not measured.

To run the Lambda invocation locally after a build, run python3 scripts/test_layer.py dist.

Supply Chain

Item How
Source archives The OpenCV and NumPy source archives are verified against SHA256 hashes pinned in scripts/build_layer.py before building
Build tools CMake, Ninja, Meson, meson-python, and Cython versions are pinned. NumPy builds without build isolation, so only the pinned tools are used
Build environment The Lambda image digest, GCC version, and tool versions are recorded in each zip's .json
GitHub Actions Actions are pinned to commit SHAs and updated by Dependabot
SBOM Each release includes a CycloneDX SBOM (.cdx.json) per zip, listing OpenCV, NumPy, and the third-party libraries linked into cv2
Provenance Each zip has a GitHub build provenance attestation

SHA256SUMS proves that a download is intact. The attestation also proves that the zip was built by this repository's workflow:

gh attestation verify opencv-4.14.0-minimal-python3.13-arm64.zip -R 076923/aws-lambda-python-opencv-layer

Build from Source

Requires Docker. Building arm64 on an x86_64 machine uses QEMU emulation and is slow.

# ./build.sh <python> <arch> all <opencv> <profile> [--numpy <version>]
./build.sh 3.13 arm64 all 4.14.0 minimal
dist/opencv-4.14.0-minimal-python3.13-arm64.zip
dist/opencv-4.14.0-minimal-python3.13-arm64.json       # versions, sizes, build info, test results
dist/opencv-4.14.0-minimal-python3.13-arm64.cdx.json   # CycloneDX SBOM
python3 scripts/test_layer.py dist   # invoke every zip through the Lambda emulator
Build stages

The build has three stages, which CI runs as separate jobs.

Stage Runs once per Output
opencv OpenCV × profile × arch cv2 built with the Python Limited API, so one binary serves Python 3.12 and later
numpy Python × arch NumPy without BLAS
package Python × arch Every zip for that runtime, after a smoke test
./build.sh 3.12 x86_64 opencv 4.14.0 standard   # artifacts/opencv-4.14.0-standard-x86_64.tar.gz
./build.sh 3.13 x86_64 numpy                    # artifacts/numpy-2.5.4-python3.13-x86_64.tar.gz
./build.sh 3.13 x86_64 package                  # dist/*.zip for every built cv2
Layer structure

Lambda extracts the layer to /opt, and /opt/python is on sys.path.

opencv-{opencv}-{profile}-python{python}-{arch}.zip
└── python/
    ├── cv2/
    │   ├── __init__.py
    │   ├── python-3/cv2.abi3.so
    │   └── licenses/
    ├── numpy/
    └── numpy-{version}.dist-info/

Release

Push a v* tag. GitHub Actions builds and tests all 36 zips, then attaches them to the release with their SBOMs, SHA256SUMS, provenance attestations, and a size table.

To change the OpenCV or NumPy version, update the workflow matrix and add the SHA256 of the new source archive to OPENCV_SOURCES or NUMPY_SOURCES in scripts/build_layer.py.

git tag v2026.11.0
git push origin v2026.11.0

Author

Daehee Yun ([email protected])