Repository: opencv
Issue: Sobel XandY-combining for performance
Labels: feature

### Describe the feature and motivation

Usually/often both Sobel X and Y are needed together (considered usecase here).
This applies to all Arch (e.g. ARM, x86, x64, OpenCL, CUDA, ..), if ImageSize > L1-Cache.
Principal idea is to go from image pipelining (finish each operator, before next one starts) to row-pipelining (each row enters L1 only once). General benefits: much faster (with typical L1 cache sizes) + less lines of code + less temporal memory.
Edge-detection is a major method in CV in general, and Sobel is taken here as example.

### Additional context

The Issue applies to both Python and C-Interface
This Interface Code should only explain the idea, it is not intended to be final.

# 1. the grayscale case (only 1 channel) -- [RGB/BGR see below]

    ddepth = cv2.CV_16S # no important now
    gray = cv2.imread(image, CV_LOAD_IMAGE_GRAYSCALE) # cv2.IMREAD_COLOR
    grad_x = cv2.Sobel(gray, ddepth, 1, 0, ksize=3, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)
    grad_y = cv2.Sobel(gray, ddepth, 0, 1, ksize=3, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)
    # a typical Image (8bit, 1..8 Megapixels) fits nowadays into L3-cache (some Megabytes), but not into L1-cache (32..100 KB)
    # if you calculate both Sobels successively, SobelY still works from L3
    # but if you would work in in parallel (e.g. Line1_gx, Line1_gy, Line2_gx, Line2_gy, ..) you would gain L1-speed for GradY
    grad_x,grad_y = SobelGradBoth(gray, ksize) # possible python syntax

# 2. grayscale cases, but combinations of Sobel X+Y

    # 2a) grad = abs(grad_x) + abs(grad_y)
    abs_grad_x = cv.convertScaleAbs(grad_x)
    abs_grad_y = cv.convertScaleAbs(grad_y)
    grad = cv.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
    grad8= cv.addWeighted(abs_grad_x, 0.25, abs_grad_y, 0.25, 0) # (in CV_8U) even better, as pixel-bandwidth limits CV
    # a single optimized call would merge 5 runs (2x Sobel, 2x convertScaleAbs, 1x addWeighted) into one (and gain L1-speed for 4 of them)
    grad = GradSumFromSobels(gray, ksize, OUT_FORMAT) # new possible syntax
    
    # 2b) thres_8U = threshold(dx^2 + dy^2, thresh*thresh, 255, cv.THRESH_BINARY)
    # same here, including extra 16 to 8 bit method (thres_8U is similar to Canny without NMS)
    
    # 2c) Magnitude^2 = (dx^2 + dy^2)
    mag2_16 = Magn2FromSobels(dx^2 + dy^2) # 16S or 16U
    mag_scl8U  = Magn8FromSobels(dx^2 + dy^2) / 512 # (255^2 + 255^2) /510 = 255, 512=2^9
    # same here = many operations together (6 vs 1)
    
    # 2d) mag = sqrt(dx^2 + dy^2) (using cv::magnitude, but caution sqrt is slow)
    mag_32F = Magn32F_FromSobels(dx^2 + dy^2)
    mag_8F  = Magn8F_FromSobels(dx^2 + dy^2) # Minifloat, less memory + faster sqrt
    # same here (and even more improvments thinkable by avoiding sqrt)
    # like: log2_mag = log2(dx^2 + dy^2) * 0.5 # log2( CV_16U ) = __builtin_clz() or LZCNT()
    # benefit: fast (no sqrt), similar information (rounded), output could be CV_8U (skip /2)
    # optional: direction bits (part of 8U byte), often direction information can be rough (strong rounded)
    # 2d1) 1 bit direction: bit = abs(dx) > abs(dy)  [e.g. bit array 7+1]
    # 2d2) 2 bit direction: bit1= sign(dx), bit2 = sign(dy) // a fast atan2 with maxerror=45deg
    
# 3. Color images, RGB/BGR (3 channel)

    # single direction, gain = 9 : 1
    sobel_sum_x = abs(grad_red_x) + abs(grad_green_x) + abs(grad_blue_x) = SobelRgbX(img,ksize) # out = 16U & 8U [scaled /1024]
    sobel_sum_y = abs(grad_red_y) + abs(grad_green_y) + abs(grad_blue_y) = SobelRgbY(img,ksize) # out = 16U & 8U [scaled /1024]
    # both direction, gain = 19 : 1  !!!!
    grad_rgb = sobel_sum_x + sobel_sum_y = SobelRgbXandY(img,ksize) # output = 16U & 8U [scaled /2048 or /(6.0*255) ]
    mag2_rgb = Magn2_FromSobelRgb(dx^2 + dy^2) # output = 16U
    magT_rgb = MagnT_FromSobelRgb(dx^2 + dy^2 > thresh*thresh) # THRESH_BINARY

Thanks for reading.