web-dev-qa-db-fra.com

Interprétation YUV_420_888 sur Samsung Galaxy S7 (Camera2)

J'ai écrit une conversion de YUV_420_888 en Bitmap, en tenant compte de la logique suivante (si je comprends bien):

enter image description here

Pour résumer l'approche: les coordonnées x et y du noyau sont congruentes à la fois avec les x et y de la partie non rembourrée du plan Y (allocation 2d) et les x et y du bitmap de sortie. Les plans U et V, cependant, ont une structure différente de celle du plan Y, car ils utilisent 1 octet pour une couverture de 4 pixels et, en outre, peuvent avoir un PixelStride qui en est plus d'un, en plus ils pourraient ont également un rembourrage qui peut être différent de celui du plan Y. Par conséquent, afin d'accéder efficacement aux U et V par le noyau, je les ai mis dans des allocations 1-d et créé un index "uvIndex" qui donne la position des U et V correspondants dans cette allocation 1-d, pour acquis ( coordonnées x, y) dans le plan Y (non rembourré) (et, par conséquent, le bitmap de sortie).

Afin de garder le rs-Kernel maigre, j'ai exclu la zone de remplissage dans le yPlane en plafonnant la plage x via LaunchOptions (cela reflète le RowStride du plan y qui peut donc être ignoré DANS le noyau). Il nous suffit donc de prendre en compte les uvPixelStride et uvRowStride dans le uvIndex, c'est-à-dire l'indice utilisé pour accéder aux valeurs u et v.

Voici mon code:

Renderscript Kernel, nommé yuv420888.rs

  #pragma version(1)
  #pragma rs Java_package_name(com.xxxyyy.testcamera2);
  #pragma rs_fp_relaxed

  int32_t width;
  int32_t height;

  uint picWidth, uvPixelStride, uvRowStride ;
  rs_allocation ypsIn,uIn,vIn;

 // The LaunchOptions ensure that the Kernel does not enter the padding  zone of Y, so yRowStride can be ignored WITHIN the Kernel.
 uchar4 __attribute__((kernel)) doConvert(uint32_t x, uint32_t y) {

 // index for accessing the uIn's and vIn's
uint uvIndex=  uvPixelStride * (x/2) + uvRowStride*(y/2);

// get the y,u,v values
uchar yps= rsGetElementAt_uchar(ypsIn, x, y);
uchar u= rsGetElementAt_uchar(uIn, uvIndex);
uchar v= rsGetElementAt_uchar(vIn, uvIndex);

// calc argb
int4 argb;
    argb.r = yps + v * 1436 / 1024 - 179;
    argb.g =  yps -u * 46549 / 131072 + 44 -v * 93604 / 131072 + 91;
    argb.b = yps +u * 1814 / 1024 - 227;
    argb.a = 255;

uchar4 out = convert_uchar4(clamp(argb, 0, 255));
return out;
}

Côté Java:

    private Bitmap YUV_420_888_toRGB(Image image, int width, int height){
    // Get the three image planes
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer buffer = planes[0].getBuffer();
    byte[] y = new byte[buffer.remaining()];
    buffer.get(y);

    buffer = planes[1].getBuffer();
    byte[] u = new byte[buffer.remaining()];
    buffer.get(u);

    buffer = planes[2].getBuffer();
    byte[] v = new byte[buffer.remaining()];
    buffer.get(v);

    // get the relevant RowStrides and PixelStrides
    // (we know from documentation that PixelStride is 1 for y)
    int yRowStride= planes[0].getRowStride();
    int uvRowStride= planes[1].getRowStride();  // we know from   documentation that RowStride is the same for u and v.
    int uvPixelStride= planes[1].getPixelStride();  // we know from   documentation that PixelStride is the same for u and v.


    // rs creation just for demo. Create rs just once in onCreate and use it again.
    RenderScript rs = RenderScript.create(this);
    //RenderScript rs = MainActivity.rs;
    ScriptC_yuv420888 mYuv420=new ScriptC_yuv420888 (rs);

    // Y,U,V are defined as global allocations, the out-Allocation is the Bitmap.
    // Note also that uAlloc and vAlloc are 1-dimensional while yAlloc is 2-dimensional.
    Type.Builder typeUcharY = new Type.Builder(rs, Element.U8(rs));
    typeUcharY.setX(yRowStride).setY(height);
    Allocation yAlloc = Allocation.createTyped(rs, typeUcharY.create());
    yAlloc.copyFrom(y);
    mYuv420.set_ypsIn(yAlloc);

    Type.Builder typeUcharUV = new Type.Builder(rs, Element.U8(rs));
    // note that the size of the u's and v's are as follows:
    //      (  (width/2)*PixelStride + padding  ) * (height/2)
    // =    (RowStride                          ) * (height/2)
    // but I noted that on the S7 it is 1 less...
    typeUcharUV.setX(u.length);
    Allocation uAlloc = Allocation.createTyped(rs, typeUcharUV.create());
    uAlloc.copyFrom(u);
    mYuv420.set_uIn(uAlloc);

    Allocation vAlloc = Allocation.createTyped(rs, typeUcharUV.create());
    vAlloc.copyFrom(v);
    mYuv420.set_vIn(vAlloc);

    // handover parameters
    mYuv420.set_picWidth(width);
    mYuv420.set_uvRowStride (uvRowStride);
    mYuv420.set_uvPixelStride (uvPixelStride);

    Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Allocation outAlloc = Allocation.createFromBitmap(rs, outBitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);

    Script.LaunchOptions lo = new Script.LaunchOptions();
    lo.setX(0, width);  // by this we ignore the y’s padding zone, i.e. the right side of x between width and yRowStride
    lo.setY(0, height);

    mYuv420.forEach_doConvert(outAlloc,lo);
    outAlloc.copyTo(outBitmap);

    return outBitmap;
}

Test sur Nexus 7 (API 22), cela renvoie des bitmaps de couleur agréable. Cet appareil, cependant, a des pas de pixels triviaux (= 1) et pas de remplissage (c'est-à-dire rowstride = largeur). Test sur le tout nouveau Samsung S7 (API 23), j'obtiens des photos dont les couleurs ne sont pas correctes - à l'exception des vertes. Mais l'image ne montre pas un biais général vers le vert, il semble juste que les couleurs non vertes ne soient pas reproduites correctement. Notez que le S7 applique un pas de pixel u/v de 2 et aucun remplissage.

Étant donné que la ligne de code la plus cruciale se trouve dans le code rs, l'accès aux plans u/v uint uvIndex = (...) Je pense qu'il pourrait y avoir un problème, probablement avec une mauvaise prise en compte des pas de pixels ici. Quelqu'un voit-il la solution? Merci.

MISE À JOUR: J'ai tout vérifié, et je suis assez sûr que le code concernant l'accès de y, u, v est correct. Le problème doit donc être avec les valeurs u et v elles-mêmes. Les couleurs non vertes ont une inclinaison violette, et en regardant les valeurs u, v, elles semblent être dans une plage plutôt étroite d'environ 110-150. Est-il vraiment possible que nous devions faire face à des conversions YUV -> RBG spécifiques à l'appareil ...?! Ai-je manqué quelque chose?

MISE À JOUR 2: avoir corrigé le code, cela fonctionne maintenant, grâce aux commentaires d'Eddy.

26
Settembrini

Regarder

floor((float) uvPixelStride*(x)/2)

qui calcule votre décalage de ligne U, V (uv_row_offset) à partir de la coordonnée Y x.

si uvPixelStride = 2, alors à mesure que x augmente:

x = 0, uv_row_offset = 0
x = 1, uv_row_offset = 1
x = 2, uv_row_offset = 2
x = 3, uv_row_offset = 3

et c'est incorrect. Il n'y a pas de valeur de pixel U/V valide à uv_row_offset = 1 ou 3, car uvPixelStride = 2.

Tu veux

uvPixelStride * floor(x/2)

(en supposant que vous ne vous fiez pas à vous souvenir du comportement critique d'arrondi de la division entière, si vous le faites alors):

uvPixelStride * (x/2)

cela devrait suffire

Avec cela, votre cartographie devient:

x = 0, uv_row_offset = 0
x = 1, uv_row_offset = 0
x = 2, uv_row_offset = 2
x = 3, uv_row_offset = 2

Voyez si cela corrige les erreurs de couleur. Dans la pratique, un adressage incorrect signifierait que tous les autres échantillons de couleur proviennent du mauvais plan de couleur, car il est probable que les données YUV sous-jacentes soient semi-planes (donc le plan U commence au plan V + 1 octet, les deux plans étant entrelacés)

7
Eddy Talvala

Pour les personnes qui rencontrent des erreurs

Android.support.v8.renderscript.RSIllegalArgumentException: Array too small for allocation type

utilisez buffer.capacity() au lieu de buffer.remaining()

et si vous avez déjà effectué quelques opérations sur l'image, vous devrez appeler la méthode rewind() sur le tampon.

5
Lolo

De plus, pour toute autre personne

Android.support.v8.renderscript.RSIllegalArgumentException: tableau trop petit pour le type d'allocation

Je l'ai corrigé en changeant yAlloc.copyFrom(y); en yAlloc.copy1DRangeFrom(0, y.length, y);

4
user2486946

Ce code nécessite l'utilisation de la bibliothèque de compatibilité RenderScript (Android.support.v8.renderscript. *).

Afin de faire fonctionner la bibliothèque de compatibilité avec Android API 23, j'ai mis à jour vers gradle-plugin 2.1.0 et Build-Tools 23.0.3 selon la réponse de Miao Wang à Comment créer des scripts Renderscript sur Android Studio, et les faire fonctionner?

Si vous suivez sa réponse et que le message d'erreur "Gradle version 2.10 is required" s'affiche, ne changez PAS

classpath 'com.Android.tools.build:gradle:2.1.0'

À la place, mettez à jour le champ distributionUrl du fichier Project\gradle\wrapper\gradle-wrapper.properties vers

distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.Zip

et changez Fichier> Paramètres> Builds, Execution, Deployment> Build Tools> Gradle> Gradle to Use default gradle wrapper selon "Gradle Version 2.10 est requis." Erreur .

1
Nick

Sur une Samsung Galaxy Tab 5 (tablette), Android version 5.1.1 (22), avec le prétendu format YUV_420_888, les calculs de rendu suivants fonctionnent bien et produisent des couleurs correctes:

uchar yValue    = rsGetElementAt_uchar(gCurrentFrame, x + y * yRowStride);
uchar vValue    = rsGetElementAt_uchar(gCurrentFrame, ( (x/2) + (y/4) * yRowStride ) + (xSize * ySize) );
uchar uValue    = rsGetElementAt_uchar(gCurrentFrame, ( (x/2) + (y/4) * yRowStride ) + (xSize * ySize) + (xSize * ySize) / 4);

Je ne comprends pas pourquoi la valeur horizontale (c'est-à-dire y) est mise à l'échelle par un facteur de quatre au lieu de deux, mais cela fonctionne bien. J'ai également dû éviter d'utiliser rsGetElementAtYuv_uchar_Y | U | V. Je crois que la valeur de foulée d'allocation associée est définie sur zéro au lieu de quelque chose de correct. L'utilisation de rsGetElementAt_uchar () est une solution de contournement raisonnable.

Sur un Samsung Galaxy S5 (téléphone intelligent), Android version 5.0 (21), avec le prétendu format YUV_420_888, je ne peux pas récupérer les valeurs u et v, elles apparaissent comme tous des zéros. Cela se traduit par une image d'aspect vert. La luminosité est correcte, mais l'image est inversée verticalement.

1
Steven Punte

Publier une solution complète pour convertir YUV-> BGR (peut également être adopté pour d'autres formats) et faire pivoter l'image en position verticale à l'aide de renderscript. L'allocation est utilisée comme entrée et le tableau d'octets est utilisé comme sortie. Il a été testé sur Android 8+, y compris les appareils Samsung également.

Java

/**
 * Renderscript-based process to convert YUV_420_888 to BGR_888 and rotation to upright.
 */
public class ImageProcessor {

    protected final String TAG = this.getClass().getSimpleName();

    private Allocation mInputAllocation;
    private Allocation mOutAllocLand;
    private Allocation mOutAllocPort;

    private Handler mProcessingHandler;
    private ScriptC_yuv_bgr mConvertScript;
    private byte[] frameBGR;

    public ProcessingTask mTask;
    private ImageListener listener;
    private Supplier<Integer> rotation;

    public ImageProcessor(RenderScript rs, Size dimensions, ImageListener listener, Supplier<Integer> rotation) {

        this.listener = listener;
        this.rotation = rotation;
        int w = dimensions.getWidth();
        int h = dimensions.getHeight();

        Type.Builder yuvTypeBuilder = new Type.Builder(rs, Element.YUV(rs));
        yuvTypeBuilder.setX(w);
        yuvTypeBuilder.setY(h);
        yuvTypeBuilder.setYuvFormat(ImageFormat.YUV_420_888);
        mInputAllocation = Allocation.createTyped(rs, yuvTypeBuilder.create(),
                Allocation.USAGE_IO_INPUT | Allocation.USAGE_SCRIPT);

        //keep 2 allocations to handle different image rotations
        mOutAllocLand = createOutBGRAlloc(rs, w, h);
        mOutAllocPort = createOutBGRAlloc(rs, h, w);

        frameBGR = new byte[w*h*3];

        HandlerThread processingThread = new HandlerThread(this.getClass().getSimpleName());
        processingThread.start();
        mProcessingHandler = new Handler(processingThread.getLooper());

        mConvertScript = new ScriptC_yuv_bgr(rs);
        mConvertScript.set_inWidth(w);
        mConvertScript.set_inHeight(h);

        mTask = new ProcessingTask(mInputAllocation);
    }

    private Allocation createOutBGRAlloc(RenderScript rs, int width, int height) {
        //Stored as Vec4, it's impossible to store as Vec3, buffer size will be for Vec4 anyway
        //using RGB_888 as alternative for BGR_888, can be just U8_3 type
        Type.Builder rgbTypeBuilderPort = new Type.Builder(rs, Element.RGB_888(rs));
        rgbTypeBuilderPort.setX(width);
        rgbTypeBuilderPort.setY(height);
        Allocation allocation = Allocation.createTyped(
            rs, rgbTypeBuilderPort.create(), Allocation.USAGE_SCRIPT
        );
        //Use auto-padding to be able to copy to x*h*3 bytes array
        allocation.setAutoPadding(true);

        return allocation;
    }

    public Surface getInputSurface() {
        return mInputAllocation.getSurface();
    }

    /**
     * Simple class to keep track of incoming frame count,
     * and to process the newest one in the processing thread
     */
    class ProcessingTask implements Runnable, Allocation.OnBufferAvailableListener {

        private int mPendingFrames = 0;

        private Allocation mInputAllocation;

        public ProcessingTask(Allocation input) {
            mInputAllocation = input;
            mInputAllocation.setOnBufferAvailableListener(this);
        }

        @Override
        public void onBufferAvailable(Allocation a) {
            synchronized(this) {
                mPendingFrames++;
                mProcessingHandler.post(this);
            }
        }

        @Override
        public void run() {
            // Find out how many frames have arrived
            int pendingFrames;
            synchronized(this) {
                pendingFrames = mPendingFrames;
                mPendingFrames = 0;

                // Discard extra messages in case processing is slower than frame rate
                mProcessingHandler.removeCallbacks(this);
            }

            // Get to newest input
            for (int i = 0; i < pendingFrames; i++) {
                mInputAllocation.ioReceive();
            }

            int rot = rotation.get();

            mConvertScript.set_currentYUVFrame(mInputAllocation);
            mConvertScript.set_rotation(rot);

            Allocation allocOut = rot==90 || rot== 270 ? mOutAllocPort : mOutAllocLand;

            // Run processing
            // ain allocation isn't really used, global frame param is used to get data from
            mConvertScript.forEach_yuv_bgr(allocOut);

            //Save to byte array, BGR 24bit
            allocOut.copyTo(frameBGR);

            int w = allocOut.getType().getX();
            int h = allocOut.getType().getY();

            if (listener != null) {
                listener.onImageAvailable(frameBGR, w, h);
            }
        }
    }

    public interface ImageListener {

        /**
         * Called when there is available image, image is in upright position.
         *
         * @param bgr BGR 24bit bytes
         * @param width image width
         * @param height image height
         */
        void onImageAvailable(byte[] bgr, int width, int height);
    }
}

RS

#pragma version(1)
#pragma rs Java_package_name(com.affectiva.camera)
#pragma rs_fp_relaxed

//Script convers YUV to BGR(uchar3)

//current YUV frame to read pixels from
rs_allocation currentYUVFrame;

//input image rotation: 0,90,180,270 clockwise
uint32_t rotation;
uint32_t inWidth;
uint32_t inHeight;

//method returns uchar3  BGR which will be set to x,y in output allocation
uchar3 __attribute__((kernel)) yuv_bgr(uint32_t x, uint32_t y) {

    // Read in pixel values from latest frame - YUV color space

    uchar3 inPixel;
    uint32_t xRot = x;
    uint32_t yRot = y;

    //Do not rotate if 0
    if (rotation==90) {
      //rotate 270 clockwise
      xRot = y;
      yRot = inHeight - 1 - x;
    } else if (rotation==180) {
      xRot = inWidth - 1 - x;
      yRot = inHeight - 1 - y;
    } else if (rotation==270) {
      //rotate 90 clockwise
      xRot = inWidth - 1 - y;
      yRot = x;
    }

    inPixel.r = rsGetElementAtYuv_uchar_Y(currentYUVFrame, xRot, yRot);
    inPixel.g = rsGetElementAtYuv_uchar_U(currentYUVFrame, xRot, yRot);
    inPixel.b = rsGetElementAtYuv_uchar_V(currentYUVFrame, xRot, yRot);

    // Convert YUV to RGB, JFIF transform with fixed-point math
    // R = Y + 1.402 * (V - 128)
    // G = Y - 0.34414 * (U - 128) - 0.71414 * (V - 128)
    // B = Y + 1.772 * (U - 128)

    int3 bgr;
    //get red pixel and assing to b
    bgr.b = inPixel.r +
            inPixel.b * 1436 / 1024 - 179;
    bgr.g = inPixel.r -
            inPixel.g * 46549 / 131072 + 44 -
            inPixel.b * 93604 / 131072 + 91;
    //get blue pixel and assign to red
    bgr.r = inPixel.r +
            inPixel.g * 1814 / 1024 - 227;

    // Write out
    return convert_uchar3(clamp(bgr, 0, 255));
}
0
Andrei .F

Re: RSIllegalArgumentException

Dans mon cas, c'était le cas que buffer.remaining () n'était pas multiple de foulée: la longueur de la dernière ligne était inférieure à foulée (c'est-à-dire uniquement jusqu'à l'emplacement des données réelles).

0
Takashi Matsuzawa