

# Uso del reemplazo de fondo con el SDK de transmisión de IVS
Reemplazo de fondo

El reemplazo del fondo es un tipo de filtro de cámara que permite a los creadores de transmisiones en directo cambiar sus fondos. Como se muestra en el diagrama siguiente, reemplazar el fondo implica:

1. Obtener una imagen de cámara a partir de la transmisión de la cámara en directo.

1. Segmentarla en componentes de primer plano y segundo plano con el ML Kit de Google.

1. Combinar la máscara de segmentación resultante con una imagen de fondo personalizada.

1. Pasarla a una fuente de imagen personalizada para su transmisión.

![\[Flujo de trabajo para implementar el reemplazo del fondo.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Background_Replacement.png)


## Web


En esta sección se presupone que ya está familiarizado con la [publicación de videos y la suscripción a ellos mediante el SDK de transmisión web](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-pub-sub-web.html).

Para sustituir el fondo de una transmisión en directo por una imagen personalizada, utilice el [modelo de segmentación de selfies](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model) con [MediaPipe Image Segmenter](https://developers.google.com/mediapipe/solutions/vision/image_segmenter). Se trata de un modelo de machine learning que identifica qué píxeles del fotograma de video están en primer plano o en segundo plano. Puede utilizar los resultados del modelo para sustituir el fondo de una transmisión en directo copiando los píxeles de primer plano de la transmisión de video a una imagen personalizada que represente el nuevo fondo.

Para integrar el reemplazo del fondo de pantalla con el SDK de transmisión web en tiempo real de IVS, debe:

1. Instalar MediaPipe y Webpack. (Nuestro ejemplo usa Webpack como paquete, pero puede usar cualquier paquete de su elección).

1. Cree `index.html`.

1. Agregue elementos multimedia.

1. Añada una etiqueta de script.

1. Cree `app.js`.

1. Cargue una imagen de fondo personalizada.

1. Cree una instancia de `ImageSegmenter`.

1. Renderice la transmisión de video en un lienzo.

1. Cree una lógica de reemplazo de fondo.

1. Cree el archivo de configuración de Webpack.

1. Agrupe su archivo JavaScript.

### Instalación de MediaPipe y Webpack


Para empezar, instale `@mediapipe/tasks-vision` y los paquetes npm de `webpack`. El siguiente ejemplo usa Webpack como un empaquetador de JavaScript; puede usar un empaquetador diferente si lo prefiere.

#### JavaScript


```
npm i @mediapipe/tasks-vision webpack webpack-cli
```

Asegúrese de actualizar también su `package.json` para especificar `webpack` al desarrollar script:

#### JavaScript


```
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
```

### Creación del archivo index.html


A continuación, cree la plantilla HTML e importe el SDK de transmisión web como una etiqueta script. En el código siguiente, asegúrese de reemplazar `<SDK version>` por la versión del SDK de transmisión que esté utilizando.

#### JavaScript


```
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- Import the SDK -->
  <script src="https://web-broadcast.live-video.net/<SDK version>/amazon-ivs-web-broadcast.js"></script>
</head>

<body>

</body>
</html>
```

### Agregue elementos multimedia


A continuación, agregue un elemento de video y dos elementos de lienzo dentro de la etiqueta corporal. El elemento de video contendrá las imágenes de la cámara en directo y se utilizará como entrada para el segmentador de imágenes de MediaPipe. El primer elemento de lienzo se utilizará para obtener una vista previa de la transmisión que se emitirá. El segundo elemento de lienzo se utilizará para renderizar la imagen personalizada que se utilizará como fondo. Como el segundo lienzo con la imagen personalizada solo se usa como fuente para copiar píxeles mediante programación al lienzo final, está oculto a la vista.

#### JavaScript


```
<div class="row local-container">
      <video id="webcam" autoplay style="display: none"></video>
    </div>
    <div class="row local-container">
      <canvas id="canvas" width="640px" height="480px"></canvas>

      <div class="column" id="local-media"></div>
      <div class="static-controls hidden" id="local-controls">
        <button class="button" id="mic-control">Mute Mic</button>
        <button class="button" id="camera-control">Mute Camera</button>
      </div>
    </div>
    <div class="row local-container">
      <canvas id="background" width="640px" height="480px" style="display: none"></canvas>
    </div>
```

### Agregue una etiqueta de script


Agregue una etiqueta de script para cargar un archivo JavaScript agrupado que contendrá el código para reemplazar el fondo y publicarlo en un escenario:

```
<script src="./dist/bundle.js"></script>
```

### Creación de app.js


A continuación, cree un archivo JavaScript para obtener los objetos de elemento para los elementos de lienzo y video que se crearon en la página HTML. Importe los módulos `ImageSegmenter` y `FilesetResolver`. El módulo `ImageSegmenter` se utilizará para realizar la tarea de segmentación.

#### JavaScript


```
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";
```

A continuación, cree una función llamada `init()` para recuperar el MediaStream de la cámara del usuario e invoque una función de devolución de llamada cada vez que termine de cargarse el fotograma de la cámara. Añada detectores de eventos para que los botones se unan y salgan de un escenario.

Tenga en cuenta que cuando nos unimos a un escenario, pasamos una variable llamada`segmentationStream`. Se trata de una transmisión de video capturada desde un elemento de lienzo, que contiene una imagen de primer plano superpuesta a la imagen personalizada que representa el fondo. Más adelante, esta transmisión personalizada se utilizará para crear una instancia de `LocalStageStream`, que se podrá publicar en un escenario.

#### JavaScript


```
const init = async () => {
  await initializeDeviceSelect();

  cameraButton.addEventListener("click", () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera";
  });

  micButton.addEventListener("click", () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic";
  });

  localCamera = await getCamera(videoDevicesList.value);
  const segmentationStream = canvasElement.captureStream();

  joinButton.addEventListener("click", () => {
    joinStage(segmentationStream);
  });

  leaveButton.addEventListener("click", () => {
    leaveStage();
  });
};
```

### Carga de una imagen de fondo personalizada


En la parte inferior de la función `init`, añada código para llamar a una función llamada`initBackgroundCanvas`, que carga una imagen personalizada de un archivo local y la renderiza en un lienzo. Definiremos esta función en el siguiente paso. Asigne la `MediaStream` recuperada de la cámara del usuario al objeto de video. Más tarde, este objeto de video se pasará al segmentador de imágenes. Además, configure una función denominada `renderVideoToCanvas` como la función de devolución de llamada para que se invoque cada vez que un fotograma de video termine de cargarse. Definiremos esta función en un paso posterior.

#### JavaScript


```
initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
```

Vamos a implementar la función `initBackgroundCanvas`, que carga una imagen desde un archivo local. En este ejemplo, utilizaremos una imagen de una playa como fondo personalizado. El lienzo que contiene la imagen personalizada se ocultará de la pantalla, ya que lo combinará con los píxeles del primer plano del elemento del lienzo que contiene la imagen de la cámara.

#### JavaScript


```
const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};
```

### Creación de una instancia de ImageSegmenter


A continuación, cree una instancia de `ImageSegmenter`, que segmentará la imagen y devolverá el resultado en forma de máscara. Al crear una instancia de `ImageSegmenter`, utilizará el [modelo de segmentación de selfies](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model).

#### JavaScript


```
const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};
```

### Renderice la transmisión de video en un lienzo


A continuación, cree la función que renderice la transmisión de video al otro elemento del lienzo. Necesitamos renderizar la transmisión de video en un lienzo para poder extraer los píxeles del primer plano utilizando la API Canvas 2D. Mientras lo hacemos, también pasaremos un fotograma de video a nuestra instancia`ImageSegmenter`, utilizando el método [segmentForVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) para segmentar el primer plano del fondo del fotograma de video. Cuando el método [segmentForVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) regrese, invoque nuestra función de devolución de llamada personalizada, `replaceBackground`, para reemplazar el fondo.

#### JavaScript


```
const renderVideoToCanvas = async () => {
  if (video.currentTime === lastWebcamTime) {
    window.requestAnimationFrame(renderVideoToCanvas);
    return;
  }
  lastWebcamTime = video.currentTime;
  canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);

  if (imageSegmenter === undefined) {
    return;
  }

  let startTimeMs = performance.now();

  imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground);
};
```

### Creación de una lógica de reemplazo en segundo plano


Cree la función `replaceBackground`, que fusiona la imagen de fondo personalizada con el primer plano de la transmisión de la cámara para reemplazar el fondo. La función recupera primero los datos de píxeles subyacentes de la imagen de fondo personalizada y la transmisión de video de los dos elementos del lienzo creados anteriormente. A continuación, recorre en iteración la máscara proporcionada por`ImageSegmenter`, que indica qué píxeles están en primer plano. A medida que recorre la máscara, copia de forma selectiva los píxeles que contienen la imagen de la cámara del usuario en los datos de píxeles de fondo correspondientes. Una vez hecho esto, convierte los datos de píxeles finales con el primer plano copiado en el fondo y los dibuja en un lienzo.

#### JavaScript


```
function replaceBackground(result) {
  let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  const mask = result.categoryMask.getAsFloat32Array();
  let j = 0;

  for (let i = 0; i < mask.length; ++i) {
    const maskVal = Math.round(mask[i] * 255.0);

    j += 4;
  // Only copy pixels on to the background image if the mask indicates they are in the foreground
    if (maskVal < 255) {
      backgroundData[j] = imageData[j];
      backgroundData[j + 1] = imageData[j + 1];
      backgroundData[j + 2] = imageData[j + 2];
      backgroundData[j + 3] = imageData[j + 3];
    }
  }

 // Convert the pixel data to a format suitable to be drawn to a canvas
  const uint8Array = new Uint8ClampedArray(backgroundData.buffer);
  const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight);
  canvasCtx.putImageData(dataNew, 0, 0);
  window.requestAnimationFrame(renderVideoToCanvas);
}
```

Como referencia, aquí está el archivo `app.js` completo que contiene toda la lógica anterior:

#### JavaScript


```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

// All helpers are expose on 'media-devices.js' and 'dom.js'
const { setupParticipant } = window;

const { Stage, LocalStageStream, SubscribeType, StageEvents, ConnectionState, StreamType } = IVSBroadcastClient;
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";

let cameraButton = document.getElementById("camera-control");
let micButton = document.getElementById("mic-control");
let joinButton = document.getElementById("join-button");
let leaveButton = document.getElementById("leave-button");

let controls = document.getElementById("local-controls");
let audioDevicesList = document.getElementById("audio-devices");
let videoDevicesList = document.getElementById("video-devices");

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;
let imageSegmenter;
let lastWebcamTime = -1;

const init = async () => {
  await initializeDeviceSelect();

  cameraButton.addEventListener("click", () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera";
  });

  micButton.addEventListener("click", () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic";
  });

  localCamera = await getCamera(videoDevicesList.value);
  const segmentationStream = canvasElement.captureStream();

  joinButton.addEventListener("click", () => {
    joinStage(segmentationStream);
  });

  leaveButton.addEventListener("click", () => {
    leaveStage();
  });

  initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
};

const joinStage = async (segmentationStream) => {
  if (connected || joining) {
    return;
  }
  joining = true;

  const token = document.getElementById("token").value;

  if (!token) {
    window.alert("Please enter a participant token");
    joining = false;
    return;
  }

  // Retrieve the User Media currently set on the page
  localMic = await getMic(audioDevicesList.value);

  cameraStageStream = new LocalStageStream(segmentationStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

  const strategy = {
    stageStreamsToPublish() {
      return [cameraStageStream, micStageStream];
    },
    shouldPublishParticipant() {
      return true;
    },
    shouldSubscribeToParticipant() {
      return SubscribeType.AUDIO_VIDEO;
    },
  };

  stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove("hidden");
    } else {
      controls.classList.add("hidden");
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log("Participant Joined:", participant);
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
    console.log("Participant Media Added: ", participant, streams);

    let streamsToDisplay = streams;

    if (participant.isLocal) {
      // Ensure to exclude local audio streams, otherwise echo will occur
      streamsToDisplay = streams.filter((stream) => stream.streamType === StreamType.VIDEO);
    }

    const videoEl = setupParticipant(participant);
    streamsToDisplay.forEach((stream) => videoEl.srcObject.addTrack(stream.mediaStreamTrack));
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
    console.log("Participant Left: ", participant);
    teardownParticipant(participant);
  });

  try {
    await stage.join();
  } catch (err) {
    joining = false;
    connected = false;
    console.error(err.message);
  }
};

const leaveStage = async () => {
  stage.leave();

  joining = false;
  connected = false;

  cameraButton.innerText = "Hide Camera";
  micButton.innerText = "Mute Mic";
  controls.classList.add("hidden");
};

function replaceBackground(result) {
  let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  const mask = result.categoryMask.getAsFloat32Array();
  let j = 0;

  for (let i = 0; i < mask.length; ++i) {
    const maskVal = Math.round(mask[i] * 255.0);

    j += 4;
    if (maskVal < 255) {
      backgroundData[j] = imageData[j];
      backgroundData[j + 1] = imageData[j + 1];
      backgroundData[j + 2] = imageData[j + 2];
      backgroundData[j + 3] = imageData[j + 3];
    }
  }
  const uint8Array = new Uint8ClampedArray(backgroundData.buffer);
  const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight);
  canvasCtx.putImageData(dataNew, 0, 0);
  window.requestAnimationFrame(renderVideoToCanvas);
}

const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};

const renderVideoToCanvas = async () => {
  if (video.currentTime === lastWebcamTime) {
    window.requestAnimationFrame(renderVideoToCanvas);
    return;
  }
  lastWebcamTime = video.currentTime;
  canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);

  if (imageSegmenter === undefined) {
    return;
  }

  let startTimeMs = performance.now();

  imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground);
};

const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};

createImageSegmenter();
init();
```

### Creación de un archivo de configuración de Webpack


Agregue esta configuración a su archivo de configuración de Webpack para empaquetar`app.js`, de modo que las llamadas de importación funcionen:

#### JavaScript


```
const path = require("path");
module.exports = {
  entry: ["./app.js"],
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};
```

### Agrupación de sus archivos JavaScript


```
npm run build
```

Inicie un servidor HTTP simple desde el directorio que contiene `index.html` y abra `localhost:8000` para ver el resultado:

```
python3 -m http.server -d ./
```

## Android


Para reemplazar el fondo de tu transmisión en directo, puede usar la API de segmentación de selfies del [ML Kit de Google](https://developers.google.com/ml-kit/vision/selfie-segmentation). La API de segmentación de selfies acepta una imagen de cámara como entrada y devuelve una máscara que proporciona una puntuación de confianza para cada píxel de la imagen, que indica si estaba en primer plano o en segundo plano. En función de la puntuación de confianza, puede recuperar el color de píxel correspondiente de la imagen de fondo o de la imagen de primer plano. Este proceso continúa hasta que se hayan examinado todas las puntuaciones de confianza de la máscara. El resultado es una nueva matriz de colores de píxeles que contiene los píxeles del primer plano combinados con los píxeles de la imagen de fondo.

Para integrar la sustitución en segundo plano con el SDK de retransmisión para transmisión en tiempo real de IVS:

1. Instale las bibliotecas CameraX y el kit ML de Google.

1. Inicialice las variables repetitivas.

1. Cree de una fuente de imágenes personalizada.

1. Administre los fotogramas de las cámaras.

1. Transfiera los marcos de las cámaras al ML Kit de Google.

1. Superponga el primer plano del marco de la cámara sobre su fondo personalizado.

1. Introduzca la nueva imagen en una fuente de imágenes personalizada.

### Instalación de las bibliotecas CameraX y del kit ML de Google


Para extraer imágenes de la transmisión de la cámara en vivo, use la biblioteca CameraX de Android. Para instalar la biblioteca CameraX y el kit ML de Google, añada lo siguiente al archivo `build.gradle` del módulo. Sustituya `${camerax_version}` y `${google_ml_kit_version}` por la última versión de las bibliotecas [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) y [ML Kit Google](https://developers.google.com/ml-kit/vision/selfie-segmentation/android), respectivamente. 

#### Java


```
implementation "com.google.mlkit:segmentation-selfie:${google_ml_kit_version}"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
```

Importe las siguientes bibliotecas:

#### Java


```
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
```

### Inicialización de variables reutilizables


Inicialice una instancia de `ImageAnalysis` y una instancia de `ExecutorService`:

#### Java


```
private lateinit var binding: ActivityMainBinding
private lateinit var cameraExecutor: ExecutorService
private var analysisUseCase: ImageAnalysis? = null
```

[Inicialice una instancia en STREAM\$1MODE](https://developers.google.com/ml-kit/vision/selfie-segmentation/android#detector_mode):

#### Java


```
private val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .build()

private val segmenter = Segmentation.getClient(options)
```

### Creación de una fuente de imágenes personalizada


En el método `onCreate` de su actividad, cree una instancia de un objeto `DeviceDiscovery` y cree una fuente de imágenes personalizada. El `Surface` proporcionado por la fuente de imagen personalizada recibirá la imagen final, con el primer plano superpuesto sobre una imagen de fondo personalizada. A continuación, creará una instancia de `ImageLocalStageStream` utilizando la fuente de imagen personalizada. Luego, la instancia de `ImageLocalStageStream` (nombrada `filterStream` en este ejemplo) puede publicarse en un escenario. Consulte la [Guía del SDK de transmisión para Android de IVS](broadcast-android.md) para obtener instrucciones sobre cómo configurar un escenario. Por último, cree también un hilo que se utilizará para gestionar la cámara.

#### Java


```
var deviceDiscovery = DeviceDiscovery(applicationContext)
var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2(
720F, 1280F
))
var surface: Surface = customSource.inputSurface
var filterStream = ImageLocalStageStream(customSource)

cameraExecutor = Executors.newSingleThreadExecutor()
```

### Administración de fotogramas de cámara


A continuación, cree una función para inicializar la cámara. Esta función utiliza la biblioteca CameraX para extraer imágenes de la transmisión de la cámara en directo. En primer lugar, se crea una instancia de `ProcessCameraProvider` llamada `cameraProviderFuture`. Este objeto representa un resultado futuro de la obtención de un proveedor de cámaras. A continuación, cargue una imagen del proyecto en forma de mapa de bits. En este ejemplo se utiliza la imagen de una playa como fondo, pero puede ser cualquier imagen que desee.

A continuación, añada un oyente a `cameraProviderFuture`. Este oyente recibe una notificación cuando la cámara está disponible o si se produce un error durante el proceso de obtención de un proveedor de cámaras.

#### Java


```
private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.beach
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;


        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            
                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

En el oyente, cree `ImageAnalysis.Builder` para acceder a cada fotograma individual de la transmisión de la cámara en directo. Establezca la estrategia de contrapresión para `STRATEGY_KEEP_ONLY_LATEST`. Esto garantiza que solo se entregue un cuadro de cámara a la vez para su procesamiento. Convierta cada fotograma de cámara individual en un mapa de bits, de forma que pueda extraer sus píxeles y luego combinarlos con la imagen de fondo personalizada.

#### Java


```
val imageAnalyzer = ImageAnalysis.Builder()
analysisUseCase = imageAnalyzer
    .setTargetResolution(Size(360, 640))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

analysisUseCase?.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
    val mediaImage = imageProxy.image
    val tempBitmap = imageProxy.toBitmap();
    val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())
```

### Transferencia de los marcos de cámara al ML Kit de Google


A continuación, cree una `InputImage` y pásela a la instancia de Segmenter para su procesamiento. Se puede crear una `InputImage` a partir de una `ImageProxy` proporcionada por la instancia de `ImageAnalysis`. Una vez que se proporciona una `InputImage` a Segmenter, devuelve una máscara con puntuaciones de confianza que indican la probabilidad de que un píxel esté en primer plano o en segundo plano. Esta máscara también proporciona propiedades de ancho y alto, que utilizará para crear una nueva matriz que contenga los píxeles de fondo de la imagen de fondo personalizada cargada anteriormente.

#### Java


```
if (mediaImage != null) {
        val inputImage =
            InputImage.fromMediaImag


segmenter.process(inputImage)
    .addOnSuccessListener { segmentationMask ->
        val mask = segmentationMask.buffer
        val maskWidth = segmentationMask.width
        val maskHeight = segmentationMask.height
        val backgroundPixels = IntArray(maskWidth * maskHeight)
        bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)
```

### Superposición del primer plano del marco de la cámara sobre el fondo personalizado


Con la máscara que contiene las puntuaciones de confianza, el marco de la cámara como mapa de bits y los píxeles de color de la imagen de fondo personalizada, tiene todo lo que necesita para superponer el primer plano al fondo personalizado. A continuación, se invoca la función `overlayForeground` con los siguientes parámetros:

#### Java


```
resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
```

Esta función recorre la máscara y comprueba los valores de confianza para determinar si se debe obtener el color de píxel correspondiente de la imagen de fondo o del marco de la cámara. Si el valor de confianza indica que lo más probable es que un píxel de la máscara esté en segundo plano, obtendrá el color de píxel correspondiente de la imagen de fondo; de lo contrario, obtendrá el color de píxel correspondiente del marco de la cámara para crear el primer plano. Una vez que la función termine de recorrer la máscara, se crea un nuevo mapa de bits con la nueva matriz de píxeles de color y se devuelve. Este nuevo mapa de bits contiene el primer plano superpuesto sobre el fondo personalizado.

#### Java


```
private fun overlayForeground(
        byteBuffer: ByteBuffer,
        maskWidth: Int,
        maskHeight: Int,
        cameraBitmap: Bitmap,
        backgroundPixels: IntArray
    ): Bitmap {
        @ColorInt val colors = IntArray(maskWidth * maskHeight)
        val cameraPixels = IntArray(maskWidth * maskHeight)

        cameraBitmap.getPixels(cameraPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

        for (i in 0 until maskWidth * maskHeight) {
            val backgroundLikelihood: Float = 1 - byteBuffer.getFloat()

            // Apply the virtual background to the color if it's not part of the foreground
            if (backgroundLikelihood > 0.9) {
                // Get the corresponding pixel color from the background image
                // Set the color in the mask based on the background image pixel color
                colors[i] = backgroundPixels.get(i)
            } else {
                // Get the corresponding pixel color from the camera frame
                // Set the color in the mask based on the camera image pixel color
                colors[i] = cameraPixels.get(i)
            }
        }

        return Bitmap.createBitmap(
            colors, maskWidth, maskHeight, Bitmap.Config.ARGB_8888
        )
    }
```

### Introducción de la nueva imagen en una fuente de imagen personalizada


A continuación, puede escribir el nuevo mapa de bits en el `Surface` proporcionado por una fuente de imagen personalizada. Esto se transmitirá a su escenario.

#### Java


```
resultBitmap = overlayForeground(mask, inputBitmap, mutableBitmap, bgBitmap)
canvas = surface.lockCanvas(null);
canvas.drawBitmap(resultBitmap, 0f, 0f, null)
```

Esta es la función completa para obtener los fotogramas de la cámara, pasarlos a Segmenter y superponerlos sobre el fondo:

#### Java


```
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
    private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.clouds
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val imageAnalyzer = ImageAnalysis.Builder()
            analysisUseCase = imageAnalyzer
                .setTargetResolution(Size(720, 1280))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()

            analysisUseCase!!.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
                val mediaImage = imageProxy.image
                val tempBitmap = imageProxy.toBitmap();
                val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())

                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                    segmenter.process(inputImage)
                        .addOnSuccessListener { segmentationMask ->
                            val mask = segmentationMask.buffer
                            val maskWidth = segmentationMask.width
                            val maskHeight = segmentationMask.height
                            val backgroundPixels = IntArray(maskWidth * maskHeight)
                            bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```