cudaimgproc: calcHist / histEven / equalizeHist on a GpuMat ROI with x offset > 4 count pixels outside the ROI (the offsetX handling from #3475 only works for offsets 0..4)

Author: SichenLiangCreated Sep 8, 2026Updated Sep 8, 2026
System information (version)
  • OpenCV => 5.x, opencv f8ef5fd (2026-09-06) + opencv_contrib 17af220 (2026-08-14), built from source with CUDA 12.6, CUDA_ARCH_BIN=8.7
  • Operating System / Platform => Ubuntu 22.04.5 aarch64 (NVIDIA Jetson Orin Nano, JetPack 6.2.3); only this device was available, but nothing in the code path depends on the device
  • Compiler => GCC 11.4.0, nvcc 12.6
Detailed description

Since #3475 (fixes #3473) histogram256Kernel handles a source whose rows are not 4-byte aligned: modules/cudaimgproc/src/cuda/hist.cu

cpp
        const int alignedOffset = fourByteAligned ? 0 : 4 - offsetX;

reads the first alignedOffset pixels of each row one by one and the rest as 32-bit words starting at src + alignedOffset (lines 69-83), then a scalar tail (lines 86-90). The caller passes the ROI's absolute x offset, modules/cudaimgproc/src/histogram.cpp:

cpp
    Point ofs; Size wholeSize;
    src.locateROI(wholeSize, ofs);
    if (mask.empty())
        hist::histogram256(src, hist.ptr<int>(), ofs.x, StreamAccessor::getStream(stream));

(the same for histEven8u, lines 595-599). This is only right for ofs.x in 0..4. For ofs.x >= 5, alignedOffset is negative: the head loop does nothing, the word loop starts ofs.x - 4 bytes before the ROI and covers (cols - alignedOffset) / 4 words, so every row contributes ofs.x - 4 extra pixels taken from the parent matrix to the left of the ROI.

Measured with a zero-filled parent and a ROI filled with values 1..255 (so every wrongly counted pixel lands in bin 0), 8 rows: x offsets 0-4 give the correct total for widths 16, 17, 113 and 640; offset 5 adds 8 (one pixel per row), 6 adds 16, 7 adds 24, 8 adds 32, 9 adds 40, 12 adds 64, 13 adds 72, for calcHist and histEven alike. equalizeHist on such a ROI is wrong accordingly (16x8 ROI at (5,3): 126 of 128 pixels differ from cv::equalizeHist, max 16; 113x50 at (5,3): 5635 of 5650, max 3). A contiguous copy of the same ROI gives the correct histogram. Offsets that are not a multiple of 4 but at most 4 work (x offset 1 on a 640x480 ROI: correct).

The masked overload calcHist(src, mask, hist) has the same offset handling and two more problems in the same kernel, hist.cu lines 138-139:

cpp
            const unsigned int* rowPtrIntAligned = (const unsigned int*)(fourByteAligned ? &src[y * srcStep] : &src[alignedOffset + y * maskStep]);
            const unsigned int* maskRowPtrIntAligned = (const unsigned int*)(fourByteAligned ? &mask[y * maskStep] : &mask[alignedOffset + y * maskStep]);

the source word pointer is built with maskStep instead of srcStep, and the mask word pointer is shifted by the source's alignedOffset. Measured with the same source ROI (width 16, every mask pixel set): a mask with the same geometry as the source shows the same extra counts at offsets 5, 6 and 8; a mask that is a ROI of a wider parent (mask step 1024, source step 512) gives wrong bins already at offsets 1 to 4 (45 to 58 of the 256 bins differ); a contiguous, 4-byte aligned mask with the source at offset 1, 2, 3, 5 or 6 fails with a sticky misaligned address CUDA error (offsets 0, 4 and 8 work).

Suggested fix: pass ofs.x % 4 (or mask with & 3 in the dispatcher) and add a test with a ROI x offset larger than 4, the same for histEven8u; in the masked kernel use srcStep for the source pointer and derive the mask's word offset from the mask's own pointer (a mask that shares the source's x offset and step works today, a mask with a different offset or step does not). The existing tests only use ROI x offsets 0 to 3 (modules/cudaimgproc/test/test_histogram.cpp, lines 53-76) and CalcHistWithMask takes its mask from a parent of the same size at the same ROI (lines 184-192), which is why none of this is caught.

Steps to reproduce
// repro_G_cuda_hist_roi.cpp — cv::cuda::calcHist / histEven / equalizeHist on a GpuMat ROI: do they count pixels
// outside the ROI?   usage: repro_G <width> <height> <xoff> <yoff> <parent_extra_cols>
#include <opencv2/core.hpp>
#include <opencv2/core/cuda.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <cstdio>
#include <cstdlib>
int main(int argc, char** argv) {
    int w = argc > 1 ? atoi(argv[1]) : 16, h = argc > 2 ? atoi(argv[2]) : 8, xo = argc > 3 ? atoi(argv[3]) : 5, yo = argc > 4 ? atoi(argv[4]) : 3, extra = argc > 5 ? atoi(argv[5]) : 9;
    cv::Mat parent(h + yo + 4, w + xo + extra, CV_8UC1, cv::Scalar(0)); // everything outside the ROI is 0
    cv::Mat roi = parent(cv::Rect(xo, yo, w, h)); cv::RNG rng(1); rng.fill(roi, cv::RNG::UNIFORM, 1, 256); // ROI pixels are 1..255, never 0
    cv::cuda::GpuMat gparent(parent); cv::cuda::GpuMat groi = gparent(cv::Rect(xo, yo, w, h)), gh, ge, geq;
    int ch[] = {0}, hs[] = {256}; float r[] = {0, 256}; const float* rr[] = {r}; cv::Mat hc; cv::calcHist(&roi, 1, ch, cv::Mat(), hc, 1, hs, rr);
    cv::cuda::calcHist(groi, gh); cv::cuda::histEven(groi, ge, 256, 0, 256); cv::Mat h1, h2; gh.download(h1); ge.download(h2);
    double s0 = cv::sum(hc)[0], s1 = cv::sum(h1)[0], s2 = cv::sum(h2)[0];
    printf("ROI %dx%d at (%d,%d), parent %dx%d, step=%zu, data%%4=%zu\n", w, h, xo, yo, parent.cols, parent.rows, groi.step, (size_t)groi.data % 4);
    printf("calcHist: cpu total=%.0f bin0=%.0f | cuda::calcHist total=%.0f bin0=%d | cuda::histEven total=%.0f bin0=%d   (ROI has %d pixels, none of them 0)\n",
           s0, hc.at<float>(0), s1, h1.at<int>(0), s2, h2.at<int>(0), w * h);
    cv::Mat hcf; hc.reshape(1, 1).convertTo(hcf, CV_32S);
    printf("calcHist bins differing from cpu: cuda::calcHist %d, cuda::histEven %d\n", cv::countNonZero(hcf != h1.reshape(1, 1)), cv::countNonZero(hcf != h2.reshape(1, 1)));
    cv::Mat ceq, geqh; cv::equalizeHist(roi, ceq); cv::cuda::equalizeHist(groi, geq); geq.download(geqh);
    printf("equalizeHist: max|cpu-gpu| = %g, pixels differing = %d / %d\n", cv::norm(ceq, geqh, cv::NORM_INF), cv::countNonZero(ceq != geqh), w * h);
    // control: contiguous copy of the same ROI
    cv::cuda::GpuMat gcopy(roi), gh3; cv::cuda::calcHist(gcopy, gh3); cv::Mat h3; gh3.download(h3);
    printf("control (contiguous copy of the ROI): cuda::calcHist total=%.0f bin0=%d, bins differing from cpu %d\n", cv::sum(h3)[0], h3.at<int>(0), cv::countNonZero(hcf != h3.reshape(1, 1)));
    return 0;
}

Built with g++ -std=c++17 -O2 repro_G.cpp -o repro_G -I<build> -I<opencv>/include -I<opencv>/modules/core/include -I<opencv>/modules/imgproc/include -I<contrib>/modules/cudaimgproc/include -I<contrib>/modules/cudev/include -I/usr/local/cuda/include -L<build>/lib -lopencv_cudaimgproc -lopencv_imgproc -lopencv_core. Output of ./repro_G 16 8 5 3 9, ./repro_G 16 8 0 0 9, ./repro_G 16 8 4 0 9, ./repro_G 113 50 5 3 9, ./repro_G 113 50 0 0 0, ./repro_G 640 480 1 1 1, ./repro_G 640 480 0 0 0:

ROI 16x8 at (5,3), parent 30x15, step=512, data%4=1
calcHist: cpu total=128 bin0=0 | cuda::calcHist total=136 bin0=8 | cuda::histEven total=136 bin0=8   (ROI has 128 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 1, cuda::histEven 1
equalizeHist: max|cpu-gpu| = 16, pixels differing = 126 / 128
control (contiguous copy of the ROI): cuda::calcHist total=128 bin0=0, bins differing from cpu 0
ROI 16x8 at (0,0), parent 25x12, step=512, data%4=0
calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 0, cuda::histEven 0
equalizeHist: max|cpu-gpu| = 0, pixels differing = 0 / 128
control (contiguous copy of the ROI): cuda::calcHist total=128 bin0=0, bins differing from cpu 0
ROI 16x8 at (4,0), parent 29x12, step=512, data%4=0
calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 0, cuda::histEven 0
equalizeHist: max|cpu-gpu| = 0, pixels differing = 0 / 128
control (contiguous copy of the ROI): cuda::calcHist total=128 bin0=0, bins differing from cpu 0
ROI 113x50 at (5,3), parent 127x57, step=512, data%4=1
calcHist: cpu total=5650 bin0=0 | cuda::calcHist total=5700 bin0=50 | cuda::histEven total=5700 bin0=50   (ROI has 5650 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 1, cuda::histEven 1
equalizeHist: max|cpu-gpu| = 3, pixels differing = 5635 / 5650
control (contiguous copy of the ROI): cuda::calcHist total=5650 bin0=0, bins differing from cpu 0
ROI 113x50 at (0,0), parent 113x54, step=512, data%4=0
calcHist: cpu total=5650 bin0=0 | cuda::calcHist total=5650 bin0=0 | cuda::histEven total=5650 bin0=0   (ROI has 5650 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 0, cuda::histEven 0
equalizeHist: max|cpu-gpu| = 0, pixels differing = 0 / 5650
control (contiguous copy of the ROI): cuda::calcHist total=5650 bin0=0, bins differing from cpu 0
ROI 640x480 at (1,1), parent 642x485, step=1024, data%4=1
calcHist: cpu total=307200 bin0=0 | cuda::calcHist total=307200 bin0=0 | cuda::histEven total=307200 bin0=0   (ROI has 307200 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 0, cuda::histEven 0
equalizeHist: max|cpu-gpu| = 0, pixels differing = 0 / 307200
control (contiguous copy of the ROI): cuda::calcHist total=307200 bin0=0, bins differing from cpu 0
ROI 640x480 at (0,0), parent 640x484, step=1024, data%4=0
calcHist: cpu total=307200 bin0=0 | cuda::calcHist total=307200 bin0=0 | cuda::histEven total=307200 bin0=0   (ROI has 307200 pixels, none of them 0)
calcHist bins differing from cpu: cuda::calcHist 0, cuda::histEven 0
equalizeHist: max|cpu-gpu| = 0, pixels differing = 0 / 307200
control (contiguous copy of the ROI): cuda::calcHist total=307200 bin0=0, bins differing from cpu 0

Totals by x offset (./repro_G $w 8 $xo 0 16, second output line, widths 16 and 113 shown; 17 and 640 behave the same):

w=16 xoff=0: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
w=16 xoff=1: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
w=16 xoff=2: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
w=16 xoff=3: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
w=16 xoff=4: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=128 bin0=0 | cuda::histEven total=128 bin0=0   (ROI has 128 pixels, none of them 0)
w=16 xoff=5: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=136 bin0=8 | cuda::histEven total=136 bin0=8   (ROI has 128 pixels, none of them 0)
w=16 xoff=6: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=144 bin0=16 | cuda::histEven total=144 bin0=16   (ROI has 128 pixels, none of them 0)
w=16 xoff=7: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=152 bin0=24 | cuda::histEven total=152 bin0=24   (ROI has 128 pixels, none of them 0)
w=16 xoff=8: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=160 bin0=32 | cuda::histEven total=160 bin0=32   (ROI has 128 pixels, none of them 0)
w=16 xoff=9: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=168 bin0=40 | cuda::histEven total=168 bin0=40   (ROI has 128 pixels, none of them 0)
w=16 xoff=12: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=192 bin0=64 | cuda::histEven total=192 bin0=64   (ROI has 128 pixels, none of them 0)
w=16 xoff=13: calcHist: cpu total=128 bin0=0 | cuda::calcHist total=200 bin0=72 | cuda::histEven total=200 bin0=72   (ROI has 128 pixels, none of them 0)
w=113 xoff=0: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=904 bin0=0 | cuda::histEven total=904 bin0=0   (ROI has 904 pixels, none of them 0)
w=113 xoff=1: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=904 bin0=0 | cuda::histEven total=904 bin0=0   (ROI has 904 pixels, none of them 0)
w=113 xoff=2: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=904 bin0=0 | cuda::histEven total=904 bin0=0   (ROI has 904 pixels, none of them 0)
w=113 xoff=3: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=904 bin0=0 | cuda::histEven total=904 bin0=0   (ROI has 904 pixels, none of them 0)
w=113 xoff=4: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=904 bin0=0 | cuda::histEven total=904 bin0=0   (ROI has 904 pixels, none of them 0)
w=113 xoff=5: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=912 bin0=8 | cuda::histEven total=912 bin0=8   (ROI has 904 pixels, none of them 0)
w=113 xoff=6: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=920 bin0=16 | cuda::histEven total=920 bin0=16   (ROI has 904 pixels, none of them 0)
w=113 xoff=7: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=928 bin0=24 | cuda::histEven total=928 bin0=24   (ROI has 904 pixels, none of them 0)
w=113 xoff=8: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=936 bin0=32 | cuda::histEven total=936 bin0=32   (ROI has 904 pixels, none of them 0)
w=113 xoff=9: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=944 bin0=40 | cuda::histEven total=944 bin0=40   (ROI has 904 pixels, none of them 0)
w=113 xoff=12: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=968 bin0=64 | cuda::histEven total=968 bin0=64   (ROI has 904 pixels, none of them 0)
w=113 xoff=13: calcHist: cpu total=904 bin0=0 | cuda::calcHist total=976 bin0=72 | cuda::histEven total=976 bin0=72   (ROI has 904 pixels, none of them 0)

Masked overload (repro_G2, below; ./repro_G2 16 $xo same|wide|contig, one process per trial):

// repro_G2_cuda_calcHist_mask_roi.cpp — the masked cv::cuda::calcHist(src, mask, hist) overload on a ROI source:
// mask with the same geometry as the source ROI (same parent width) vs a mask that is a ROI of a wider parent
// (different step) vs a contiguous mask.  usage: repro_G2 <width> <xoff> <same|wide|contig>
#include <opencv2/core.hpp>
#include <opencv2/core/cuda.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main(int argc, char** argv) {
    int w = argc > 1 ? atoi(argv[1]) : 16, xo = argc > 2 ? atoi(argv[2]) : 5, h = 8; const char* mk = argc > 3 ? argv[3] : "same";
    cv::Mat parent(h + 4, w + xo + 9, CV_8UC1, cv::Scalar(0)); cv::Mat roi = parent(cv::Rect(xo, 2, w, h)); cv::RNG rng(1); rng.fill(roi, cv::RNG::UNIFORM, 1, 256);
    cv::cuda::GpuMat gparent(parent); cv::cuda::GpuMat groi = gparent(cv::Rect(xo, 2, w, h));
    cv::Mat maskAll(h, w, CV_8UC1, cv::Scalar(255)); cv::cuda::GpuMat gmask, gmparent;
    if (!strcmp(mk, "same")) { cv::Mat mp(h + 4, w + xo + 9, CV_8UC1, cv::Scalar(255)); gmparent.upload(mp); gmask = gmparent(cv::Rect(xo, 2, w, h)); }
    else if (!strcmp(mk, "wide")) { cv::Mat mp(h + 4, 1000, CV_8UC1, cv::Scalar(255)); gmparent.upload(mp); gmask = gmparent(cv::Rect(xo, 2, w, h)); }
    else gmask.upload(maskAll);
    int ch[] = {0}, hs[] = {256}; float r[] = {0, 256}; const float* rr[] = {r}; cv::Mat hc; cv::calcHist(&roi, 1, ch, maskAll, hc, 1, hs, rr);
    printf("w=%d xoff=%d mask=%-6s src.step=%zu mask.step=%zu mask.data%%4=%zu : ", w, xo, mk, groi.step, gmask.step, (size_t)gmask.data % 4); fflush(stdout);
    try { cv::cuda::GpuMat gh; cv::cuda::calcHist(groi, gmask, gh); cv::Mat h1; gh.download(h1); cv::Mat hcf; hc.reshape(1, 1).convertTo(hcf, CV_32S);
          printf("cpu total=%.0f bin0=%.0f | cuda total=%.0f bin0=%d, bins differing %d\n", cv::sum(hc)[0], hc.at<float>(0), cv::sum(h1)[0], h1.at<int>(0), cv::countNonZero(hcf != h1.reshape(1, 1))); }
    catch (const cv::Exception& e) { std::string s = e.err; for (auto& c : s) if (c == '\n') c = ' '; printf("EXC %s\n", s.c_str()); return 1; }
    return 0;
}
w=16 xoff=0 mask=same   src.step=512 mask.step=512 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=1 mask=same   src.step=512 mask.step=512 mask.data%4=1 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=2 mask=same   src.step=512 mask.step=512 mask.data%4=2 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=3 mask=same   src.step=512 mask.step=512 mask.data%4=3 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=4 mask=same   src.step=512 mask.step=512 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=5 mask=same   src.step=512 mask.step=512 mask.data%4=1 : cpu total=128 bin0=0 | cuda total=136 bin0=8, bins differing 1
w=16 xoff=6 mask=same   src.step=512 mask.step=512 mask.data%4=2 : cpu total=128 bin0=0 | cuda total=144 bin0=16, bins differing 1
w=16 xoff=8 mask=same   src.step=512 mask.step=512 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=160 bin0=32, bins differing 1
w=16 xoff=0 mask=wide   src.step=512 mask.step=1024 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=1 mask=wide   src.step=512 mask.step=1024 mask.data%4=1 : cpu total=128 bin0=0 | cuda total=128 bin0=12, bins differing 45
w=16 xoff=2 mask=wide   src.step=512 mask.step=1024 mask.data%4=2 : cpu total=128 bin0=0 | cuda total=128 bin0=12, bins differing 46
w=16 xoff=3 mask=wide   src.step=512 mask.step=1024 mask.data%4=3 : cpu total=128 bin0=0 | cuda total=128 bin0=12, bins differing 47
w=16 xoff=4 mask=wide   src.step=512 mask.step=1024 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=16, bins differing 58
w=16 xoff=5 mask=wide   src.step=512 mask.step=1024 mask.data%4=1 : cpu total=128 bin0=0 | cuda total=136 bin0=20, bins differing 55
w=16 xoff=6 mask=wide   src.step=512 mask.step=1024 mask.data%4=2 : cpu total=128 bin0=0 | cuda total=144 bin0=24, bins differing 52
w=16 xoff=8 mask=wide   src.step=512 mask.step=1024 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=160 bin0=36, bins differing 58
w=16 xoff=0 mask=contig src.step=512 mask.step=512 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=1 mask=contig src.step=512 mask.step=512 mask.data%4=0 : EXC misaligned address
w=16 xoff=2 mask=contig src.step=512 mask.step=512 mask.data%4=0 : EXC misaligned address
w=16 xoff=3 mask=contig src.step=512 mask.step=512 mask.data%4=0 : EXC misaligned address
w=16 xoff=4 mask=contig src.step=512 mask.step=512 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0
w=16 xoff=5 mask=contig src.step=512 mask.step=512 mask.data%4=0 : EXC misaligned address
w=16 xoff=6 mask=contig src.step=512 mask.step=512 mask.data%4=0 : EXC misaligned address
w=16 xoff=8 mask=contig src.step=512 mask.step=512 mask.data%4=0 : cpu total=128 bin0=0 | cuda total=128 bin0=0, bins differing 0

Note: the investigation and this report were prepared with an AI assistant; all runs are from my own board and the output blocks are the programs' own output, pasted line for line.

Issue submission checklist
  • I report the issue, it's not a question
  • I checked the problem with documentation, FAQ, open issues, forum.opencv.org, Stack Overflow, etc and have not found any solution
  • I updated to the latest OpenCV version and the issue is still there
  • There is reproducer code and related data files: videos, images, onnx, etc