OpenCV 5.0 removed cv::dnn::readNetFromCaffe() and readNetFromDarknet(). If you have .caffemodel or Darknet .weights assets in production — and a surprising number of inspection systems, face pipelines and YOLOv3/v4 deployments still do — those call sites stop compiling the day you upgrade. This is the modernization process we follow: inventory, convert to ONNX, validate, and retrain when conversion is not worth it.
1. Inventory the legacy assets
Find every model file and every loader call before deciding anything:
find . -type f \( -name '*.caffemodel' -o -name '*.prototxt' -o -name '*.weights' -o -name '*.cfg' \) \
-printf '%s\t%p\n' | sort -n
grep -rnE 'readNetFromCaffe|readNetFromDarknet|readNet\(' --include='*.py' --include='*.cpp' --include='*.java' .
For each model record: framework, architecture (a .prototxt/.cfg header tells you), input size and preprocessing (mean, scale, BGR/RGB), where it runs (server, Jetson, phone), and whether you still have the training data. The last column decides between "convert" and "retrain" later.
Note the preprocessing especially. Caffe models almost always expect BGR with a mean subtraction and no scaling; Darknet expects RGB in [0, 1]. Getting this wrong after conversion is the most common "the ONNX model is broken" report that is not actually a conversion bug.
2. Convert Darknet to ONNX
For YOLOv3/v4-family .cfg + .weights, the simplest reliable route is Ultralytics, which can load Darknet weights and export them through the standard export pipeline, or the darknet2onnx tool from the pytorch-YOLOv4 project that the official migration guide points at:
git clone https://github.com/Tianxiaomo/pytorch-YOLOv4.git
cd pytorch-YOLOv4
pip install -r requirements.txt onnx onnxruntime
python tool/darknet2onnx.py yolov4.cfg yolov4.weights image.jpg 1
# -> yolov4_1_3_608_608_static.onnx
The resulting graph outputs raw boxes and class scores; you keep your existing NMS step. If the .cfg has custom layers, the converter will tell you which, and that is usually the point where retraining (step 6) becomes cheaper.
3. Convert Caffe to ONNX
Caffe has had no release since 2017 and does not install cleanly on modern Python, so convert on a pinned environment. The migration guide recommends caffe-onnx, which parses the .prototxt/.caffemodel directly without needing Caffe installed:
git clone https://github.com/asiryan/caffe-onnx.git && cd caffe-onnx
pip install -r requirements.txt
python convert2onnx.py deploy.prototxt weights.caffemodel model_name ./out
# -> out/model_name.onnx
Unsupported layers (custom Python layers, some Slice/Crop variants, old BN formulations) are reported by name. Two fallbacks when that happens: rebuild the architecture in PyTorch and load the .caffemodel weights by layer name, or go straight to step 6.
After either conversion, always simplify and check the graph:
pip install onnx onnxsim
python -m onnxsim model.onnx model_sim.onnx
python -c "import onnx; m=onnx.load('model_sim.onnx'); onnx.checker.check_model(m); print(m.opset_import)"
OpenCV 5's new engine covers 80%+ of the ONNX operator set and handles dynamic shapes, but a simplified, static-shape graph with a mainstream opset (13-17) loads fastest and fails least.
4. Validate outputs
Conversion is not done until the numbers match. Run the original and the converted model on the same batch and compare, using the old OpenCV 4.x build for the reference side (keep one around in a virtualenv for exactly this purpose):
# reference_4x.py -- run under opencv-python 4.x
import cv2, numpy as np, sys
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "weights.caffemodel")
img = cv2.imread(sys.argv[1])
blob = cv2.dnn.blobFromImage(img, 1.0, (300, 300), (104, 117, 123), swapRB=False)
net.setInput(blob)
np.save("ref.npy", net.forward())
# candidate_5x.py -- run under opencv-python 5.x
import cv2, numpy as np, sys
net = cv2.dnn.readNetFromONNX("model_sim.onnx")
img = cv2.imread(sys.argv[1])
blob = cv2.dnn.blobFromImage(img, 1.0, (300, 300), (104, 117, 123), swapRB=False)
net.setInput(blob)
out = net.forward()
ref = np.load("ref.npy")
print("max abs diff:", np.abs(out - ref).max())
assert np.allclose(out, ref, atol=1e-3), "conversion drift"
Do this on a representative set, not one image, and compare end-to-end metrics (mAP, per-class recall on your validation set) rather than only tensor diffs. Expect tiny differences from fused operators; expect zero difference in which objects are found. If the ONNX model loads in OpenCV 5 only through the classic engine (OPENCV_FORCE_DNN_ENGINE=1 works, =2 fails), note it: that is fine today but worth fixing before the classic engine is deprecated.
5. Replace the loader calls
# before
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "weights.caffemodel")
net = cv2.dnn.readNetFromDarknet("yolov4.cfg", "yolov4.weights")
# after
net = cv2.dnn.readNetFromONNX("model_sim.onnx")
For GPU deployments remember the 5.0 rule: the new engine is CPU-only, so pin engine=cv2.dnn.ENGINE_CLASSIC before setPreferableBackend(DNN_BACKEND_CUDA), or build with WITH_ONNXRUNTIME=ON and use ENGINE_ORT. Keep the old .caffemodel and .weights files in version control until the ONNX versions have run in production for a release cycle.
6. When to retrain instead
Conversion is worth it when the model is accurate, the training data is gone, or the architecture is exotic and the task is simple. It is not worth it when:
- The converter hits custom layers and you would be hand-porting network definitions.
- The model is a YOLOv3/v4 detector and you still have the labelled data. A YOLO26 model trained on the same data will be more accurate, NMS-free, smaller, and exports cleanly to ONNX, TensorRT and LiteRT in one command:
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640)
model.export(format="onnx", imgsz=640, simplify=True)
- The Caffe model is a 2015-era classifier (AlexNet, VGG, early ResNet) doing a task a modern, smaller backbone handles better.
Either way, the downstream pipeline - letterbox, decode, draw - is OpenCV code you already own, and it carries over unchanged. See Running YOLO26 in OpenCV 5 for the modern inference side.
Sitting on a fleet of legacy models and a 4.x pin? Our OpenCV 5 migration service includes the model inventory and conversion work.