Commit 98e92563a3 for ffmpeg

commit 98e92563a3b60dbf6d370fd3491d7f896398e4c1
Author: Minh Anh Nguyen <dongngu@amd.com>
Date:   Thu Oct 1 11:14:53 2026 -0400

    avfilter/dnn: add dnn_classify support to ONNX Runtime backend

    Add dnn_classify filter support to the ONNX Runtime DNN backend so
    classification inference can run with ONNX models.

    Classifier outputs of rank 2 [N,C] and rank 1 [C] are promoted to
    [N,C,1,1], since common classification models produce these shapes.
    The output batch dimension is validated to equal 1 before
    post-processing.

    Fix a deadlock when a frame has no bounding box to classify: the
    request is returned to the queue instead of being destroyed, and the
    lltask queue is checked before popping a request. Frames with multiple
    bounding boxes are handled by looping through all queued lltasks in
    execute_model_onnx.

    Bounding box boundary checks use > instead of >=, so a box ending
    exactly at the frame edge is no longer rejected.

diff --git a/libavfilter/dnn/dnn_backend_onnx.c b/libavfilter/dnn/dnn_backend_onnx.c
index 009387b36d..94bc13d719 100644
--- a/libavfilter/dnn/dnn_backend_onnx.c
+++ b/libavfilter/dnn/dnn_backend_onnx.c
@@ -25,6 +25,7 @@

 #include "libavutil/opt.h"
 #include "libavutil/avassert.h"
+#include "libavutil/detection_bbox.h"
 #include "libavutil/imgutils.h"
 #include "libavutil/mem.h"
 #include "libavutil/avstring.h"
@@ -97,25 +98,106 @@ static void init_ort_api(void)
         }                                                       \
     } while (0)

-static int extract_lltask_from_task(TaskItem *task, Queue *lltask_queue)
+static int contain_valid_detection_bbox(AVFrame *frame)
+{
+    AVFrameSideData *sd;
+    const AVDetectionBBoxHeader *header;
+    const AVDetectionBBox *bbox;
+
+    sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DETECTION_BBOXES);
+    if (!sd)
+        return 0;
+
+    if (!sd->size)
+        return 0;
+
+    header = (const AVDetectionBBoxHeader *)sd->data;
+    if (!header->nb_bboxes)
+        return 0;
+
+    for (uint32_t i = 0; i < header->nb_bboxes; i++) {
+        bbox = av_get_detection_bbox(header, i);
+        if (bbox->x < 0 || bbox->w < 0 || bbox->x + bbox->w > frame->width)
+            return 0;
+        if (bbox->y < 0 || bbox->h < 0 || bbox->y + bbox->h > frame->height)
+            return 0;
+        if (bbox->classify_count == AV_NUM_DETECTION_BBOX_CLASSIFY)
+            return 0;
+    }
+
+    return 1;
+}
+
+static int extract_lltask_from_task(DNNFunctionType func_type, TaskItem *task,
+                                    Queue *lltask_queue, DNNExecBaseParams *exec_params)
 {
     ONNXModel     *onnx_model = (ONNXModel *)task->model;
     DnnContext           *ctx = onnx_model->ctx;
-    LastLevelTaskItem *lltask = av_malloc(sizeof(*lltask));

-    if (!lltask) {
-        av_log(ctx, AV_LOG_ERROR, "Failed to allocate memory for LastLevelTaskItem\n");
-        return AVERROR(ENOMEM);
+    switch (func_type) {
+    case DFT_PROCESS_FRAME:
+    case DFT_ANALYTICS_DETECT:
+    {
+        LastLevelTaskItem *lltask = av_malloc(sizeof(*lltask));
+        if (!lltask) {
+            av_log(ctx, AV_LOG_ERROR, "Failed to allocate memory for LastLevelTaskItem\n");
+            return AVERROR(ENOMEM);
+        }
+        task->inference_todo = 1;
+        task->inference_done = 0;
+        lltask->task = task;
+        if (ff_queue_push_back(lltask_queue, lltask) < 0) {
+            av_log(ctx, AV_LOG_ERROR, "Failed to push back lltask_queue.\n");
+            av_freep(&lltask);
+            return AVERROR(ENOMEM);
+        }
+        return 0;
     }
-    task->inference_todo = 1;
-    task->inference_done = 0;
-    lltask->task = task;
-    if (ff_queue_push_back(lltask_queue, lltask) < 0) {
-        av_log(ctx, AV_LOG_ERROR, "Failed to push back lltask_queue.\n");
-        av_freep(&lltask);
-        return AVERROR(ENOMEM);
+    case DFT_ANALYTICS_CLASSIFY:
+    {
+        const AVDetectionBBoxHeader *header;
+        AVFrame *frame = task->in_frame;
+        AVFrameSideData *sd;
+        DNNExecClassificationParams *params = (DNNExecClassificationParams *)exec_params;
+
+        task->inference_todo = 0;
+        task->inference_done = 0;
+
+        if (!contain_valid_detection_bbox(frame))
+            return 0;
+
+        sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DETECTION_BBOXES);
+        header = (const AVDetectionBBoxHeader *)sd->data;
+
+        for (uint32_t i = 0; i < header->nb_bboxes; i++) {
+            LastLevelTaskItem *lltask;
+            const AVDetectionBBox *bbox = av_get_detection_bbox(header, i);
+
+            if (params->target) {
+                if (av_strncasecmp(bbox->detect_label, params->target, sizeof(bbox->detect_label)) != 0)
+                    continue;
+            }
+
+            lltask = av_malloc(sizeof(*lltask));
+            if (!lltask) {
+                av_log(ctx, AV_LOG_ERROR, "Failed to allocate memory for LastLevelTaskItem\n");
+                return AVERROR(ENOMEM);
+            }
+            task->inference_todo++;
+            lltask->task = task;
+            lltask->bbox_index = i;
+            if (ff_queue_push_back(lltask_queue, lltask) < 0) {
+                av_log(ctx, AV_LOG_ERROR, "Failed to push back lltask_queue.\n");
+                av_freep(&lltask);
+                return AVERROR(ENOMEM);
+            }
+        }
+        return 0;
+    }
+    default:
+        avpriv_report_missing_feature(ctx, "model function type %d", func_type);
+        return AVERROR(ENOSYS);
     }
-    return 0;
 }

 static void onnx_free_request(ONNXInferRequest *request)
@@ -435,6 +517,11 @@ static int fill_model_input_onnx(ONNXModel *onnx_model, ONNXRequestItem *request
         if (ret < 0)
             goto err;
         break;
+    case DFT_ANALYTICS_CLASSIFY:
+        ret = ff_frame_to_dnn_classify(task->in_frame, &input, lltask->bbox_index, ctx);
+        if (ret < 0)
+            goto err;
+        break;
     default:
         avpriv_report_missing_feature(ctx, "model function type %d", onnx_model->model.func_type);
         ret = AVERROR(ENOSYS);
@@ -673,6 +760,18 @@ static void infer_completion_callback(void *args)
             outputs[i].dims[1] = 1;
             outputs[i].dims[2] = dims[1];
             outputs[i].dims[3] = dims[2];
+        } else if (num_dims == 2) {
+            /* [N, C] -> [N, C, 1, 1] */
+            outputs[i].dims[0] = dims[0];
+            outputs[i].dims[1] = dims[1];
+            outputs[i].dims[2] = 1;
+            outputs[i].dims[3] = 1;
+        } else if (num_dims == 1) {
+            /* [C] -> [1, C, 1, 1] */
+            outputs[i].dims[0] = 1;
+            outputs[i].dims[1] = dims[0];
+            outputs[i].dims[2] = 1;
+            outputs[i].dims[3] = 1;
         } else {
             avpriv_report_missing_feature(ctx,
                 "Support for %zu-dimensional output (tensor[%u])", num_dims, i);
@@ -681,6 +780,15 @@ static void infer_completion_callback(void *args)
             goto err;
         }

+        if (outputs[i].dims[0] != 1) {
+            av_log(ctx, AV_LOG_ERROR,
+                   "Output tensor[%u] batch size %d unsupported, must be 1\n",
+                   i, outputs[i].dims[0]);
+            av_free(dims);
+            g_ort->ReleaseTensorTypeAndShapeInfo(tensor_info);
+            goto err;
+        }
+
         status = g_ort->GetTensorMutableData(infer_request->output_tensors[i], &outputs[i].data);
         if (status != NULL) {
             av_log(ctx, AV_LOG_ERROR, "Failed to get tensor[%u] data pointer\n", i);
@@ -715,6 +823,15 @@ static void infer_completion_callback(void *args)
         if (ret < 0)
             goto err;
         break;
+    case DFT_ANALYTICS_CLASSIFY:
+        if (!onnx_model->model.classify_post_proc) {
+            av_log(ctx, AV_LOG_ERROR, "classify filter needs to provide classify_post_proc\n");
+            goto err;
+        }
+        onnx_model->model.classify_post_proc(task->in_frame, outputs,
+                                             request->lltask->bbox_index,
+                                             onnx_model->model.filter_ctx);
+        break;
     default:
         avpriv_report_missing_feature(ctx, "model function type %d", onnx_model->model.func_type);
         goto err;
@@ -732,45 +849,54 @@ err:
     }
 }

-static int execute_model_onnx(ONNXRequestItem *request, Queue *lltask_queue)
+static int execute_model_onnx(ONNXModel *onnx_model, ONNXRequestItem *request, Queue *lltask_queue)
 {
-    ONNXModel *onnx_model = NULL;
     LastLevelTaskItem *lltask;
     TaskItem *task = NULL;
     int ret = 0;

     if (ff_queue_size(lltask_queue) == 0) {
-        destroy_request_item(&request);
+        if (ff_safe_queue_push_back(onnx_model->request_queue, request) < 0) {
+            destroy_request_item(&request);
+        }
         return 0;
     }

-    lltask = (LastLevelTaskItem *)ff_queue_peek_front(lltask_queue);
-    if (lltask == NULL) {
-        av_log(NULL, AV_LOG_ERROR, "Failed to get LastLevelTaskItem\n");
-        destroy_request_item(&request);
-        return AVERROR(EINVAL);
-    }
-    task = lltask->task;
-    onnx_model = (ONNXModel *)task->model;
+    /* Drain all lltasks for the current frame. */
+    for (;;) {
+        lltask = (LastLevelTaskItem *)ff_queue_peek_front(lltask_queue);
+        if (lltask == NULL) {
+            av_log(NULL, AV_LOG_ERROR, "Failed to get LastLevelTaskItem\n");
+            ret = AVERROR(EINVAL);
+            goto err;
+        }
+        task = lltask->task;

-    ret = fill_model_input_onnx(onnx_model, request);
-    if (ret != 0) {
-        goto err;
-    }
+        ret = fill_model_input_onnx(onnx_model, request);
+        if (ret != 0) {
+            goto err;
+        }
+
+        if (task->async) {
+            avpriv_report_missing_feature(onnx_model->ctx, "ONNX async inference");
+            ret = AVERROR(ENOSYS);
+            goto err;
+        }

-    if (task->async) {
-        avpriv_report_missing_feature(onnx_model->ctx, "ONNX async inference");
-        ret = AVERROR(ENOSYS);
-        goto err;
-    } else {
         ret = onnx_start_inference((void *)request);
         if (ret != 0) {
             goto err;
         }
         infer_completion_callback(request);
-        return (task->inference_done == task->inference_todo) ? 0 : DNN_GENERIC_ERROR;
+
+        if (ff_queue_size(lltask_queue) == 0) {
+            break;
+        }
+        request = (ONNXRequestItem *)ff_safe_queue_pop_front(onnx_model->request_queue);
     }

+    return (task->inference_done == task->inference_todo) ? 0 : DNN_GENERIC_ERROR;
+
 err:
     av_freep(&request->lltask);
     onnx_free_request(request->infer_request);
@@ -801,7 +927,7 @@ static int get_output_onnx(DNNModel *model, const char *input_name, int input_wi
         goto err;
     }

-    ret = extract_lltask_from_task(&task, onnx_model->lltask_queue);
+    ret = extract_lltask_from_task(DFT_PROCESS_FRAME, &task, onnx_model->lltask_queue, NULL);
     if (ret != 0) {
         av_log(ctx, AV_LOG_ERROR, "Unable to extract last level task from task.\n");
         goto err;
@@ -814,7 +940,7 @@ static int get_output_onnx(DNNModel *model, const char *input_name, int input_wi
         goto err;
     }

-    ret = execute_model_onnx(request, onnx_model->lltask_queue);
+    ret = execute_model_onnx(onnx_model, request, onnx_model->lltask_queue);
     *output_width = task.out_frame->width;
     *output_height = task.out_frame->height;

@@ -1174,19 +1300,24 @@ static int dnn_execute_model_onnx(const DNNModel *model, DNNExecBaseParams *exec
         return ret;
     }

-    ret = extract_lltask_from_task(task, onnx_model->lltask_queue);
+    ret = extract_lltask_from_task(model->func_type, task, onnx_model->lltask_queue, exec_params);
     if (ret != 0) {
         av_log(ctx, AV_LOG_ERROR, "Unable to extract last level task from task.\n");
         return ret;
     }

+    /* No lltasks queued, nothing to infer. */
+    if (ff_queue_size(onnx_model->lltask_queue) == 0) {
+        return 0;
+    }
+
     request = (ONNXRequestItem *)ff_safe_queue_pop_front(onnx_model->request_queue);
     if (!request) {
         av_log(ctx, AV_LOG_ERROR, "Unable to get infer request.\n");
         return AVERROR(EINVAL);
     }

-    return execute_model_onnx(request, onnx_model->lltask_queue);
+    return execute_model_onnx(onnx_model, request, onnx_model->lltask_queue);
 }

 static DNNAsyncStatusType dnn_get_result_onnx(const DNNModel *model, AVFrame **in, AVFrame **out)
@@ -1209,7 +1340,7 @@ static int dnn_flush_onnx(const DNNModel *model)
         return AVERROR(EINVAL);
     }

-    return execute_model_onnx(request, onnx_model->lltask_queue);
+    return execute_model_onnx(onnx_model, request, onnx_model->lltask_queue);
 }

 const DNNModule ff_dnn_backend_onnx = {
diff --git a/libavfilter/dnn/dnn_io_proc.c b/libavfilter/dnn/dnn_io_proc.c
index 0a18c56b84..6b79336766 100644
--- a/libavfilter/dnn/dnn_io_proc.c
+++ b/libavfilter/dnn/dnn_io_proc.c
@@ -337,6 +337,9 @@ static enum AVPixelFormat get_pixel_format(DNNData *data)
     return AV_PIX_FMT_BGR24;
 }

+static void detect_write_tensor(DNNData *input, const uint8_t *src,
+                                int src_linesize, int w, int h);
+
 int ff_frame_to_dnn_classify(AVFrame *frame, DNNData *input, uint32_t bbox_index, void *log_ctx)
 {
     const AVPixFmtDescriptor *desc;
@@ -362,11 +365,6 @@ int ff_frame_to_dnn_classify(AVFrame *frame, DNNData *input, uint32_t bbox_index
         return AVERROR(ENOSYS);
     }

-    if (input->layout == DL_NCHW) {
-        av_log(log_ctx, AV_LOG_ERROR, "dnn_classify input data doesn't support layout: NCHW\n");
-        return AVERROR(ENOSYS);
-    }
-
     width_idx = dnn_get_width_idx_by_layout(input->layout);
     height_idx = dnn_get_height_idx_by_layout(input->layout);

@@ -378,28 +376,6 @@ int ff_frame_to_dnn_classify(AVFrame *frame, DNNData *input, uint32_t bbox_index
     top = bbox->y;
     height = bbox->h;

-    fmt = get_pixel_format(input);
-    sws_ctx = sws_getContext(width, height, frame->format,
-                             input->dims[width_idx],
-                             input->dims[height_idx], fmt,
-                             SWS_FAST_BILINEAR, NULL, NULL, NULL);
-    if (!sws_ctx) {
-        av_log(log_ctx, AV_LOG_ERROR, "Failed to create scale context for the conversion "
-               "fmt:%s s:%dx%d -> fmt:%s s:%dx%d\n",
-               av_get_pix_fmt_name(frame->format), width, height,
-               av_get_pix_fmt_name(fmt),
-               input->dims[width_idx],
-               input->dims[height_idx]);
-        return AVERROR(EINVAL);
-    }
-
-    ret = av_image_fill_linesizes(linesizes, fmt, input->dims[width_idx]);
-    if (ret < 0) {
-        av_log(log_ctx, AV_LOG_ERROR, "unable to get linesizes with av_image_fill_linesizes");
-        sws_freeContext(sws_ctx);
-        return ret;
-    }
-
     desc = av_pix_fmt_desc_get(frame->format);
     offsetx[1] = offsetx[2] = AV_CEIL_RSHIFT(left, desc->log2_chroma_w);
     offsetx[0] = offsetx[3] = left;
@@ -411,11 +387,66 @@ int ff_frame_to_dnn_classify(AVFrame *frame, DNNData *input, uint32_t bbox_index
     for (int k = 0; frame->data[k]; k++)
         bbox_data[k] = frame->data[k] + offsety[k] * frame->linesize[k] + offsetx[k] * max_step[k];

-    sws_scale(sws_ctx, (const uint8_t *const *)&bbox_data, frame->linesize,
-                       0, height,
-                       (uint8_t *const [4]){input->data, 0, 0, 0}, linesizes);
+    if (input->layout == DL_NCHW) {
+        /*
+         * For NCHW layout, scale the bbox region into a packed RGB temp buffer,
+         * then deinterleave into the NCHW tensor using detect_write_tensor().
+         */
+        int dst_w = input->dims[width_idx];
+        int dst_h = input->dims[height_idx];
+        int tmp_linesize = dst_w * 3;
+        uint8_t *tmp_buf = av_malloc(tmp_linesize * dst_h);
+        if (!tmp_buf)
+            return AVERROR(ENOMEM);

-    sws_freeContext(sws_ctx);
+        sws_ctx = sws_getContext(width, height, frame->format,
+                                 dst_w, dst_h, AV_PIX_FMT_RGB24,
+                                 SWS_FAST_BILINEAR, NULL, NULL, NULL);
+        if (!sws_ctx) {
+            av_log(log_ctx, AV_LOG_ERROR, "Failed to create scale context for the conversion "
+                   "fmt:%s s:%dx%d -> fmt:%s s:%dx%d\n",
+                   av_get_pix_fmt_name(frame->format), width, height,
+                   av_get_pix_fmt_name(AV_PIX_FMT_RGB24), dst_w, dst_h);
+            av_free(tmp_buf);
+            return AVERROR(EINVAL);
+        }
+
+        sws_scale(sws_ctx, (const uint8_t *const *)&bbox_data, frame->linesize,
+                           0, height,
+                           (uint8_t *const [4]){tmp_buf, 0, 0, 0},
+                           (const int [4]){tmp_linesize, 0, 0, 0});
+        sws_freeContext(sws_ctx);
+
+        detect_write_tensor(input, tmp_buf, tmp_linesize, dst_w, dst_h);
+        av_free(tmp_buf);
+    } else {
+        fmt = get_pixel_format(input);
+        sws_ctx = sws_getContext(width, height, frame->format,
+                                 input->dims[width_idx],
+                                 input->dims[height_idx], fmt,
+                                 SWS_FAST_BILINEAR, NULL, NULL, NULL);
+        if (!sws_ctx) {
+            av_log(log_ctx, AV_LOG_ERROR, "Failed to create scale context for the conversion "
+                   "fmt:%s s:%dx%d -> fmt:%s s:%dx%d\n",
+                   av_get_pix_fmt_name(frame->format), width, height,
+                   av_get_pix_fmt_name(fmt),
+                   input->dims[width_idx],
+                   input->dims[height_idx]);
+            return AVERROR(EINVAL);
+        }
+
+        ret = av_image_fill_linesizes(linesizes, fmt, input->dims[width_idx]);
+        if (ret < 0) {
+            av_log(log_ctx, AV_LOG_ERROR, "unable to get linesizes with av_image_fill_linesizes");
+            sws_freeContext(sws_ctx);
+            return ret;
+        }
+
+        sws_scale(sws_ctx, (const uint8_t *const *)&bbox_data, frame->linesize,
+                           0, height,
+                           (uint8_t *const [4]){input->data, 0, 0, 0}, linesizes);
+        sws_freeContext(sws_ctx);
+    }

     return ret;
 }
diff --git a/libavfilter/vf_dnn_classify.c b/libavfilter/vf_dnn_classify.c
index f92c41ab76..8faf6685ee 100644
--- a/libavfilter/vf_dnn_classify.c
+++ b/libavfilter/vf_dnn_classify.c
@@ -48,6 +48,9 @@ static const AVOption dnn_classify_options[] = {
     { "dnn_backend", "DNN backend",                OFFSET(backend_type),     AV_OPT_TYPE_INT,       { .i64 = DNN_OV },    INT_MIN, INT_MAX, FLAGS, .unit = "backend" },
 #if (CONFIG_LIBOPENVINO == 1)
     { "openvino",    "openvino backend flag",      0,                        AV_OPT_TYPE_CONST,     { .i64 = DNN_OV },    0, 0, FLAGS, .unit = "backend" },
+#endif
+#if (CONFIG_LIBONNXRUNTIME == 1)
+    { "onnx",        "onnx backend flag",          0,                        AV_OPT_TYPE_CONST,     { .i64 = DNN_ONNX },  0, 0, FLAGS, .unit = "backend" },
 #endif
     { "confidence",  "threshold of confidence",    OFFSET2(confidence),      AV_OPT_TYPE_FLOAT,     { .dbl = 0.5 },  0, 1, FLAGS},
     { "labels",      "path to labels file",        OFFSET2(labels_filename), AV_OPT_TYPE_STRING,    { .str = NULL }, 0, 0, FLAGS },
@@ -55,7 +58,7 @@ static const AVOption dnn_classify_options[] = {
     { NULL }
 };

-AVFILTER_DNN_DEFINE_CLASS(dnn_classify, DNN_OV);
+AVFILTER_DNN_DEFINE_CLASS(dnn_classify, DNN_OV | DNN_ONNX);

 static int dnn_classify_post_proc(AVFrame *frame, DNNData *output, uint32_t bbox_index, AVFilterContext *filter_ctx)
 {