PR #25537 opened by Jake (jakefineman)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25537
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25537.patch

Two fixes for dnn_classify with the OpenVINO backend. They are complementary to 
#24736 and apply cleanly together with it.

**Problem**

1. With OpenVINO 2.0, fill_model_input_ov() always describes the input tensor 
as NHWC and takes its dimensions from the model input after preprocessing. 
dnn_classify does not call ff_dnn_get_input(), so without layout= the model 
layout is never resolved and an NCHW model keeps its shape. For googlenet-12, 
[1,3,224,224], the bounding box is scaled into a 224x3 packed BGR image, which 
writes 2016 of the 150528 bytes of the tensor from ov_tensor_create(). The rest 
is uninitialized heap memory, so the labels and confidences change from run to 
run for the same input. A single-channel NHWC model is worse: dnn_detect and 
dnn_classify write three bytes per pixel and overrun the tensor.
2. dnn_execute_model_ov() refuses synchronous classification with ENOSYS, which 
vf_dnn_classify turns into EIO, so dnn_classify with async=0 fails on the first 
frame on master, for every model.

**Fix**

1. In fill_model_input_ov(), fail with a hint to set layout=nchw when the NHWC 
channel dimension of a dnn_detect or dnn_classify input is not 3. Both filters 
always write 3-channel packed pixels. dnn_processing is unchanged.
2. Run synchronous requests until the inference queue is empty, as the ONNX 
Runtime backend does, since classification queues one inference per matching 
bounding box, possibly none. execute_model_ov() now checks that its own 
inference completed rather than the whole task.

**Relation to #24736**

#24736 makes dnn_classify call ff_dnn_get_input(), so the layout is resolved 
from the shape and googlenet-12 without layout= is filled correctly. That fixes 
the default case. The first patch here covers what is left: an explicit 
layout=nhwc on an NCHW model, a shape the dims[1] <= 3 guess gets wrong, and 
the single-channel overrun. With the check, the filter either writes the whole 
input tensor or fails. The second patch is independent of #24736: async=0 
classification fails with EIO on master and with #24736.

**Testing**

OpenVINO 2026.4, macOS arm64. dnn_detect uses a synthetic [1,3,64,64] SSD model 
with a constant output (one box per frame above the threshold). dnn_classify 
uses googlenet-12 or a synthetic [1,3,64,64] classifier with a constant output. 
The input is 25 identical frames:

    ffmpeg -nostdin -nostats -hide_banner -f lavfi -i 
smptebars=s=160x120:r=25:d=1,format=bgr24 \
      -vf 
"dnn_detect=dnn_backend=openvino:model=ssd.onnx:input=data:output=det:layout=nchw:async=A,\
    
dnn_classify=dnn_backend=openvino:model=googlenet-12.onnx:input=data_0:output=prob_1:confidence=0[:layout=nchw]:async=A,showinfo"
 \
      -f null -

Each case ran 10 times. The result of a run is a hash of the per-frame bounding 
box and classify lines from showinfo.

| build | classifier | layout= | async | result |
|---|---|---|---|---|
| master | googlenet-12 | unset | 1 | 10 different results in 10 runs, 14 
different per-frame results in one run |
| master | googlenet-12 | nchw | 1 | identical in all runs and frames (A) |
| master | any | any | 0 | exit 251, "classify for sync execution is not 
implemented" |
| master + #24736 | googlenet-12 | unset | 1 | identical to A |
| this PR | googlenet-12 | unset | 1, 0 | exit 251, "Model input has 224 
channels in NHWC layout, expected 3; set layout=nchw for an NCHW model." |
| this PR | googlenet-12 | nchw | 1, 0 | identical to A |
| this PR | synthetic | nchw | 1, 0 | identical to master with async=1 |
| this PR | synthetic | unset | 1, 0 | exit 251, "Model input has 64 channels 
..." |

The #24736 row comes from a build that also has other pending dnn patches. The 
synthetic classifier's output does not depend on its input, so on master it 
gives the same result with and without layout=.

Builds without new warnings; make fate-source and git diff --check pass.

Not tested: async=0 with several boxes per frame, IR models without layout 
information, and the pre-2.0 OpenVINO API.


>From d1ef24dee4d0caa6c0ecb69cae4f7095da974bd5 Mon Sep 17 00:00:00 2001
From: Jake Fineman <[email protected]>
Date: Tue, 27 Oct 2026 13:06:40 -0400
Subject: [PATCH 1/2] avfilter/dnn_backend_openvino: reject non-NHWC RGB
 analytics input

With OpenVINO 2.0, fill_model_input_ov() always describes the input
tensor as NHWC and takes its dimensions from the model input after
preprocessing. dnn_classify does not call ff_dnn_get_input(), so
without layout= the model layout stays unknown and an NCHW model keeps
its shape. For googlenet-12, [1,3,224,224], dnn_classify reads it as
H=3, W=224, C=224 and scales the bounding box into a 224x3 packed BGR
image. That writes 2016 of the 150528 bytes of the tensor from
ov_tensor_create(); the rest is uninitialized heap memory, and the
labels and confidences change from run to run.

The same happens with layout=nhwc on an NCHW model, or when the layout
get_input_ov() guesses from the shape is wrong. For a single-channel
NHWC model, both dnn_detect and dnn_classify write three bytes per
pixel and overrun the tensor.

Both filters always write 3-channel packed pixels, so fail with a hint
to set layout=nchw when the NHWC channel dimension is not 3.
dnn_processing is unchanged.

Tested with dnn_detect,dnn_classify and googlenet-12 on 25 identical
frames, 10 runs per case: without layout=, master gives 10 different
results in 10 runs, and this patch fails with the hint. With
layout=nchw, the results match master with layout=nchw.

Signed-off-by: Jake Fineman <[email protected]>
---
 libavfilter/dnn/dnn_backend_openvino.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/libavfilter/dnn/dnn_backend_openvino.c 
b/libavfilter/dnn/dnn_backend_openvino.c
index 82fd145a860..e23436ce8b7 100644
--- a/libavfilter/dnn/dnn_backend_openvino.c
+++ b/libavfilter/dnn/dnn_backend_openvino.c
@@ -237,6 +237,13 @@ static int fill_model_input_ov(OVModel *ov_model, 
OVRequestItem *request)
         input.dims[i] = dims[i];
     input.layout = DL_NHWC;
     input.dt = precision_to_datatype(precision);
+    // detect and classify write packed 3-channel pixels
+    if (ov_model->model.func_type != DFT_PROCESS_FRAME && input.dims[3] != 3) {
+        av_log(ctx, AV_LOG_ERROR, "Model input has %d channels in NHWC layout, 
expected 3; "
+               "set layout=nchw for an NCHW model.\n", input.dims[3]);
+        ov_shape_free(&input_shape);
+        return AVERROR(EINVAL);
+    }
 #else
     status = ie_infer_request_get_blob(request->infer_request, 
task->input_name, &input_blob);
     if (status != OK) {
-- 
2.54.0


>From 6cd9439b0899a86ebad716b72ab2101b8e06924f Mon Sep 17 00:00:00 2001
From: Jake Fineman <[email protected]>
Date: Tue, 27 Oct 2026 13:06:46 -0400
Subject: [PATCH 2/2] avfilter/dnn_backend_openvino: support dnn_classify with
 async=0

dnn_execute_model_ov() refused synchronous classification with ENOSYS,
which vf_dnn_classify turns into EIO, so dnn_detect,dnn_classify with
async=0 always failed on the first frame that reached dnn_classify.

The synchronous path ran exactly one request per frame. That fits
processing and detection, which queue one inference per frame, but
classification queues one per matching bounding box, possibly none.
With several boxes the frame's task would be left incomplete, and
execute_model_ov() would report an error because inference_done had
not reached inference_todo yet; with none, execute_model_ov() would be
called on an empty queue, where it frees the request instead of
returning it to the pool.

Run requests until the inference queue is empty, as the ONNX Runtime
backend does, and make execute_model_ov() check that its own inference
completed rather than the whole task.

Tested with dnn_detect,dnn_classify on 25 frames, 10 runs per case:
async=0 output is identical to async=1, for googlenet-12 with
layout=nchw and for a synthetic classifier, with one box per frame.

Signed-off-by: Jake Fineman <[email protected]>
---
 libavfilter/dnn/dnn_backend_openvino.c | 29 +++++++++++++-------------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/libavfilter/dnn/dnn_backend_openvino.c 
b/libavfilter/dnn/dnn_backend_openvino.c
index e23436ce8b7..f2bb22c27d4 100644
--- a/libavfilter/dnn/dnn_backend_openvino.c
+++ b/libavfilter/dnn/dnn_backend_openvino.c
@@ -1023,6 +1023,7 @@ static int execute_model_ov(OVRequestItem *request, Queue 
*inferenceq)
         }
         return 0;
     } else {
+        uint32_t done = task->inference_done;
         status = ov_infer_request_infer(request->infer_request);
         if (status != OK) {
             av_log(NULL, AV_LOG_ERROR, "Failed to start synchronous model 
inference for OV2\n");
@@ -1030,7 +1031,7 @@ static int execute_model_ov(OVRequestItem *request, Queue 
*inferenceq)
             goto err;
         }
         infer_completion_callback(request);
-        return (task->inference_done == task->inference_todo) ? 0 : 
DNN_GENERIC_ERROR;
+        return task->inference_done > done ? 0 : DNN_GENERIC_ERROR;
     }
 #else
     if (task->async) {
@@ -1048,6 +1049,7 @@ static int execute_model_ov(OVRequestItem *request, Queue 
*inferenceq)
         }
         return 0;
     } else {
+        uint32_t done = task->inference_done;
         status = ie_infer_request_infer(request->infer_request);
         if (status != OK) {
             av_log(ctx, AV_LOG_ERROR, "Failed to start synchronous model 
inference\n");
@@ -1055,7 +1057,7 @@ static int execute_model_ov(OVRequestItem *request, Queue 
*inferenceq)
             goto err;
         }
         infer_completion_callback(request);
-        return (task->inference_done == task->inference_todo) ? 0 : 
DNN_GENERIC_ERROR;
+        return task->inference_done > done ? 0 : DNN_GENERIC_ERROR;
     }
 #endif
 err:
@@ -1550,24 +1552,23 @@ static int dnn_execute_model_ov(const DNNModel *model, 
DNNExecBaseParams *exec_p
         return 0;
     }
     else {
-        if (model->func_type == DFT_ANALYTICS_CLASSIFY) {
-            // Classification filter has not been completely
-            // tested with the sync mode. So, do not support now.
-            avpriv_report_missing_feature(ctx, "classify for sync execution");
-            return AVERROR(ENOSYS);
-        }
-
         if (ctx->batch_size > 1) {
             avpriv_report_missing_feature(ctx, "batch mode for sync 
execution");
             return AVERROR(ENOSYS);
         }
 
-        request = ff_safe_queue_pop_front(ov_model->request_queue);
-        if (!request) {
-            av_log(ctx, AV_LOG_ERROR, "unable to get infer request.\n");
-            return AVERROR(EINVAL);
+        // classify queues one inference per bounding box, possibly none
+        while (ff_queue_size(ov_model->lltask_queue) > 0) {
+            request = ff_safe_queue_pop_front(ov_model->request_queue);
+            if (!request) {
+                av_log(ctx, AV_LOG_ERROR, "unable to get infer request.\n");
+                return AVERROR(EINVAL);
+            }
+            ret = execute_model_ov(request, ov_model->lltask_queue);
+            if (ret != 0)
+                return ret;
         }
-        return execute_model_ov(request, ov_model->lltask_queue);
+        return 0;
     }
 fail:
     av_frame_free(&exec_params->in_frame);
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to