

# 搭配 IVS 廣播 SDK 使用背景替換
<a name="broadcast-3p-camera-filters-background-replacement"></a>

背景替換是一種攝影機濾鏡，可讓即時串流創作者更改背景。如下圖所示，替換背景包含：

1. 從即時攝影機供稿獲取攝影機影像。

1. 使用 Google ML Kit 將其分割成前景和背景組件。

1. 組合產生的分割遮罩與自訂背景影像。

1. 將其傳遞給自訂影像來源以進行廣播。

![\[實作背景替換的工作流程。\]](http://docs.aws.amazon.com/zh_tw/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Background_Replacement.png)


## Web
<a name="background-replacement-web"></a>

本節假設您已熟悉[使用 Web 廣播 SDK 發布和訂閱影片](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-pub-sub-web.html)。

若要以自訂影像替換即時串流的背景，請使用具有 [MediaPipe 影像分割器](https://developers.google.com/mediapipe/solutions/vision/image_segmenter)的[自拍分割模型](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model)。這是一種機器學習模型，可識別影片影格中的哪些像素位於前景或背景中。然後，您可以使用模型的結果來替換即時串流的背景，方法是將影片供稿中的前景像素複製到代表新背景的自訂影像。

若要整合背景替換與 IVS 即時串流 Web 廣播 SDK，您需要：

1. 安裝 MediaPipe 和 Webpack。(我們的範例使用 Webpack 作為打包工具，但您可以自行選擇任何打包工具。)

1. 建立 `index.html`。

1. 新增媒體元素。

1. 新增指令碼標籤。

1. 建立 `app.js`。

1. 載入自訂背景影像。

1. 建立 `ImageSegmenter` 的執行個體。

1. 將影片供稿轉譯到畫布。

1. 建立背景替換邏輯。

1. 建立 Webpack 組態檔。

1. 綁定自己的 JavaScript 檔案。

### 安裝 MediaPipe 和 Webpack
<a name="background-replacement-web-install-mediapipe-webpack"></a>

若要開始，請先安裝 `@mediapipe/tasks-vision` 和 `webpack` npm 套件。以下範例使用 Webpack 作為 JavaScript 打包工具；如果願意，您也可以使用不同的打包工具。

#### JavaScript
<a name="background-replacement-web-install-mediapipe-webpack-code"></a>

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

請務必更新自己的 `package.json` 將 `webpack` 指定為建置指令碼：

#### JavaScript
<a name="background-replacement-web-update-package-json-code"></a>

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

### 建立 index.html
<a name="background-replacement-web-create-index"></a>

接下來，建立 HTML 樣板並將 Web 廣播 SDK 匯入為指令碼標籤。在下列程式碼中，請務必用您的廣播 SDK 版本取代 `<SDK version>`。

#### JavaScript
<a name="background-replacement-web-create-index-code"></a>

```
<!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>
```

### 新增媒體元素
<a name="background-replacement-web-add-media-elements"></a>

接下來，在 body 標籤中新增一個影片元素和兩個畫布元素。影片元素會包含即時攝影機供稿，並將用作 MediaPipe 影像分割器的輸入。第一個畫布元素將用於轉譯要廣播的供稿的預覽。第二個畫布元素將用於轉譯要當作背景的自訂影像。由於具有自訂影像的第二個畫布僅用於將像素以編程方式複製到最終畫布的來源，檢視中會隱藏該畫布。

#### JavaScript
<a name="background-replacement-web-add-media-elements-code"></a>

```
<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>
```

### 新增指令碼標籤
<a name="background-replacement-web-add-script-tag"></a>

新增指令碼標籤來載入綁定的 JavaScript 檔案，該檔案會包含執行背景替換並將其發布至階段的程式碼：

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

### 建立 app.js
<a name="background-replacement-web-create-appjs"></a>

接下來建立一個 JavaScript 檔案，獲取在 HTML 頁面中建立的畫布和影片元素的元素物件。匯入 `ImageSegmenter` 和 `FilesetResolver` 模組。`ImageSegmenter` 模組將用於執行分割任務。

#### JavaScript
<a name="create-appjs-import-imagesegmenter-fileresolver-code"></a>

```
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";
```

接下來建立一個名為 `init()` 的函數，從使用者的攝影機擷取 MediaStream，並在每次攝影機影格完成加載時調用回呼函數。為加入和離開階段按鈕新增事件接聽程式。

請注意，加入階段時，我們會傳遞一個名為 `segmentationStream` 的變數。這是從畫布元素擷取的影片串流，其中包含疊加在代表背景的自訂影像上的前景影像。稍後，此自訂串流將用於建立可發布至階段的 `LocalStageStream` 執行個體。

#### JavaScript
<a name="create-appjs-create-init-code"></a>

```
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();
  });
};
```

### 載入自訂背景影像
<a name="background-replacement-web-background-image"></a>

在 `init` 函數底部新增代碼來呼叫名為 `initBackgroundCanvas` 的函數，該函數會從本地檔案加載自訂影像並將其轉譯到畫布上。我們將在下一個步驟中定義此函數。將從使用者攝影機擷取的 `MediaStream` 指派給影片物件。稍後，此影片物件將傳遞給影像分割器。另外，設定一個名為 `renderVideoToCanvas` 的回呼函數，在影片影格完成加載時調用。我們將在後續步驟中定義此函數。

#### JavaScript
<a name="background-replacement-web-load-background-image-code"></a>

```
initBackgroundCanvas();

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

讓我們實現從本地檔案載入影像的 `initBackgroundCanvas` 函數。此範例使用海灘影像作為自訂背景。包含自訂影像的畫布將被隱藏而不顯示，這是因為您會將其與包含攝影機供稿的畫布元素的前景像素合併。

#### JavaScript
<a name="background-replacement-web-implement-initBackgroundCanvas-code"></a>

```
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);
  };
};
```

### 建立 ImageSegmenter 的執行個體
<a name="background-replacement-web-imagesegmenter"></a>

接下來建立 `ImageSegmenter` 的執行個體，該執行個體會分割影像並將結果回傳為遮罩。建立 `ImageSegmenter` 的執行個體時，您會用到[自拍分割模型](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model)。

#### JavaScript
<a name="background-replacement-web-imagesegmenter-code"></a>

```
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,
  });
};
```

### 將影片供稿轉譯到畫布
<a name="background-replacement-web-render-video-to-canvas"></a>

接下來，建立將影片供稿轉譯到另一個畫布元素的函數。我們需要將影片供稿轉譯到畫布，以便使用 Canvas 2D API 從中提取前景像素。執行此操作時，我們也會將影片影格傳遞給我們的 `ImageSegmenter` 執行個體，使用 [segmentforVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) 方法分割影片影格中的前景和背景。當 [segmentforVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) 方法返回時，它會調用我們的自訂回呼函數 `replaceBackground` 來執行背景替換。

#### JavaScript
<a name="background-replacement-web-render-video-to-canvas-code"></a>

```
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);
};
```

### 建立背景替換邏輯
<a name="background-replacement-web-logic"></a>

建立 `replaceBackground` 函數，將自訂背景影像與攝影機供稿的前景合併以替換背景。該函數會首先從先前建立的兩個畫布元素中，檢索自訂背景影像的基礎像素資料和影片供稿。然後，它反复執行 `ImageSegmenter` 提供的遮罩，其中指出哪些像素屬於前景。在反复執行遮罩時，它會選擇性地將包含使用者攝影機供稿的像素複製到對應的背景像素資料中。完成後，它會將前景複本上的最終像素資料轉換為背景並繪製到畫布上。

#### JavaScript
<a name="background-replacement-web-logic-create-replacebackground-code"></a>

```
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);
}
```

作為參考，這裡的完整 `app.js` 檔案包含了上述所有邏輯：

#### JavaScript
<a name="background-replacement-web-logic-app-js-code"></a>

```
/*! 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();
```

### 建立一個 Webpack 組態檔
<a name="background-replacement-web-webpack-config"></a>

將此組態新增到自己的 Webpack 組態檔來綁定 `app.js`，讓匯入呼叫起作用：

#### JavaScript
<a name="background-replacement-web-webpack-config-code"></a>

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

### 綁定自己的 JavaScript 檔案
<a name="background-replacement-web-bundle-javascript"></a>

```
npm run build
```

從包含 `index.html` 的目錄啟動一個簡單的 HTTP 伺服器，然後打開 `localhost:8000` 查看結果：

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

## Android
<a name="background-replacement-android"></a>

要替換即時串流中的背景，您可以使用 [Google ML Kit](https://developers.google.com/ml-kit/vision/selfie-segmentation) 的自拍分割 API。自拍分割 API 接受攝影機影像作為輸入，並可傳回遮罩為影像的每個像素提供信賴度分數，指出該像素是在前景中還是背景中。然後，您就能根據信賴度分數從背景影像或前景影像擷取對應的像素顏色。這個過程會持續進行，直到檢查完遮罩中的所有信賴度分數為止。結果會產生一個新的像素顏色陣列，其中包含前景像素與背景影像中像素的組合。

若要整合背景替換與 IVS 即時串流 Android 廣播 SDK，您需要：

1. 安裝 CameraX 程式庫和 Google ML Kit。

1. 初始化樣板變數。

1. 建立自訂影像來源。

1. 管理攝影機影格。

1. 將攝影機影格傳遞給 Google ML Kit。

1. 將攝影機影格前景覆疊到自訂背景上。

1. 將新影像提供給自訂影像來源。

### 安裝 CameraX 程式庫和 Google ML Kit
<a name="background-replacement-android-install-camerax-googleml"></a>

要從即時攝影機供稿中提取影像，請使用 Android 的 CameraX 程式庫。要安裝 CameraX 程式庫和 Google ML Kit，請將以下內容新增到模組的 `build.gradle` 檔案中。用最新版本的 [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) 和 [Google ML Kit](https://developers.google.com/ml-kit/vision/selfie-segmentation/android) 程式庫分別替換 `${camerax_version}` 與 `${google_ml_kit_version}`。

#### Java
<a name="background-replacement-android-install-camerax-googleml-code"></a>

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

匯入下列程式庫：

#### Java
<a name="background-replacement-android-import-libraries-code"></a>

```
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
```

### 初始化樣板變數
<a name="background-replacement-android-initialize-variables"></a>

初始化 `ImageAnalysis` 的執行個體和 `ExecutorService` 的執行個體：

#### Java
<a name="background-replacement-android-initialize-imageanalysis-executorservice-code"></a>

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

在 [STREAM\$1MODE](https://developers.google.com/ml-kit/vision/selfie-segmentation/android#detector_mode) 中初始化一個分割器執行個體：

#### Java
<a name="background-replacement-android-initialize-segmenter-code"></a>

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

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

### 建立自訂影像來源
<a name="background-replacement-android-create-image-source"></a>

在活動的 `onCreate` 方法中，建立 `DeviceDiscovery` 物件的執行個體，並建立一個自訂影像來源。自訂影像來源提供的 `Surface` 會收到前景疊加在自訂背景影像上的最終影像。然後，您要使用自訂影像來源建立 `ImageLocalStageStream` 的執行個體。之後，`ImageLocalStageStream` 的執行個體 (在此範例中名為 `filterStream`) 就能發布至階段。如需如何設定階段的說明，請參閱 [IVS Android 廣播 SDK 指南](broadcast-android.md)。最後，也要建立一個用於管理攝影機的線程。

#### Java
<a name="background-replacement-android-create-image-source-code"></a>

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

cameraExecutor = Executors.newSingleThreadExecutor()
```

### 管理攝影機影格
<a name="background-replacement-android-camera-frames"></a>

接下來，建立一個函數來初始化攝影機。此函數使用 CameraX 程式庫從即時攝影機供稿中提取影像。首先，您要建立名為 `cameraProviderFuture` 的 `ProcessCameraProvider` 執行個體。該物件表示獲得攝影機提供者的未來結果。然後，您將專案中的影像載入為點陣圖。此範例使用海灘影像作為背景，但您可以使用任何影像。

接著，您將接聽程式新增到 `cameraProviderFuture`。當攝影機變得可用或在取得攝影機提供者的過程中發生錯誤，此接聽程式機會受到通知。

#### Java
<a name="background-replacement-android-initialize-camera-code"></a>

```
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))
    }
```

在接聽程式中，建立 `ImageAnalysis.Builder` 存取即時攝影機供稿中的每個單獨影格。將背壓策略設定為 `STRATEGY_KEEP_ONLY_LATEST`。這樣可以確保一次僅交付一個攝影機影格進行處理。將每個單獨的攝影機影格轉換為點陣圖，以便您可以提取其像素，並於稍後將其與自訂背景影像合併。

#### Java
<a name="background-replacement-android-create-imageanalysisbuilder-code"></a>

```
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())
```

### 將攝影機影格傳遞給 Google ML Kit
<a name="background-replacement-android-frames-to-mlkit"></a>

接下來，建立 `InputImage` 並將其傳遞給分割器的執行個體進行處理。可在 `ImageAnalysis` 執行個體提供的 `ImageProxy` 中建立 `InputImage`。只要將 `InputImage` 提供給分割器，就會回傳一個帶有信賴度分數的遮罩，指示像素處於前景或背景的可能性。這個遮罩還提供寬高屬性，可供您建立一組新的陣列，其中包含先前載入的自訂背景影像的背景像素。

#### Java
<a name="background-replacement-android-frames-to-mlkit-code"></a>

```
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)
```

### 將攝影機影格前景覆疊到自訂背景上
<a name="background-replacement-android-overlay-frame-foreground"></a>

有了包含信賴度分數的遮罩、當成點陣圖的攝影機影格以及自訂背景影像中的色彩像素，您就擁有將前景覆疊到自訂背景上所需的一切。接著，就能使用下列參數呼叫 `overlayForeground` 函數：

#### Java
<a name="background-replacement-android-call-overlayforeground-code"></a>

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

此函數會反复執行遮罩，並檢查信賴度值，從而決定是從背景影像還是攝影機影格取得對應的像素顏色。如果信賴度值表示遮罩中的像素很可能出現在背景中，將從背景影像中獲取相應的像素顏色；否則，將從攝影機影格中獲取相應的像素顏色來建置前景。函數完成對遮罩的反覆處理後，就會使用新的色彩像素陣列建立新的點陣圖並傳回。這個新的點陣圖包含疊加在自訂背景上的前景。

#### Java
<a name="background-replacement-android-run-overlayforeground-code"></a>

```
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
        )
    }
```

### 將新影像提供給自訂影像來源
<a name="background-replacement-android-custom-image-source"></a>

然後，您可以將新的點陣圖寫入由自訂影像來源提供的 `Surface`。這會將其廣播到您的階段。

#### Java
<a name="background-replacement-android-custom-image-source-code"></a>

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

以下是獲取攝影機影格、傳遞給分割器並覆疊在背景上的完整函數：

#### Java
<a name="background-replacement-android-custom-image-source-startcamera-code"></a>

```
@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))
    }
```