web-dev-qa-db-fra.com

MediaCodec et Camera: les espaces de couleurs ne correspondent pas

J'essayais de faire fonctionner l'encodage H264 avec les entrées capturées par l'appareil photo sur une tablette Android à l'aide du nouveau MediaCodec de bas niveau. J'ai eu quelques difficultés avec cela, car le MediaCodecAPI est mal documenté, mais j'ai enfin quelque chose à travailler.

Je configure la caméra comme suit:

        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setPreviewFormat(ImageFormat.YV12); // <1>
        parameters.setPreviewFpsRange(4000,60000);
        parameters.setPreviewSize(640, 480);            
        mCamera.setParameters(parameters);

Pour la partie encodage, j'instancie l'objet MediaCodec comme suit:

    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 640, 480);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500000);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar); // <2>
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();

L’objectif final est de créer un flux RTP (et de correspondre à Skype), mais jusqu’à présent, je ne fais que diffuser le fichier brut H264 directement sur mon bureau. J'utilise le pipeline GStreamer suivant pour afficher le résultat:

gst-launch udpsrc port=5555 ! video/x-h264,width=640,height=480,framerate=15/1 ! ffdec_h264 ! autovideosink

Tout fonctionne bien, sauf pour les couleurs. Je dois définir 2 formats de couleur sur l'ordinateur: un pour l'aperçu de l'appareil photo (ligne étiquetée avec <1>) et un pour l'objet MediaCodec (étiqueté avec <2>)

Pour déterminer les valeurs acceptables pour les lignes <1>, j’ai utilisé parameters.getSupportedPreviewFormats(). De ce fait, je sais que les seuls formats pris en charge sur l'appareil photo sont ImageFormat.NV21 et ImageFormat.YV2 .

Pour <2>, j'ai récupéré l'objet MediaCodecInfo.CodecCapabilities de type video/avc, correspondant aux valeurs entières 19 (correspondant à MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar } et _ ne correspond à aucune valeur de MediaCodecInfo.CodecCapabilities ). 

Toute valeur autre que celle indiquée ci-dessus entraîne un blocage.

La combinaison de ces paramètres donne des résultats différents, que je montrerai ci-dessous. Voici la capture d'écran sur Android (c'est-à-dire les "vraies" couleurs): Input on Android-tablet Voici les résultats montrés par Gstreamer:

<1> = NV21, <2> = COLOR_FormatYUV420Planar Gstreamer-output for NV21-COLOR_FormatYUV420Planar

<1> = NV21, <2> = 2130708361 Gstreamer-output for NV21-2130708361

<1> = YV2, <2> = COLOR_FormatYUV420Planar Gstreamer-output for YV2-COLOR_FormatYUV420Planar

<1> = YV2, <2> = 2130708361 Gstreamer-output for YV2-2130708361

Comme on peut le constater, aucun d’entre eux n’est satisfaisant. L'espace de couleurs YV2 semble le plus prometteur, mais il semble que le rouge (Cr) et le bleu (Cb) soient inversés. Le NV21 semble entrelacé je suppose (cependant, je ne suis pas expert dans ce domaine).

Puisque le but est de communiquer avec Skype, je suppose que je ne devrais pas changer le décodeur (c'est-à-dire la commande Gstreamer), n'est-ce pas? Est-ce à résoudre dans Android et si oui: comment? Ou cela peut-il être résolu en ajoutant certaines informations de charge utile RTP? Une autre suggestion?

31
gleerman

Je l'ai résolu en échangeant moi-même les byteplanes au niveau Android, en utilisant une simple fonction:

public byte[] swapYV12toI420(byte[] yv12bytes, int width, int height) {
    byte[] i420bytes = new byte[yv12bytes.length];
    for (int i = 0; i < width*height; i++)
        i420bytes[i] = yv12bytes[i];
    for (int i = width*height; i < width*height + (width/2*height/2); i++)
        i420bytes[i] = yv12bytes[i + (width/2*height/2)];
    for (int i = width*height + (width/2*height/2); i < width*height + 2*(width/2*height/2); i++)
        i420bytes[i] = yv12bytes[i - (width/2*height/2)];
    return i420bytes;
}
7
gleerman

Je pense qu'il est plus efficace d'échanger les valeurs en place.

        int wh4 = input.length/6; //wh4 = width*height/4
        byte tmp;
        for (int i=wh4*4; i<wh4*5; i++)
            {
            tmp = input[i];
            input[i] = input[i+wh4];
            input[i+wh4] = tmp;
            }

Peut-être même mieux, vous pouvez remplacer

            inputBuffer.put(input);

Avec les 3 tranches planes dans le bon ordre

            inputBuffer.put(input, 0, wh4*4);
            inputBuffer.put(input, wh4*5, wh4);
            inputBuffer.put(input, wh4*4, wh4);

Je pense que cela ne devrait avoir qu'une infime surcharge

6
John La Rooy

Il semblerait qu'Android transmette en YV12, mais le format défini dans les en-têtes H264 est YUV420. Ces formats sont égaux sauf que les canaux U et V sont dans un ordre différent, ce qui explique l’échange de rouge et de bleu.

Le mieux serait bien sûr de régler le paramètre du côté Android. Mais s'il n'y a aucun moyen de définir des paramètres compatibles pour la caméra et l'encodeur, vous devrez forcer le format du côté GStreamer.

Cela peut être fait en ajoutant un élément capssetter après le ffdec_h264

... ! ffdec_h264 ! capssetter caps="video/x-raw-yuv, format=(fourcc)YV12" ! colorspace ! ...

3
jpa

Avec ImageFormat.NV21 défini sur la caméra et COLOR_FormatYUV420Planar pour le codeur, une ombre bleue semblable se chevauche dans mon cas. Si j'ai bien compris, la fonction d'échange ci-dessus ne peut pas être utilisée dans mon cas, des suggestions sur un algorithme pouvant être utilisé à cette fin? ps: C’est un écran noir complet au décodeur lorsque le format de prévisualisation de la caméra est défini sur YV12

0
Lakshmi