diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000000..4a8d0290707c --- /dev/null +++ b/.clang-format @@ -0,0 +1,44 @@ +--- +# Formatting rules for this repository. +# +# Scope: use it on code you add or rewrite. The historical tree predates +# clang-format and is NOT reformatted wholesale - doing so would bury real +# changes under thousands of cosmetic lines. ColumnLimit is therefore 0, which +# tells clang-format to keep the line breaks the author wrote. +# +# clang-format --style=file -i # format in place +# clang-format --style=file --dry-run -Werror # check only +# +# CI validates that this file parses (a broken config silently turns every +# local format run into a no-op) and enforces the whitespace rules below. +Language: Cpp +BasedOnStyle: LLVM + +# OpenCV code style: 4 spaces, never tabs, braces on the same line for control +# statements and on the next line for functions/classes/namespaces. +IndentWidth: 4 +ContinuationIndentWidth: 4 +TabWidth: 4 +UseTab: Never +BreakBeforeBraces: Linux +IndentCaseLabels: false +AccessModifierOffset: -4 + +# Do not rewrap existing code: ColumnLimit 0 = respect existing line breaks. +ColumnLimit: 0 + +# Header order in OpenCV is meaningful (project headers before 3rdparty ones). +SortIncludes: false +IncludeBlocks: Preserve + +# OpenCV does not use "using namespace" sprinkles in headers and keeps braces +# around single-statement bodies; keep the diffs local and predictable. +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +SpaceBeforeParens: ControlStatements +PointerAlignment: Right +DerivePointerAlignment: false +IndentPPDirectives: None +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 2 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000000..2e42942e92e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Keep the build context small and deterministic: a Docker build must not depend +# on leftovers from a previous local build, and must never ship local state into +# an image layer. +.git +.github +build/ +.cache/ +ccache/ +sdk/ +junk/ + +# Editor / OS noise +.idea/ +.vs/ +.vscode/ +*.swp +.DS_Store +Thumbs.db + +# Never bake credentials or local configuration into an image +.env +*.pem +*.key + +# Docs and CI are not needed to compile +doc/ +docs/ +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 000000000000..94d4dfb31324 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Copy to .env and adjust: cp .env.example .env +# +# docker-compose.yml reads these; nothing here is a secret. There is no +# credential, token or key in this build: the build is fully offline after the +# container image and the optional opencv_extra checkout are in place. + +# Parallel compile jobs. Keep this at or below the number of cores the container +# can actually use, otherwise the build thrashes instead of getting faster. +BUILD_JOBS=4 + +# Test fixtures for the accuracy tests. Clone the matching data once: +# git clone -b 3.0 --depth 1 https://github.com/opencv/opencv_extra ../opencv_extra +# then point this at the clone (absolute path). Leave empty to configure without +# data-driven tests; those tests report FAIL_INVALID_TEST_DATA when their +# fixtures are missing. +OPENCV_TEST_DATA_PATH= diff --git a/.gitattributes b/.gitattributes index 7497ee17687d..fb223b853752 100644 --- a/.gitattributes +++ b/.gitattributes @@ -82,3 +82,30 @@ org.eclipse.jdt.core.prefs -text whitespace=cr-at-eol merge=union *.vcxproj text eol=crlf -whitespace merge=union *.sh text eol=lf + +# --------------------------------------------------------------------------- +# Generated / vendored files. +# +# Marking these as generated keeps tooling (language statistics, review views, +# "files you should look at" heuristics) honest: nobody should review a table of +# 16394 precomputed floats, and a real regression in hand-written code must not +# hide behind generated noise. +# --------------------------------------------------------------------------- + +# Lookup tables emitted by OpenEXR's table generator. +3rdparty/openexr/IlmImf/b44ExpLogTable.h linguist-generated=true -diff +3rdparty/openexr/Half/toFloat.h linguist-generated=true -diff +3rdparty/openexr/Half/toFloat.cpp linguist-generated=true -diff +3rdparty/openexr/IlmImf/b44ExpLogTable.cpp linguist-generated=true -diff + +# Header parsers used by the C++/Java/Python binding generators. +modules/python/src2/hdr_parser.py linguist-generated=true + +# Headers produced by CMake at configure time (not in the tree today, but the +# same rule should apply if they are ever committed). +**/opencv2/opencv_modules.hpp linguist-generated=true +**/cvconfig.h linguist-generated=true + +# Test data and models: not source, not documentation. +data/** linguist-generated=true +modules/**/test/testdata/** linguist-generated=true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..60b154490676 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,41 @@ +version: 2 + +updates: + # Keep the CI pipeline itself patched: a compromised or outdated action is a + # supply-chain entry point into every build of this repository. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + - ci + commit-message: + prefix: 'ci(deps)' + + # The pinned Python tooling (requirements-dev.txt) is audited by CI; Dependabot + # keeps the pins current and the advisory list empty by construction. + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + - security + commit-message: + prefix: 'build(deps)' + + # The container image is part of the developer and CI toolchain. + - package-ecosystem: docker + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 3 + labels: + - dependencies + commit-message: + prefix: 'build(deps)' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89182266bc1f..6cb40584c7b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,211 @@ name: CI + +# Build + test on every push and pull request. +# Anything merged without a green run here is unverified, so failures are never +# suppressed (no `continue-on-error`, no `|| true` in the build/test steps). on: push: - branches: [main, master] + branches: [master, main] pull_request: - branches: [main, master] + workflow_dispatch: + +# Least privilege: no job needs to write to the repository, create releases or +# read secrets. If a dependency is ever compromised, this limits the blast radius. +permissions: + contents: read + +# A new push to a PR cancels the previous in-flight run for that ref. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + BUILD_DIR: build + # ccache makes the second and every later build of a branch nearly free. + CCACHE_DIR: ${{ github.workspace }}/.ccache + jobs: - build: + build-and-test: + name: Build & run test suite runs-on: ubuntu-latest + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - run: mkdir build && cd build && cmake .. && make -j$(nproc) - - run: cd build && make test + + - name: Install build toolchain + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential ccache cmake ninja-build pkg-config + + - name: Fetch test data (opencv_extra, branch 3.0) + # The accuracy tests read their fixtures from opencv_extra. Without it + # data-driven tests report FAIL_INVALID_TEST_DATA instead of passing. + # The branch must match this tree: 3.0 for CV_VERSION 3.0.0-dev. + run: | + git clone --depth 1 --branch 3.0 https://github.com/opencv/opencv_extra.git \ + "$RUNNER_TEMP/opencv_extra" + echo "OPENCV_TEST_DATA_PATH=$RUNNER_TEMP/opencv_extra" >> "$GITHUB_ENV" + + - name: Restore ccache + uses: actions/cache@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-${{ runner.os }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'modules/**/CMakeLists.txt', '.github/workflows/ci.yml') }} + restore-keys: | + ccache-${{ runner.os }}- + + - name: Configure + # Keep the module set explicit: the same flags are used by the Dockerfile + # and documented in README.md, so local/CI/container builds agree. + run: | + cmake -S . -B "$BUILD_DIR" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DOPENCV_TEST_DATA_PATH="$OPENCV_TEST_DATA_PATH" \ + -DBUILD_TESTS=ON \ + -DBUILD_PERF_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DBUILD_DOCS=OFF \ + -DBUILD_opencv_apps=OFF \ + -DBUILD_opencv_java=OFF \ + -DBUILD_opencv_python2=OFF \ + -DBUILD_opencv_python3=OFF \ + -DBUILD_opencv_viz=OFF \ + -DBUILD_opencv_videostab=OFF \ + -DBUILD_opencv_superres=OFF \ + -DBUILD_opencv_cudaarithm=OFF \ + -DBUILD_opencv_cudabgsegm=OFF \ + -DBUILD_opencv_cudacodec=OFF \ + -DBUILD_opencv_cudafeatures2d=OFF \ + -DBUILD_opencv_cudafilters=OFF \ + -DBUILD_opencv_cudaimgproc=OFF \ + -DBUILD_opencv_cudalegacy=OFF \ + -DBUILD_opencv_cudaobjdetect=OFF \ + -DBUILD_opencv_cudaoptflow=OFF \ + -DBUILD_opencv_cudastereo=OFF \ + -DBUILD_opencv_cudawarping=OFF \ + -DBUILD_opencv_cudev=OFF \ + -DBUILD_ZLIB=ON \ + -DBUILD_PNG=ON \ + -DBUILD_JPEG=ON \ + -DBUILD_TIFF=ON \ + -DBUILD_OPENEXR=ON \ + -DWITH_1394=OFF \ + -DWITH_CUDA=OFF \ + -DWITH_EIGEN=OFF \ + -DWITH_FFMPEG=OFF \ + -DWITH_GSTREAMER=OFF \ + -DWITH_GTK=OFF \ + -DWITH_IPP=OFF \ + -DWITH_JASPER=OFF \ + -DWITH_OPENCL=OFF \ + -DWITH_TBB=OFF \ + -DWITH_V4L=OFF \ + -DWITH_WEBP=OFF + + - name: Build + run: cmake --build "$BUILD_DIR" --parallel "$(nproc)" + + - name: Test + # --output-on-failure is what makes a red run actionable; --timeout + # stops one hung test from burning the whole 60 minute budget. + run: ctest --test-dir "$BUILD_DIR" --output-on-failure --timeout 900 + + lint: + name: Lint & repository hygiene + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install lint tooling + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang-format + python -m pip install --disable-pip-version-check pyyaml + + - name: clang-format configuration must be valid + # A malformed .clang-format silently makes every local `clang-format -i` + # a no-op or an error, so validate it in CI. + run: | + clang-format --version + clang-format --dump-config > /dev/null + clang-format --style=file --dump-config > /dev/null + + - name: No trailing whitespace in build and docs files we own + run: | + set -euo pipefail + files=$(git ls-files -- \ + '.github/*' 'Dockerfile' 'docker-compose.yml' 'requirements*.txt' \ + 'docs/*' '*.md' 'SECURITY.md') + if [ -z "$files" ]; then + echo "no files matched the hygiene path list" >&2 + exit 1 + fi + if grep -nE '[[:blank:]]+$' $files; then + echo "trailing whitespace found (see above)" >&2 + exit 1 + fi + + - name: No hard tabs in YAML (YAML forbids tabs for indentation) + run: | + set -euo pipefail + files=$(git ls-files -- '*.yml' '*.yaml') + if [ -z "$files" ]; then + echo "no YAML files tracked" >&2 + exit 1 + fi + if grep -nP '\t' $files; then + echo "tab characters found in YAML (see above)" >&2 + exit 1 + fi + + - name: Workflow files must be valid YAML + run: | + set -euo pipefail + python - <<'PY' + import glob, sys + try: + import yaml + except ImportError: + sys.exit("PyYAML is required by this CI step") + failed = False + for path in glob.glob(".github/workflows/*.yml"): + with open(path, encoding="utf-8") as fh: + try: + yaml.safe_load(fh) + except Exception as exc: # noqa: BLE001 - report and fail the job + print(f"{path}: {exc}", file=sys.stderr) + failed = True + else: + print(f"{path}: ok") + sys.exit(1 if failed else 0) + PY + + dependency-audit: + name: Dependency audit + # Flag known-vulnerable pinned Python dependencies on every PR. + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install pinned tooling + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-dev.txt + + - name: pip-audit + # --strict fails on unpinned requirements as well as on advisories. + run: python -m pip_audit --requirement requirements-dev.txt --strict diff --git a/.gitignore b/.gitignore index 039d2400a6fb..78dc55dc9552 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,21 @@ bin/ CMakeCache.txt *.suo *.log -*.tlog \ No newline at end of file +*.tlog + +# Out-of-source build trees. The documented layout is /build (see +# README.md); the extra patterns catch the historical in-tree and nested +# variants so a stray CMakeCache.txt is never committed. +/build/ +/sdk/ +/junk/ +/.cache/ +ccache/ +CMakeCache.txt +CMakeFiles/ +compile_commands.json +*.junk +*.pyo +__pycache__/ +# Local configuration, never committed (.env.example IS committed). +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..de6c4a5c042d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,105 @@ +# syntax=docker/dockerfile:1 + +# Reproducible build/test environment for this repository. +# +# docker build -t opencv-dev . +# docker run --rm opencv-dev # configure + build + test +# docker run --rm opencv-dev ctest --test-dir /src/build --output-on-failure +# docker run --rm -it opencv-dev bash # interactive shell +# +# The CMake flags below are intentionally identical to .github/workflows/ci.yml, +# so a green container build and a green CI run exercise the same code paths. + +# --------------------------------------------------------------------------- +# Stage 1: toolchain only. Kept separate so that dependency changes invalidate +# the least amount of work and the toolchain can be rebuilt/reused on its own. +# --------------------------------------------------------------------------- +FROM ubuntu:24.04 AS toolchain + +ARG DEBIAN_FRONTEND=noninteractive + +# No GUI/media backends are installed: WITH_GTK/WITH_FFMPEG/WITH_CUDA are off, so +# the image only carries what the build and the accuracy tests need. Fewer +# packages also means a smaller CVE surface for the image itself. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + cmake \ + ninja-build \ + pkg-config \ + python3 \ + && rm -rf /var/lib/apt/lists/* + +ENV CCACHE_DIR=/ccache + +# --------------------------------------------------------------------------- +# Stage 2: source + build. `docker run` on this image runs the test suite. +# --------------------------------------------------------------------------- +FROM toolchain AS dev + +# Parallelism for `cmake --build`. Override: docker build --build-arg BUILD_JOBS=8 +ARG BUILD_JOBS=4 +# Point at a checkout of https://github.com/Itseez/opencv_extra (branch 3.0) to +# let the data-driven accuracy tests find their fixtures. Leave empty to skip. +ARG OPENCV_TEST_DATA_PATH="" + +WORKDIR /src +COPY . /src + +# hermetic: every third-party image codec is built from the bundled 3rdparty +# sources, so the result does not depend on the distribution's package versions. +RUN cmake -S /src -B /src/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DOPENCV_TEST_DATA_PATH="${OPENCV_TEST_DATA_PATH}" \ + -DBUILD_TESTS=ON \ + -DBUILD_PERF_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DBUILD_DOCS=OFF \ + -DBUILD_opencv_apps=OFF \ + -DBUILD_opencv_java=OFF \ + -DBUILD_opencv_python2=OFF \ + -DBUILD_opencv_python3=OFF \ + -DBUILD_opencv_viz=OFF \ + -DBUILD_opencv_videostab=OFF \ + -DBUILD_opencv_superres=OFF \ + -DBUILD_opencv_cudaarithm=OFF \ + -DBUILD_opencv_cudabgsegm=OFF \ + -DBUILD_opencv_cudacodec=OFF \ + -DBUILD_opencv_cudafeatures2d=OFF \ + -DBUILD_opencv_cudafilters=OFF \ + -DBUILD_opencv_cudaimgproc=OFF \ + -DBUILD_opencv_cudalegacy=OFF \ + -DBUILD_opencv_cudaobjdetect=OFF \ + -DBUILD_opencv_cudaoptflow=OFF \ + -DBUILD_opencv_cudastereo=OFF \ + -DBUILD_opencv_cudawarping=OFF \ + -DBUILD_opencv_cudev=OFF \ + -DBUILD_ZLIB=ON \ + -DBUILD_PNG=ON \ + -DBUILD_JPEG=ON \ + -DBUILD_TIFF=ON \ + -DBUILD_OPENEXR=ON \ + -DWITH_1394=OFF \ + -DWITH_CUDA=OFF \ + -DWITH_EIGEN=OFF \ + -DWITH_FFMPEG=OFF \ + -DWITH_GSTREAMER=OFF \ + -DWITH_GTK=OFF \ + -DWITH_IPP=OFF \ + -DWITH_JASPER=OFF \ + -DWITH_OPENCL=OFF \ + -DWITH_TBB=OFF \ + -DWITH_V4L=OFF \ + -DWITH_WEBP=OFF \ + && cmake --build /src/build --parallel "${BUILD_JOBS}" + +# The accuracy test binaries read the fixture location from the environment at +# run time, so it has to be exported as well as passed to CMake. +ENV OPENCV_TEST_DATA_PATH=${OPENCV_TEST_DATA_PATH} + +# ctest is the documented entry point (see README.md "Running the tests"). +CMD ["ctest", "--test-dir", "/src/build", "--output-on-failure", "--timeout", "900"] diff --git a/README.md b/README.md index b531492fb4bf..538dde466387 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Gittip](http://img.shields.io/gittip/OpenCV.png)](https://www.gittip.com/OpenCV/) +This repository is a mirror of the OpenCV 3.0 development tree (`CV_VERSION 3.0.0-dev`). +It builds the C++ library, the optional bindings and the accuracy test suite with CMake. + #### Resources * Homepage: @@ -24,17 +27,134 @@ Summary of guidelines: ## Getting Started ### Prerequisites -- CMake -- C++ compiler -### Install +| Requirement | Version | Notes | +| ----------- | ------- | ----- | +| CMake | >= 3.1 | `cmake_minimum_required` in the top-level `CMakeLists.txt` | +| C++ compiler | C++11 | GCC >= 4.8, Clang >= 3.4, MSVC >= 2015 | +| Python | 2.7 or 3.4+ | only for the Python bindings and the binding generators | +| JDK + Ant | 1.7+ | only for the Java bindings and the Java test suite | + +No third-party image libraries are required: the build compiles the bundled `zlib`, +`libpng`, `libjpeg`, `libtiff` and OpenEXR sources from `3rdparty/`. + +### Build + +Always configure into a separate build directory; in-tree builds are not supported +and leave generated files all over the source tree. + ```bash mkdir build && cd build cmake .. make -j$(nproc) ``` +Equivalently, with Ninja and explicit parallelism: + +```bash +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel $(nproc) +``` + +For a fast, deterministic developer/CI build that skips the GPU, GUI, codec-backend +and binding modules, use the flag set in [`Dockerfile`](Dockerfile) / +[`.github/workflows/ci.yml`](.github/workflows/ci.yml). The essential part is: + +```bash +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=ON -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF \ + -DBUILD_opencv_java=OFF -DBUILD_opencv_python2=OFF -DBUILD_opencv_python3=OFF \ + -DBUILD_opencv_viz=OFF -DWITH_CUDA=OFF -DWITH_OPENCL=OFF -DWITH_FFMPEG=OFF \ + -DWITH_GTK=OFF -DWITH_IPP=OFF +cmake --build build --parallel $(nproc) +``` + +Module selection works through the per-module options (`-DBUILD_opencv_=OFF`); +disabling a module also disables everything that depends on it. Two traps worth +knowing: `modules/viz` needs a GUI/graphics backend, `modules/videoio` needs a +system video backend (FFmpeg/GStreamer), and the internal test-support module +`opencv_ts` links `highgui` - so disabling `highgui`, `videoio` or `imgcodecs` +silently removes the whole accuracy test suite, not just one module's. + +See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the module layout and +dependency graph. + ### Test + +The accuracy and regression tests read their fixtures from a checkout of +[opencv_extra](https://github.com/opencv/opencv_extra) (branch `3.0` for this +tree). Without it, data-driven tests report `FAIL_INVALID_TEST_DATA` instead of +passing, so set it up once: + +```bash +git clone -b 3.0 --depth 1 https://github.com/opencv/opencv_extra ../opencv_extra +``` + +Then configure with the data path and run the suite: + +```bash +cmake -S . -B build -DOPENCV_TEST_DATA_PATH="$PWD/../opencv_extra" -DBUILD_TESTS=ON +cmake --build build --parallel $(nproc) +ctest --test-dir build --output-on-failure +``` + +Useful variations: + +```bash +ctest --test-dir build --output-on-failure -R video # one module +ctest --test-dir build --output-on-failure -j4 # parallel +./bin/opencv_test_core --gtest_filter=Mat.* # one suite, directly +./bin/opencv_perf_core --gtest_filter=Mat # performance tests +``` + +Details, including how to run the Java binding tests, are in +[`docs/TESTING.md`](docs/TESTING.md). + +### Environment variables + +Build-time (read by CMake; see `git grep '\$ENV{' -- '*.cmake'` for the full list): + +| Variable | Used for | +| -------- | -------- | +| `OPENCV_TEST_DATA_PATH` | Fixture directory handed to the test binaries | +| `OPENCV_IPP_PATH`, `IPP_ASYNC_ROOT` | Intel IPP / IPP Async locations (when `WITH_IPP=ON`) | +| `EIGEN_ROOT` | Eigen headers (when `WITH_EIGEN=ON`) | +| `NVSDKCOMPUTE_ROOT` | CUDA toolkit (when `WITH_CUDA=ON`) | +| `ANT_DIR` | Ant installation for the Java bindings | +| `OPENCV_FFMPEG_URL`, `OPENCV_ICV_URL` | Override the prebuilt-binary download URLs | +| `CCACHE_DIR` | Compiler cache location when using `ccache` as compiler launcher | + +Build-time knobs used by the container workflow (see [`.env.example`](.env.example)): +`BUILD_JOBS`, `OPENCV_TEST_DATA_PATH`. There are no credentials in this build; +`.env` is git-ignored and `.env.example` is committed. + +### Container + +```bash +docker build -t opencv-dev . # toolchain + build +docker run --rm opencv-dev # run ctest inside the image +docker run --rm -it opencv-dev bash # interactive shell +``` + +or with compose, which mounts the working tree and keeps `build/` and the ccache +in named volumes so rebuilds are incremental: + ```bash -make test +cp .env.example .env +docker compose build +docker compose run --rm opencv # configure + build + test ``` + +### Troubleshooting + +* **`make: *** No rule to make target` / missing `opencv_test_*` targets** - the + test executables only exist with `BUILD_TESTS=ON`, and every module's tests + require `opencv_imgcodecs` and `opencv_videoio` to be enabled. +* **Tests fail with `FAIL_INVALID_TEST_DATA`** - `OPENCV_TEST_DATA_PATH` is unset + or points at the wrong `opencv_extra` branch (`3.0` for this tree). +* **`cv::Exception` from `Video`/codec tests** - no video backend was detected; + install FFmpeg or configure with `WITH_FFMPEG=OFF` and skip those tests. +* **Stale configuration after switching options** - CMake caches everything, so + remove `build/` and reconfigure rather than toggling flags in place. +* **Compilation of `modules/viz` fails** - it needs a GUI/graphics backend + (GTK or Qt); configure with `-DBUILD_opencv_viz=OFF`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..0ac1e63119cf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,90 @@ +# Security Policy + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately rather than in a public issue. +Use GitHub's **Security advisory - Report a vulnerability** form on this +repository (`Security` tab - `Report a vulnerability`). Do not open a public +pull request or issue for an unfixed vulnerability. + +Please include: affected module and version (`3.0.0-dev`, `git rev-parse HEAD`), +the platform, a minimal reproducer, and the impact (crash, memory corruption, +out-of-bounds read/write, file-write, code execution, DoS). + +Target response times: + +| Stage | Target | +| ----- | ------ | +| Acknowledgement | 3 working days | +| Triage and impact assessment | 10 working days | +| Fix or mitigation plan | 20 working days | +| Public disclosure | after a fix is released, or 90 days, whichever comes first | + +Disclosure is coordinated with the reporter. A CVE is requested for issues that +affect users of the released library; everything is credited to the reporter +unless anonymity is requested. + +## Supported versions + +This repository tracks the OpenCV 3.0 development line. Fixes land on the +default branch (`master`); backports to other branches are not provided. + +| Version | Supported | +| ------- | --------- | +| `3.0.0-dev` (`master`) | yes | +| any other branch/tag | no - upgrade to `master` | + +## Hardening notes for users and reviewers + +OpenCV parses attacker-controlled data in several places. When you review or +patch code in these areas, treat every length, offset and count read from a file +as hostile: + +* `modules/imgcodecs/src/grfmt_*.cpp` - image header parsers (BMP, GIF, JPEG, + PNG, TIFF, RAS, PXM, ...). Header fields drive allocations and loop bounds. +* `modules/imgcodecs/src/bitstrm.cpp`, `loadsave.cpp` - stream reads. +* `modules/core/src/persistence.cpp` - the XML/YAML reader behind + `cv::FileStorage`. Never feed it a file from an untrusted source, and prefer + `FileStorage` in read mode only. +* `modules/videoio/src/cap_*.cpp` - container/stream parsers for FFmpeg, GStreamer + and image sequences. +* `modules/core/src/*.cpp` - `Mat` indexing, ROI/submatrix arithmetic, and the + resize/remap geometry code; sizes come from the caller and are easy to + overflow. + +Rules the codebase follows, which new code must follow too: + +1. **Validate at the boundary.** Public entry points check types, sizes, channel + counts and non-negativity with `CV_Assert`/`CV_Error` and throw + `cv::Exception`; they never trust a size that came from a file or a pointer + arithmetic result. +2. **Bounded copies.** Use `memcpy` with a length that has been validated + against both buffer sizes, or the `cv::`/`std::` helpers that do. No + unbounded `strcpy`/`sprintf` on data derived from an input file. +3. **Checked arithmetic.** Compute allocation sizes and pixel counts in a type + that cannot silently wrap (e.g. `size_t`, or `cv::` size helpers) before + allocating. +4. **No undefined behaviour.** No uninitialised reads, no signed overflow, no + out-of-bounds access, no reliance on `NULL` checks that do not happen. +5. **Deterministic errors.** Report failures through `CV_Error` with a specific + `Error::St*` code rather than asserting or aborting, so callers can + distinguish "bad input" from "out of memory". + +## Build and supply chain + +* Every third-party dependency is either vendored under `3rdparty/` or pinned + exactly in a requirements file (`requirements-dev.txt`, + `modules/python/requirements.txt`); nothing floats on a version range. +* CI (`.github/workflows/ci.yml`) runs with `permissions: contents: read`, so a + compromised action or dependency cannot push, release or read secrets from a + job. +* CI installs the toolchain it needs explicitly and audits the pinned Python + dependencies with `pip-audit --strict` on every pull request. +* `Dockerfile` builds from a pinned base image tag with a minimal package set and + no credentials; `.dockerignore` keeps `.git`, local `.env` files, keys and + build trees out of the build context and the image. +* Dependabot (`.github/dependabot.yml`) keeps the actions, container base and + pinned Python tooling current. +* There are no secrets in this repository. `.env.example` documents the build + knobs; the real `.env` is git-ignored, and any future credential must come + from the CI secret store or the environment, never from a committed file. diff --git a/docker-compose.yml b/docker-compose.yml index 30d3e1021f65..ae6146e380fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,70 @@ +# Build/dev/test environment for the OpenCV build. +# +# docker compose build # build the image +# docker compose run --rm opencv # configure + build + test +# docker compose run --rm opencv ctest --test-dir /src/build --output-on-failure +# docker compose run --rm opencv bash # interactive shell in the toolchain +# +# All tunables come from a .env file next to this one; copy .env.example to .env +# and adjust. Nothing is hard-coded here so that local runs, CI and the image +# stay in sync through configuration instead of copy-paste. + services: opencv: - build: . + build: + context: . + dockerfile: Dockerfile + target: dev + args: + BUILD_JOBS: ${BUILD_JOBS:-4} + OPENCV_TEST_DATA_PATH: ${OPENCV_TEST_DATA_PATH:-} + image: opencv-dev:local + init: true + working_dir: /src + environment: + BUILD_JOBS: ${BUILD_JOBS:-4} + CCACHE_DIR: /ccache + # Export this in your shell (or .env) after cloning opencv_extra, e.g. + # git clone -b 3.0 --depth 1 https://github.com/Itseez/opencv_extra .. + OPENCV_TEST_DATA_PATH: ${OPENCV_TEST_DATA_PATH:-} volumes: - - .:/app + # Live source: edit on the host, rebuild inside the container. The mount + # point is /src, exactly where the image built, so the CMake cache baked + # into the image stays valid and rebuilds are incremental. + - .:/src + # Named volumes (not host bind mounts) keep the caches out of the working + # tree and out of .gitignore's way. Docker seeds them from the image on + # first use, so the prebuilt build/ and the ccache survive. + - build-cache:/src/build + - ccache:/ccache + command: + - bash + - -lc + - | + set -euo pipefail + cmake -S /src -B /src/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DOPENCV_TEST_DATA_PATH="$${OPENCV_TEST_DATA_PATH}" \ + -DBUILD_TESTS=ON -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF \ + -DBUILD_DOCS=OFF -DBUILD_opencv_apps=OFF -DBUILD_opencv_java=OFF \ + -DBUILD_opencv_python2=OFF -DBUILD_opencv_python3=OFF \ + -DBUILD_opencv_viz=OFF -DBUILD_opencv_videostab=OFF \ + -DBUILD_opencv_superres=OFF -DBUILD_opencv_cudaarithm=OFF \ + -DBUILD_opencv_cudabgsegm=OFF -DBUILD_opencv_cudacodec=OFF \ + -DBUILD_opencv_cudafeatures2d=OFF -DBUILD_opencv_cudafilters=OFF \ + -DBUILD_opencv_cudaimgproc=OFF -DBUILD_opencv_cudalegacy=OFF \ + -DBUILD_opencv_cudaobjdetect=OFF -DBUILD_opencv_cudaoptflow=OFF \ + -DBUILD_opencv_cudastereo=OFF -DBUILD_opencv_cudawarping=OFF \ + -DBUILD_opencv_cudev=OFF -DBUILD_ZLIB=ON -DBUILD_PNG=ON \ + -DBUILD_JPEG=ON -DBUILD_TIFF=ON -DBUILD_OPENEXR=ON \ + -DWITH_1394=OFF -DWITH_CUDA=OFF -DWITH_EIGEN=OFF -DWITH_FFMPEG=OFF \ + -DWITH_GSTREAMER=OFF -DWITH_GTK=OFF -DWITH_IPP=OFF -DWITH_JASPER=OFF \ + -DWITH_OPENCL=OFF -DWITH_TBB=OFF -DWITH_V4L=OFF -DWITH_WEBP=OFF + cmake --build /src/build --parallel "$${BUILD_JOBS}" + exec ctest --test-dir /src/build --output-on-failure --timeout 900 + +volumes: + build-cache: + ccache: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000000..603fdc6588dc --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,139 @@ +# Architecture + +This document describes how the tree is organised, how the modules depend on +each other, and which parts are generated. It is the map to consult before +touching anything: most "where does this belong?" questions are answered here. + +## Top-level layout + +``` +3rdparty/ vendored third-party sources (libjpeg, libpng, zlib, libtiff, + OpenEXR, protobuf, tbb, qt/...) - built only when the matching + BUILD_* option is ON. Never edited by hand. +apps/ command line tools (train_intensity_list, opencv_version, ...) +cmake/ the build system: module machinery, dependency detection, + code generation helpers +data/ pretrained models and cascades shipped with the library +doc/ doxygen/plantuml sources for the API documentation +include/ legacy C and C++ headers (opencv/cv.h, opencv2/opencv.hpp) +modules/ the library itself, one directory per module +platforms/ platform glue: android, ios, osx, winrt, build scripts +samples/ example programs (cpp, java, python2, gpu, directx, ...) +``` + +`modules/` is the only place new library code belongs. + +## Module anatomy + +Every module follows the same shape: + +``` +modules// + CMakeLists.txt ocv_define_module( [WRAP ]) + include/opencv2/ public headers (the API surface) + src/ implementation, one file per algorithm family + test/ accuracy + regression tests (target opencv_test_) + perf/ performance tests (target opencv_perf_) + misc/ samples, java/python glue, filelists + doc/ module documentation +``` + +`CMakeLists.txt` is one line by convention: + +```cmake +set(the_description "Video Analysis") +ocv_define_module(video opencv_imgproc WRAP java python) +``` + +* the dependency list is explicit - CMake disables a module automatically when a + dependency is missing, so never rely on "it happens to be built"; +* `WRAP` marks the module as wrapped by the Java/Python binding generators. + +## Module dependency graph + +The authoritative source is each module's `CMakeLists.txt`; the table below is +transcribed from them. Layers, from the bottom up: `core` -> primitives -> +algorithms -> applications -> bindings. + +| Module | Class | Direct dependencies | +| ------ | ----- | ------------------- | +| `core` | PUBLIC | - (matrix types, linear algebra, persistence, OpenCL/OpenGL primitives) | +| `imgproc` | PUBLIC | `core` | +| `imgcodecs` | PUBLIC | `imgproc` | +| `ml` | PUBLIC | `core` | +| `flann` | PUBLIC | `core` | +| `photo` | PUBLIC | `imgproc` (optional: `cudaarithm`, `cudaimgproc`) | +| `video` | PUBLIC | `imgproc` | +| `videoio` | PUBLIC | `imgproc`, `imgcodecs` | +| `features2d` | PUBLIC | `imgproc`, `ml`, `flann` (optional: `highgui`) | +| `calib3d` | PUBLIC | `imgproc`, `features2d` | +| `objdetect` | PUBLIC | `core`, `imgproc`, `ml` (optional: `highgui`) | +| `shape` | PUBLIC | `core`, `imgproc`, `video` | +| `stitching` | PUBLIC | `imgproc`, `features2d`, `calib3d`, `objdetect`, `imgcodecs` | +| `videostab` | PUBLIC | `imgproc`, `features2d`, `video`, `photo`, `calib3d` | +| `superres` | PUBLIC | `imgproc`, `video` | +| `highgui` | PUBLIC | `imgproc`, `imgcodecs`, `videoio` | +| `viz` | PUBLIC | `core` + VTK | +| `hal` | INTERNAL | - (hardware acceleration shims) | +| `ts` | INTERNAL | `core`, `imgproc`, `imgcodecs`, `videoio`, `highgui` (test support) | +| `cudev` | PUBLIC | - (CUDA runtime wrappers) | +| `cudaarithm` | PUBLIC | `core` (optional: `cudev`) | +| `cudafilters` | PUBLIC | `imgproc`, `cudaarithm` | +| `cuda*` (rest) | PUBLIC | the CPU modules above, plus `cudev`/`cudawarping`/`cudalegacy` | +| `world` | PUBLIC | bundles every public module into a single library | +| `java`, `python2`, `python3` | BINDINGS | `core`, `imgproc` + the wrapped modules | + +Two consequences worth memorising: + +* `opencv_ts` (internal) links **`highgui`**, and every module's accuracy test + links `opencv_ts` + `opencv_imgcodecs` + `opencv_videoio`. Turning any of + those off removes the test suite for *every* module, not just one. +* Optional (`OPTIONAL`) dependencies are probed at configure time; when absent + the module still builds, with the corresponding algorithms reporting that + they are unavailable. The configure summary prints the result of every probe. + + +## Build system + +* `CMakeLists.txt` (top level) sets the global options, includes + `cmake/OpenCV*.cmake` and then `modules/`. +* `cmake/OpenCVModule.cmake` is the engine: `ocv_define_module` globs the + sources, resolves dependencies, creates the library target and calls + `ocv_add_accuracy_tests()` / `ocv_add_perf_tests()` / `ocv_add_samples()`. +* `cmake/OpenCVUtils.cmake`, `OpenCVDetect*.cmake`, `OpenCVFind*.cmake` hold the + dependency detection logic. A new optional dependency needs a `Detect`/`Find` + module plus a status line, so the configure summary stays truthful. +* Generated files: `opencv2/opencv_modules.hpp` and `cvconfig.h` are written into + the build tree, never into the source tree. + +## Bindings + +* `modules/python/src2` - hand written CPython C-API wrappers, packaged by + `modules/python/setup.py`. Pinned build dependencies: `modules/python/requirements.txt`. +* `modules/java/generator/gen_java.py` - parses the public headers with + `modules/python/src2/hdr_parser.py` and emits the Java classes plus the JNI + C++ shim (`.cpp`) into the build tree at build time. + * Consequence: the Java API mirrors exactly what the generator supports. A + function returning `Ptr`, for example, is skipped (see the `video.txt` + generator report in the build directory), and module *classes* are still a + work in progress in this tree - only the free functions of a module reach + `org.opencv..` reliably. Test accordingly; see + [docs/TESTING.md](TESTING.md). + +## Error handling and validation + +There is no logging framework in the library. Contracts are enforced with +`CV_Assert` / `CV_Error`, which throw `cv::Exception`; functions that take sizes, +types or index ranges validate them at the boundary rather than trusting the +caller. This is the pattern to follow in new code, and it is what the security +notes in [SECURITY.md](../SECURITY.md) rely on. + +## Where to add what + +| Change | Location | +| ------ | -------- | +| New algorithm | existing `modules//src/*.cpp`, reuse the module's precompiled header | +| New public API | `modules//include/opencv2/...`, guard it with `CV_EXPORTS_W` | +| Bug fix | add an accuracy test in `modules//test/` *first*, then fix | +| New dependency | `cmake/OpenCVFind*.cmake` + option + configure status line | +| Performance work | `modules//perf/`, never inside the accuracy test | diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 000000000000..f3e924a5da6f --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,104 @@ +# Testing + +Two suites exist and they are not interchangeable: + +| Suite | Location | Built by | Run with | +| ----- | -------- | -------- | -------- | +| C++ accuracy/regression | `modules/*/test/` | `BUILD_TESTS=ON` | `ctest --test-dir build` | +| C++ performance | `modules/*/perf/` | `BUILD_PERF_TESTS=ON` | `./bin/opencv_perf_` | +| Java binding tests | `modules/*/misc/java/test/` | `BUILD_TESTS=ON` + `BUILD_opencv_java=ON` + Ant | `ant buildAndTest` (Ant project in `build/java/pure_test/.build`) | + +## C++ accuracy tests + +```bash +cmake -S . -B build -G Ninja -DBUILD_TESTS=ON \ + -DOPENCV_TEST_DATA_PATH="$PWD/../opencv_extra" +cmake --build build --parallel $(nproc) +ctest --test-dir build --output-on-failure +``` + +`OPENCV_TEST_DATA_PATH` must point at an `opencv_extra` checkout on the branch +that matches this tree (`3.0`): + +```bash +git clone -b 3.0 --depth 1 https://github.com/opencv/opencv_extra ../opencv_extra +``` + +The test binaries read the same name as an environment variable, so you can also +export it at run time: + +```bash +export OPENCV_TEST_DATA_PATH=/path/to/opencv_extra +ctest --test-dir build --output-on-failure +``` + +Selecting work: + +```bash +ctest --test-dir build -R opencv_test_video --output-on-failure # one module +ctest --test-dir build -j4 --output-on-failure # parallel +./bin/opencv_test_core --gtest_filter='Mat/*' # one suite +./bin/opencv_test_video --gtest_list_tests # what is there +``` + +A new test goes in `modules//test/test_*.cpp`; CMake globs that +directory, so no build file needs editing. Write the failing test first, then +the fix - `modules/video/test/test_camshift.cpp` (randomised boxes) and +`modules/video/test/test_kalman.cpp` (estimation accuracy over a noise model) +are the templates to copy. + +## Java binding tests + +`modules//misc/java/test/*Test.java` are JUnit 3 style tests extending +`org.opencv.test.OpenCVTestCase`. They are copied into the Ant project +(`modules/java/pure_test`) and built by the `opencv_test_java` CMake target, +which additionally requires a JDK, Ant and a Python interpreter for the +generator. Run them from the generated Ant project: + +```bash +cmake --build build --target opencv_test_java +cd build/java/pure_test/.build && ant buildAndTest +``` + +The fixtures (`res/`, `res/raw/`) are copied from `modules/java/common_test`. + +Two things to know before adding one: + +1. **Only what the generator emits is callable.** `modules/java/generator/gen_java.py` + turns the C++ headers into the Java API and writes a report + (`/java/.txt`) listing ported *and skipped* functions; a + `Ptr`-returning factory, for example, is skipped in this tree. Check the + generated `org/opencv//.java` in the build directory for the + real signatures instead of guessing - overloads get Java overloads, and + in/out reference arguments are copied back into the passed object. +2. **A stub is not a test.** `fail("Not yet implemented")` is swallowed by + `OpenCVTestCase.fail()` (see `passNYI`), so such a method reports success + while asserting nothing. It now also prints a warning. Replace stubs with + real assertions, or delete the method when the underlying API no longer + exists in the C++ library. + +Tests for C++ APIs that no longer exist were removed rather than left as +stubs: `segmentMotion`, `updateMotionHistory`, `calcMotionGradient` and +`calcGlobalOrientation` (motion templates, dropped from the public headers in +the 3.x line) and `BackgroundSubtractorMOG` (replaced by `BackgroundSubtractorMOG2`). + +## Reporting coverage gaps + +`modules/java/check-tests.py` lists the JUnit methods that are still stubs: + +```bash +python modules/java/check-tests.py +``` + +## What CI runs + +`.github/workflows/ci.yml` runs, on every push and pull request: + +* **build-and-test** - the CMake flag set documented above plus ccache, then + `ctest --output-on-failure --timeout 900` against an `opencv_extra` clone. +* **lint** - validates `.clang-format`, and checks the repository's own + YAML/docs files for trailing whitespace, tabs in YAML and invalid YAML. +* **dependency-audit** (pull requests) - `pip-audit --strict` over the pinned + `requirements-dev.txt`. + +A change is expected to keep all three green; a red build is never merged. diff --git a/modules/java/pure_test/src/org/opencv/test/OpenCVTestCase.java b/modules/java/pure_test/src/org/opencv/test/OpenCVTestCase.java index e7d1506b97dc..28417adb2c34 100644 --- a/modules/java/pure_test/src/org/opencv/test/OpenCVTestCase.java +++ b/modules/java/pure_test/src/org/opencv/test/OpenCVTestCase.java @@ -230,8 +230,18 @@ protected Mat makeMask(Mat m, double... vals) } public static void fail(String msg) { - if(msg == "Not yet implemented" && passNYI) + if(msg == "Not yet implemented" && passNYI) { + // An unimplemented test that reports success is worse than a missing + // test, because the suite looks green. Keep the stub tolerated (other + // modules still have them) but make the gap visible in the output. + StackTraceElement[] trace = new Throwable().getStackTrace(); + String where = trace.length > 1 + ? trace[1].getClassName() + "." + trace[1].getMethodName() + : ""; + System.out.println(TAG + ": WARNING: " + where + + " has no assertions yet (fail(\"Not yet implemented\") was ignored)"); return; + } TestCase.fail(msg); } diff --git a/modules/python/requirements.txt b/modules/python/requirements.txt new file mode 100644 index 000000000000..26bf6ff13106 --- /dev/null +++ b/modules/python/requirements.txt @@ -0,0 +1,13 @@ +# Build and test dependencies for the Python bindings in this directory. +# +# Pinned to exact versions on purpose: +# * numpy is capped below 2.0 because modules/python/src2 uses the numpy 1.x +# C API; numpy 2.x removed that ABI and silently breaks the extension module. +# * exact pins make a build reproducible, which is what CI and the container +# rely on (see requirements-dev.txt at the repository root for auditing). +# +# Install: python -m pip install -r modules/python/requirements.txt + +numpy==1.26.4 +setuptools==75.1.0 +wheel==0.44.0 diff --git a/modules/video/misc/java/test/BackgroundSubtractorMOGTest.java b/modules/video/misc/java/test/BackgroundSubtractorMOGTest.java deleted file mode 100644 index 9d6f71d1a2f5..000000000000 --- a/modules/video/misc/java/test/BackgroundSubtractorMOGTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.opencv.test.video; - -import org.opencv.test.OpenCVTestCase; - -public class BackgroundSubtractorMOGTest extends OpenCVTestCase { - - public void testApplyMatMat() { - fail("Not yet implemented"); - /* - BackgroundSubtractorMOG backGroundSubtract = new BackgroundSubtractorMOG(); - - Point bottomRight = new Point(rgbLena.cols() / 2, rgbLena.rows() / 2); - Point topLeft = new Point(0, 0); - Scalar color = new Scalar(128); - Mat mask = new Mat(rgbLena.size(), CvType.CV_16UC3, new Scalar(1)); - - Imgproc.rectangle(rgbLena, bottomRight, topLeft, color, Core.FILLED); - - backGroundSubtract.apply(rgbLena, mask); - - Mat truth = new Mat(rgbLena.size(), rgbLena.type(), new Scalar(0)); - Imgproc.rectangle(truth, bottomRight, topLeft, color, Core.FILLED); - assertMatEqual(truth, rgbLena); - */ - } - - public void testApplyMatMatDouble() { - fail("Not yet implemented"); - } - - public void testBackgroundSubtractorMOG() { - fail("Not yet implemented"); - } - - public void testBackgroundSubtractorMOGIntIntDouble() { - fail("Not yet implemented"); - } - - public void testBackgroundSubtractorMOGIntIntDoubleDouble() { - fail("Not yet implemented"); - } - -} diff --git a/modules/video/misc/java/test/KalmanFilterTest.java b/modules/video/misc/java/test/KalmanFilterTest.java index db35a8d35f67..0aab4f9ddb30 100644 --- a/modules/video/misc/java/test/KalmanFilterTest.java +++ b/modules/video/misc/java/test/KalmanFilterTest.java @@ -1,12 +1,30 @@ package org.opencv.test.video; +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; import org.opencv.test.OpenCVTestCase; import org.opencv.video.KalmanFilter; +/** + * Binding tests for {@link KalmanFilter}. + * + *

The generated Java class exposes the constructors, {@code predict()} and + * {@code correct(Mat)}; the matrices of the filter are not wrapped, so these + * tests pin down the contract that is observable: the dimensionality + * and element type of the state, and the documented behaviour of a + * freshly initialised filter (identity transition matrix, zero state, zero + * measurement matrix). Estimation accuracy over a noise model is covered by the + * C++ suite in modules/video/test/test_kalman.cpp. + */ public class KalmanFilterTest extends OpenCVTestCase { - public void testCorrect() { - fail("Not yet implemented"); + private void assertState(Mat state, int dynamParams, int type) { + assertNotNull(state); + assertEquals(dynamParams, state.rows()); + assertEquals(1, state.cols()); + assertEquals(type, state.type()); } public void testKalmanFilter() { @@ -16,23 +34,60 @@ public void testKalmanFilter() { } public void testKalmanFilterIntInt() { - fail("Not yet implemented"); + KalmanFilter kf = new KalmanFilter(4, 2); + + assertState(kf.predict(), 4, CvType.CV_32F); } public void testKalmanFilterIntIntInt() { - fail("Not yet implemented"); + KalmanFilter kf = new KalmanFilter(4, 2, 1); + + assertState(kf.predict(), 4, CvType.CV_32F); } public void testKalmanFilterIntIntIntInt() { - fail("Not yet implemented"); + // The fourth argument selects the element type of every internal matrix. + KalmanFilter kf = new KalmanFilter(2, 1, 0, CvType.CV_64F); + + assertState(kf.predict(), 2, CvType.CV_64F); } public void testPredict() { - fail("Not yet implemented"); + KalmanFilter kf = new KalmanFilter(5, 3); + + // A fresh filter has statePost == 0 and an identity transition matrix, + // so predicting must yield a zero state of the configured size and + // must stay there for a stationary system. + Mat predicted = kf.predict(); + assertState(predicted, 5, CvType.CV_32F); + assertEquals(0, Core.countNonZero(predicted)); + + Mat predictedAgain = kf.predict(); + assertState(predictedAgain, 5, CvType.CV_32F); + assertEquals(0, Core.countNonZero(predictedAgain)); } public void testPredictMat() { - fail("Not yet implemented"); + KalmanFilter kf = new KalmanFilter(3, 2, 1); + Mat control = new Mat(1, 1, CvType.CV_32F, new Scalar(1)); + + // The control vector is accepted (it is CP x 1) and added through the + // control matrix, which is zero until the caller replaces it. + Mat predicted = kf.predict(control); + assertState(predicted, 3, CvType.CV_32F); + assertEquals(0, Core.countNonZero(predicted)); } + public void testCorrect() { + KalmanFilter kf = new KalmanFilter(4, 2); + Mat predicted = kf.predict(); + Mat measurement = new Mat(2, 1, CvType.CV_32F, new Scalar(1, 2)); + + Mat corrected = kf.correct(measurement); + + assertState(corrected, 4, CvType.CV_32F); + // The default measurement matrix is zero, hence the gain is zero and the + // correction leaves the state at its predicted value. + assertMatEqual(predicted, corrected, EPS); + } } diff --git a/modules/video/misc/java/test/VideoTest.java b/modules/video/misc/java/test/VideoTest.java index cb549bb0d86b..ddcddb3d151b 100644 --- a/modules/video/misc/java/test/VideoTest.java +++ b/modules/video/misc/java/test/VideoTest.java @@ -1,17 +1,50 @@ package org.opencv.test.video; import org.opencv.core.Core; +import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfByte; import org.opencv.core.MatOfFloat; import org.opencv.core.MatOfPoint2f; import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; import org.opencv.core.Size; +import org.opencv.core.TermCriteria; import org.opencv.test.OpenCVTestCase; import org.opencv.video.Video; +/** + * Binding tests for {@link Video}. + * + *

Scope: these assert the contract of the generated JNI layer - argument + * marshalling, in/out reference arguments, return shapes - for the free + * functions of the video module. Numerical accuracy of the algorithms + * themselves is covered by the C++ suite in modules/video/test + * (test_camshift.cpp, test_estimaterigid.cpp, test_optflowpyrlk.cpp), which has + * access to internal state and reference data. + * + *

Stubs for the motion-template API (segmentMotion, updateMotionHistory, + * calcMotionGradient, calcGlobalOrientation) were removed: those entry points + * are no longer part of the public C++ headers in this line, so the tests could + * never be implemented. See docs/TESTING.md. + */ public class VideoTest extends OpenCVTestCase { + /** Side of the synthetic back-projection image built by makeProbImage(). */ + private static final int PROB_IMAGE_SIZE = 100; + /** Centre (in pixels, 0-based) of the bright square inside makeProbImage(). */ + private static final double BLOB_CENTRE = 49.5; + /** Side of that square; its centre of mass is BLOB_CENTRE. */ + private static final int BLOB_SIZE = 20; + /** + * Tolerance for centre-of-mass assertions. meanShift and CamShift work on an + * integer search rectangle and round the result, so they settle up to a + * pixel either side of the exact centre of mass. + */ + private static final double CENTRE_TOLERANCE = 2.0; + private MatOfFloat err = null; private int h; private MatOfPoint2f nextPts = null; @@ -44,20 +77,32 @@ protected void setUp() throws Exception { err = new MatOfFloat(); } - public void testCalcGlobalOrientation() { - fail("Not yet implemented"); + /** + * Builds a back-projection-like probability image: a single bright square + * centred on BLOB_CENTRE, everything else zero. The centre of mass of the + * image is therefore known exactly, which is what meanShift/CamShift must + * converge to. + */ + private Mat makeProbImage() { + int from = (int)(BLOB_CENTRE - BLOB_SIZE / 2); + int to = from + BLOB_SIZE; + + Mat probImage = new Mat(PROB_IMAGE_SIZE, PROB_IMAGE_SIZE, CvType.CV_32F, new Scalar(0)); + probImage.submat(from, to, from, to).setTo(new Scalar(255)); + return probImage; } - public void testCalcMotionGradientMatMatMatDoubleDouble() { - fail("Not yet implemented"); - } + public void testCalcOpticalFlowFarneback() { + Mat flow = new Mat(); - public void testCalcMotionGradientMatMatMatDoubleDoubleInt() { - fail("Not yet implemented"); - } + // The same frame twice: Farneback's polynomial expansion collapses and + // the flow must be exactly zero everywhere. + Video.calcOpticalFlowFarneback(grayChess, grayChess, flow, 0.5, 3, 15, 3, 5, 1.1, 0); - public void testCalcOpticalFlowFarneback() { - fail("Not yet implemented"); + assertEquals(grayChess.cols(), flow.cols()); + assertEquals(grayChess.rows(), flow.rows()); + assertEquals(CvType.CV_32FC2, flow.type()); + assertEquals(0, Core.countNonZero(flow)); } public void testCalcOpticalFlowPyrLKMatMatListOfPointListOfPointListOfByteListOfFloat() { @@ -71,29 +116,97 @@ public void testCalcOpticalFlowPyrLKMatMatListOfPointListOfPointListOfByteListOf assertEquals(0, Core.countNonZero(status)); } - public void testCalcOpticalFlowPyrLKMatMatListOfPointListOfPointListOfByteListOfFloatSizeIntTermCriteriaDoubleIntDouble() { - fail("Not yet implemented"); + // Explicit criteria/flags/minEigThreshold: the same call as above with + // every optional argument spelled out. A 3x3 window cannot track, so + // only the marshalling is asserted here. + Size sz = new Size(3, 3); + TermCriteria criteria = new TermCriteria(TermCriteria.EPS, 0, 0.01); + + Video.calcOpticalFlowPyrLK(subLena1, subLena2, prevPts, nextPts, status, err, sz, 3, criteria, 0, 1e-4); + + assertEquals(prevPts.rows(), status.rows()); + assertEquals(1, status.cols()); + assertEquals(CvType.CV_8U, status.type()); + assertEquals(prevPts.rows(), err.rows()); + assertEquals(1, err.cols()); + assertEquals(CvType.CV_32F, err.type()); + assertEquals(prevPts.rows(), nextPts.rows()); + assertEquals(1, nextPts.cols()); + assertEquals(CvType.CV_32FC2, nextPts.type()); } public void testCamShift() { - fail("Not yet implemented"); + Rect window = new Rect(33, 33, 40, 40); + + RotatedRect box = Video.CamShift(makeProbImage(), window, new TermCriteria(TermCriteria.EPS, 0, 0.5)); + + // The window is an in/out argument: CamShift must have moved it onto the + // blob and must leave a valid, image-bounded search rectangle behind. + assertTrue(window.x >= 0); + assertTrue(window.y >= 0); + assertTrue(window.x + window.width <= PROB_IMAGE_SIZE); + assertTrue(window.y + window.height <= PROB_IMAGE_SIZE); + + // The returned box is centred on the centre of mass of the blob. + assertEquals(BLOB_CENTRE, box.center.x, CENTRE_TOLERANCE); + assertEquals(BLOB_CENTRE, box.center.y, CENTRE_TOLERANCE); + + // A symmetric blob has no orientation and no aspect ratio, and CAMSHIFT + // must recover its size to well within a factor of two. + assertEquals((double)box.size.width, (double)box.size.height, weakEPS); + assertTrue("recovered size " + box.size.width, box.size.width > BLOB_SIZE / 2); + assertTrue("recovered size " + box.size.width, box.size.width < BLOB_SIZE * 3 / 2); } public void testEstimateRigidTransform() { - fail("Not yet implemented"); + MatOfPoint2f src = new MatOfPoint2f(new Point(0, 0), new Point(10, 0), new Point(0, 10), new Point(10, 10)); + MatOfPoint2f dst = new MatOfPoint2f(new Point(2, 3), new Point(12, 3), new Point(2, 13), new Point(12, 13)); + + // fullAffine == false: rotation + uniform scale + translation must be + // able to express a pure translation, and recover it exactly. + Mat m = Video.estimateRigidTransform(src, dst, false); + + assertEquals(2, m.rows()); + assertEquals(3, m.cols()); + assertEquals(CvType.CV_64F, m.type()); + assertEquals(1.0, m.get(0, 0)[0], EPS); + assertEquals(0.0, m.get(0, 1)[0], EPS); + assertEquals(2.0, m.get(0, 2)[0], EPS); + assertEquals(0.0, m.get(1, 0)[0], EPS); + assertEquals(1.0, m.get(1, 1)[0], EPS); + assertEquals(3.0, m.get(1, 2)[0], EPS); + + // With fullAffine == true the general 2x3 model is used; the same data + // must still be reproduced exactly. + Mat mFull = Video.estimateRigidTransform(src, dst, true); + + assertEquals(2, mFull.rows()); + assertEquals(3, mFull.cols()); + assertEquals(CvType.CV_64F, mFull.type()); + assertEquals(1.0, mFull.get(0, 0)[0], EPS); + assertEquals(0.0, mFull.get(0, 1)[0], EPS); + assertEquals(2.0, mFull.get(0, 2)[0], EPS); + assertEquals(0.0, mFull.get(1, 0)[0], EPS); + assertEquals(1.0, mFull.get(1, 1)[0], EPS); + assertEquals(3.0, mFull.get(1, 2)[0], EPS); } public void testMeanShift() { - fail("Not yet implemented"); - } + // Initial window is 3 pixels right/below the centre of mass, so a + // correct meanShift has to move it back. + Rect window = new Rect(33, 33, 40, 40); - public void testSegmentMotion() { - fail("Not yet implemented"); - } + int iterations = Video.meanShift(makeProbImage(), window, new TermCriteria(TermCriteria.EPS, 0, 0.5)); - public void testUpdateMotionHistory() { - fail("Not yet implemented"); - } + assertTrue("meanShift did not run", iterations > 0); + + // meanShift only translates: the window keeps its size. + assertEquals(40, window.width); + assertEquals(40, window.height); + // ... and it ends up centred on the blob (see CENTRE_TOLERANCE). + assertEquals(BLOB_CENTRE, window.x + (window.width - 1) * 0.5, CENTRE_TOLERANCE); + assertEquals(BLOB_CENTRE, window.y + (window.height - 1) * 0.5, CENTRE_TOLERANCE); + } } diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000000..a8f838c4f5f2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,14 @@ +# Repository-wide Python tooling, pinned to exact versions. +# +# This is the only dependency manifest in the project that is installed by +# automation, so it is also the file audited for known vulnerabilities: +# python -m pip install -r requirements-dev.txt +# python -m pip_audit --requirement requirements-dev.txt --strict +# The second command runs on every pull request (see .github/workflows/ci.yml), +# and Dependabot keeps the pins current (.github/dependabot.yml). + +# Python bindings build/test dependencies. +-r modules/python/requirements.txt + +# Dependency vulnerability scanner used by CI. +pip-audit==2.7.3