You add OpenCV as a git submodule, put add_subdirectory(external/opencv) and find_package(OpenCV REQUIRED) into your top-level CMakeLists.txt, and CMake stops at configure time: no package configuration file provided by OpenCV. Comment out find_package and configure passes, but the first #include <opencv2/opencv.hpp> in your own source fails. One cause sits behind both failures: a submodule clone is source code only, and the files both mechanisms need are written by OpenCV's install step. This how-to gives you the two set-ups that work — build OpenCV once into a local prefix and use find_package, or keep add_subdirectory and link the opencv_core and opencv_imgproc targets directly — with the exact commands, the exact config-file location, and the three checks that tell you which mechanism your project is actually using.
Why find_package fails on a submodule clone
git submodule add https://github.com/opencv/opencv.git external/opencv clones the OpenCV repository: source files and its own CMakeLists.txt, nothing built. find_package(OpenCV) runs in config mode and searches known prefixes for OpenCVConfig.cmake or opencv-config.cmake. OpenCV writes that config file during the install step, not during the clone, so the search has nothing to find and configure stops with exactly the error you saw.
The include side fails for the same reason. OpenCV stores its headers per module, under modules/<name>/include/opencv2/<name>/ in the source tree, and part of the flat opencv2 include tree is generated as the build runs. The single include directory that #include <opencv2/opencv.hpp> resolves against does not exist in a fresh checkout, so there is no path you can hand to target_include_directories that would work. A library that keeps one flat include directory in its checkout — GLFW is the common example — can be consumed by hand-adding that path; OpenCV cannot.
That also explains the half-fix. With find_package commented out, configure succeeds because add_subdirectory(external/opencv) defines live build targets. But ${OpenCV_INCLUDE_DIRS} and ${OpenCV_LIBRARIES} are outputs of find_package's config file; they stay empty here, so target_include_directories and target_link_libraries receive nothing and fail silently. Your editor still navigates into opencv2/opencv.hpp because it indexes the source tree directly; the compiler, which follows the include paths you actually passed, has none.
Choose one mechanism, not both
Both working set-ups use the same OpenCV build. They differ in who consumes it — decide once per project:
| Mechanism A: install, then find_package | Mechanism B: add_subdirectory | |
|---|---|---|
| Configure call |
find_package(OpenCV REQUIRED) after one build and install |
add_subdirectory(external/opencv), no find_package
|
| What you link | imported targets: opencv_core, opencv_imgproc
|
the same target names, built inside your tree |
| Configure cost | seconds | OpenCV's full CMake runs inside yours, dependency probing included |
| Fits | application code, CI, several binaries | a patched or option-pinned OpenCV, small tools |
Run Mechanism A as the default for vision application code: configure stays fast, the install prefix is one artifact you can cache, and several executables share a single OpenCV. Choose Mechanism B only when you must compile a patched or heavily option-pinned OpenCV inside your own build tree, and accept that every configure re-runs OpenCV's own CMake. Never keep find_package and add_subdirectory in the same project: before an install exists, find_package fails hard; if it succeeds anyway, it can resolve a different OpenCV than the targets you just built.
Mechanism A: build once into a project-local prefix
You need a machine with a C++ compiler, CMake and git; the commands below come from Ubuntu 22.04 with its default CMake 3.22 and translate directly to other platforms.
- Add the submodule and check out its sources:
git submodule add https://github.com/opencv/opencv.git external/opencv
git submodule update --init --recursive
- Configure OpenCV into its own build tree with the install prefix inside your project —
CMAKE_INSTALL_PREFIXdecides where that prefix is. Turning off tests, performance tests, examples and apps cuts the build down to the libraries you actually link:
cmake -S external/opencv -B external/opencv/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/external/opencv/install" -DBUILD_SHARED_LIBS=ON -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_opencv_apps=OFF
- Build and install. As an illustrative reference point, this took about 8 minutes on one 8-thread Linux desktop with those options, and the configure step alone finishes in under 10 seconds; your module set and core count move those numbers:
cmake --build external/opencv/build -j$(nproc)
cmake --install external/opencv/build
- Point your project at that prefix. Either pass the prefix itself on the command line, or set
OpenCV_DIRto the folder that holds the config file — both reach the sameOpenCVConfig.cmake:
cmake -S . -B out/build -DCMAKE_PREFIX_PATH="$PWD/external/opencv/install"
The config file lives at external/opencv/install/lib/cmake/opencv4/OpenCVConfig.cmake; OpenCV_DIR must name that opencv4 directory.
- In your own CMakeLists.txt, find the package and link its targets. Prefer the named targets over
${OpenCV_LIBS}: each target carries its own include directories, a typo fails configure loudly, and theCOMPONENTSargument maps one-to-one onto them:
find_package(OpenCV REQUIRED COMPONENTS core imgproc)
add_executable(my_target main.cpp)
target_link_libraries(my_target PRIVATE opencv_core opencv_imgproc)
Request only components your build compiled: a module the build left out — contrib modules are the usual case — fails configure the same way as the original error. The missing OpenCV aruco module fix covers one such case end to end.
Mechanism B: keep add_subdirectory, drop find_package
-
Clone the submodule as in step 1 of Mechanism A.
-
Set OpenCV's build options before the
add_subdirectorycall so they apply to it, and trim what gets built:
set(BUILD_SHARED_LIBS ON)
set(BUILD_TESTS OFF)
set(BUILD_PERF_TESTS OFF)
set(BUILD_EXAMPLES OFF)
add_subdirectory(external/opencv)
- Link the target names. Never reach for
${OpenCV_INCLUDE_DIRS}or${OpenCV_LIBRARIES}here — they arefind_packageoutputs and stay empty, which is precisely the configure-passes-includes-fail state:
add_executable(my_target main.cpp)
target_link_libraries(my_target PRIVATE opencv_core opencv_imgproc)
The in-tree targets carry their include directories with them, so #include <opencv2/opencv.hpp> resolves without any target_include_directories of your own.
- Remove
find_package(OpenCV)from the project entirely — that is the whole mechanism.
OpenCV's CMake pulls third-party dependencies, GUI and video-codec development packages in particular, while the build runs rather than at configure time, so expect the first Mechanism B build to ask for one or two dev packages a build container often lacks. Every later configure of your project also re-runs OpenCV's full CMake; that standing cost is the price of building OpenCV inside your tree.
Prove the fix with three checks
Assume the project lives at /home/dev/vision-app — an illustrative path; substitute yours. Each check produces a concrete, greppable result.
Check 1 — the config file exists only after install.
ls /home/dev/vision-app/external/opencv/install/lib/cmake/opencv4/OpenCVConfig.cmake
Before the install step this path does not exist, and that absence is your original error. After it, the file is there, next to OpenCVConfig-version.cmake.
Check 2 — CMake resolved your prefix.
message(STATUS "OpenCV_DIR=${OpenCV_DIR}")
message(STATUS "OpenCV_LIBS=${OpenCV_LIBS}")
if(TARGET opencv_core)
message(STATUS "opencv_core target present")
endif()
Mechanism A prints OpenCV_DIR=/home/dev/vision-app/external/opencv/install/lib/cmake/opencv4 and a non-empty OpenCV_LIBS. Mechanism B leaves OpenCV_DIR empty because find_package never runs; there the proof is the opencv_core target present line.
Check 3 — compile, link and run one line that names its library.
#include <opencv2/opencv.hpp>
#include <cstdio>
int main() {
std::printf("OpenCV %s\n", CV_VERSION);
return 0;
}
CV_VERSION is OpenCV's own version macro, and the binary prints, for example, OpenCV 4.11.0 — the release you installed, not the 4.5.4 that Ubuntu 22.04's libopencv-dev provides. One line of output proves the include path, the link and the runtime library all come from your prefix.
If that first run stops with the loader unable to find libopencv_core.so.4.*, the link is already correct and only the runtime search is missing, because a project-local prefix is not on the loader's default path. Run the test once with LD_LIBRARY_PATH="$PWD/external/opencv/install/lib:$LD_LIBRARY_PATH", or set CMAKE_BUILD_RPATH (build tree) or CMAKE_INSTALL_RPATH (installed binary) to that lib directory when you configure my_target.
Mistakes that look like this bug
-
find_packageandadd_subdirectoryboth present: delete one. Which one is the decision above. -
OpenCV_DIRpointed atexternal/opencv— the source tree. The file lives in the install tree,external/opencv/install/lib/cmake/opencv4. - A fresh clone without
git submodule update --init --recursive:add_subdirectorythen fails becauseexternal/opencvcontains no CMakeLists.txt at all. Init submodules before the first configure. - Trusting the editor: IntelliSense navigates into
opencv2/opencv.hppby indexing the source tree while the compile fails. Only the compiler's include paths count. - Hand-building include paths from
modules/*/include: it misses the generated part of theopencv2tree. Let the targets or the installed config file supply the paths. - Passing
${OpenCV_LIBS}after a configure that passed: empty variable, nothing linked, and the failure reappears at link time as undefined references tocv::symbols. Link the target names.
When the answer is different
Two neighbours fail in ways that look identical from the error text:
- The missing-config error comes from a prebuilt SDK's sample tree rather than your submodule: the fix is the SDK's own config path, and CMake can't find OpenCV in SDK samples: fix the config path is the article for it.
- OpenCV headers resolve but a camera SDK around them will not build: that diagnosis starts elsewhere — see Camera SDK missing OpenCV headers: causes, tests and fixes.
And one shortcut removes the submodule altogether: Ubuntu 22.04's libopencv-dev package ships OpenCV 4.5.4, and after sudo apt install libopencv-dev the plain find_package(OpenCV REQUIRED) works against the system prefix with no variables at all. The submodule route earns its cost only when you need a newer release, a patch, or module options the package does not give you.
The same discipline decides how the rest of a vision application links. The vendor SDK behind your frame grabbers and the runtime stack on vision controllers both arrive as install trees with their own config files or headers: build and install first, find and link targets second. When the target is an industrial controller rather than your desktop, a walkthrough of OpenCV builds with CMake paths on a PLCnext target shows the same prefix-and-config pattern on that class of hardware.
From a green build to the hardware
Once main.cpp compiles, links and prints its version, the rest of the project is acquisition. Pick the area scan cameras that match the part and your resolution budget, then the interface that matches this machine — GigE straight into the controller's network port, CoaXPress through a frame grabber. The build-a-system configurator walks camera, lens, light and controller in order; if a build error appears only on the machine that runs the acquisition hardware, send the configure output through the contact page and we will read it against your CMakeLists.txt.
Questions
Why does add_subdirectory(external/opencv) configure fine but find_package(OpenCV) fail in the same file?
add_subdirectory reads OpenCV's own CMakeLists and defines build targets such as opencv_core inside your project. find_package runs a search for an installed OpenCVConfig.cmake, a file OpenCV writes only during the install step. A submodule checkout contains neither the config file nor the assembled include directory, so the search cannot succeed until you build and install once.
Where exactly is OpenCVConfig.cmake after the install step?
Directly under the prefix you set, at prefix/lib/cmake/opencv4/, beside OpenCVConfig-version.cmake, which carries out find_package's version check. OpenCV_DIR must name that opencv4 folder itself, the directory holding the file, while CMAKE_PREFIX_PATH can point at the prefix because CMake's config search tries prefix/lib/cmake/name patterns automatically. With CMAKE_INSTALL_PREFIX set to external/opencv/install at your project root, the full path is external/opencv/install/lib/cmake/opencv4.
Should I link ${OpenCV_LIBS} or the opencv_core target?
Prefer the named targets: opencv_core, opencv_imgproc and the rest. They carry their own include directories, a typo fails configure loudly, and the COMPONENTS argument in find_package maps one-to-one onto them. ${OpenCV_LIBS} works after a successful find_package but stays empty under add_subdirectory, which links nothing and surfaces later as undefined references to cv:: symbols at link time.
Do I need to install OpenCV system-wide with sudo?
No. Set CMAKE_INSTALL_PREFIX to a folder inside your project, such as external/opencv/install, run cmake --install, and point CMAKE_PREFIX_PATH or OpenCV_DIR at that folder. A project-local prefix keeps the machine clean, avoids shadowing the distribution's libopencv-dev for other tools, and can be deleted together with the build directory when you rebase or change versions.
Is there a version of OpenCV I can use without a submodule at all?
Yes. Ubuntu 22.04 ships libopencv-dev with OpenCV 4.5.4; after installing it, find_package(OpenCV REQUIRED) works against the system prefix with no extra variables. Choose the submodule route when you need a newer release, a patch, or build options such as a specific module set that the distribution package does not offer.