Search code examples
javaarraysopencvjava-native-interface

OpenCV's Java bindings Mat.get() gives weird results


I have a 8UC1 Mat image. I'm trying to convert it to Java 2D byte array with [x][y] coordinates. Here's what I have so far:

byte[][] arr = new byte[mat.cols()][mat.rows()];
for (int colIndex = 0; colIndex < mat.cols(); colIndex++) {
    mat.get(0, colIndex, arr[colIndex]);
}

However, that gives me totally scrambled results. For example a mat with .dump() such as this:

[255, 255, 255, 255, 255, 255, 255, 255, 255, 255;
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0;
 255, 255, 255, 255, 255, 255, 255, 255, 255, 255;
 255, 255, 255, 255, 255, 255, 255, 255, 255, 255;
 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]

gives me this (don't mind the -1, that's OK):

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 
-1 -1 -1 -1 -1 -1 -1 -1 -1 0 
-1 -1 -1 -1 -1 -1 -1 -1 0 0 
-1 -1 -1 -1 -1 -1 -1 0 0 0 
-1 -1 -1 -1 -1 -1 0 0 0 0 

Solution

  • It is about the layout of memory inside a Mat and the way Mat.get is used.
    The code in the question is trying to construct a bidimensional array where the first index indexes columns and the 2nd index indexes rows.
    And said code is calling Mat.get expecting to fill a column on each call.

    Memory layout.
    For a two dimensional matrix of a single channel, as it is the case, opencv stores its values by rows. And each row is padded to some appropriate alignment.
    For the sake of explanation let's assume here the 10 bytes occupied by a row are padded to a total of 16 bytes.
    Our matrix would be stored like this :

    Position 0  : 255,255,255,255,255,255,255,255,255,255,?,?,?,?,?,?,  
    Position 16 :   0,  0,  0,  0,  0,  0,  0,  0,   0, 0,?,?,?,?,?,?,   
    Position 32 : 255,255,255,255,255,255,255,255,255,255,?,?,?,?,?,?,  
    Position 48 : 255,255,255,255,255,255,255,255,255,255,?,?,?,?,?,?,  
    Position 64 : 255,255,255,255,255,255,255,255,255,255,?,?,?,?,?,?,  
    

    With position being relative to the position of element at (0,0). From that diagram it should be clear that the last element, the one at (4,9), is at position 73.
    ? means an unused value. So we can't make any guarantees about the value held there.
    It is also possible for the Mat to be using a padding of size 0 and in the tests I did of that code that was the case. The layout then is like this (layout 2 )

    Position 0  : 255,255,255,255,255,255,255,255,255,255
    Position 10 :   0,  0,  0,  0,  0,  0,  0,  0,   0, 0
    Position 20 : 255,255,255,255,255,255,255,255,255,255
    Position 30 : 255,255,255,255,255,255,255,255,255,255
    Position 40 : 255,255,255,255,255,255,255,255,255,255
    

    Mat.get
    This is actually speculation because documentation is incomplete. I'll have to check sources to be sure.
    Mat.get(int i, int j, byte[] arr) fills the array arr with the values stored starting at the one at (i,j) and until the array is filled.
    So, what happens when we call mat.get(0, colIndex, arr[colIndex]) with colIndex having a value of 9?
    In the previous table, the element at (0,9) is at Position 9. arr[colIndex] is an array of size 5, so it is going to be filled with the values from position 9 till 13, which are : [ 255, ?, ?, ?, ? ] with layout 1 and are [255, 0, 0, 0, 0] with layout 2. Which is why the last colum of the output of OP's program is [255, 0, 0, 0, 0]. Notice that in layout 2 when you read the last element of a row in row order and want more elements the next one will be the 1st element of the following row.
    arr[9] is supposed to hold a column, the 10th column. But the call to Mat.get does not fill it with such column but with the value at (0,9) and the ones which follow in row order (not column order), with those values being unspecified because of padding (layout 1) or being the ones from the next row (layout 2).

    Ugly solution that keeps 1st index as columns and 2nd as rows :

    class SimpleSample
    {
    
        static
        {
            System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        }
    
        public static void printArr( byte[][] arr )
        {
            for ( int y=0; y<arr[0].length; ++y )
            {
                for ( int x=0; x<arr.length; ++x )
                    System.out.print( arr[x][y] + ", " );
                System.out.println("");
            }
    
        }
    
        public static void main(String[] args)
        {
            Mat mat = new Mat(5, 10, CvType.CV_8UC1, new Scalar(255));
            mat.row(1).setTo(new Scalar(0));
            System.out.println( mat.dump() );
            byte[][] arr = new byte[mat.cols()][mat.rows()];
    
            byte[] tmp = new byte[1];
            for ( int i=0; i<mat.rows(); ++i )
            {
                for ( int j=0; j<mat.cols(); ++j )
                {
                    mat.get(i, j, tmp);
                    arr[j][i] = tmp[0];
                }
            }
            printArr(arr);
        }
    
    }
    

    We get the expected output :

    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255;
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255;
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255;
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,

    Solution which changes 1st index of arr to rows and 2nd index to columns.

    class SimpleSample
    {
    
        static
        {
            System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        }
    
        public static void printArr( byte[][] arr )
        {
            for ( int y=0; y<arr.length; ++y )
            {
                for ( int x=0; x<arr[y].length; ++x )
                    System.out.print( arr[y][x] + ", " ); // Was arr[x][y] in prev code.
                System.out.println("");
            }
    
        }
    
        public static void main(String[] args)
        {
            Mat mat = new Mat(5, 10, CvType.CV_8UC1, new Scalar(255));
            mat.row(1).setTo(new Scalar(0));
            System.out.println( mat.dump() );
            byte[][] arr = new byte[mat.rows()][mat.cols()];
    
            for ( int i=0; i<mat.rows(); ++i )
                    mat.get(i, 0, arr[i]);
            printArr(arr);
        }
    }
    

    We get the same output.
    Notice though that here we have to use arr as arr[y][x] (1st rows, 2nd columns as in opencv) which is probably not what the OP wants.