IVS Broadcast SDK와 함께 배경 교체 사용
배경 교체는 실시간 스트리밍 제작자가 배경을 변경할 수 있도록 하는 카메라 필터의 일종입니다. 다음 다이어그램에서 볼 수 있듯이 배경 교체 작업은 다음과 같습니다.
-
라이브 카메라 피드에서 카메라 이미지를 가져옵니다.
-
Google ML Kit를 사용하여 전경 구성 요소와 배경 구성 요소로 구분합니다.
-
생성된 분할 마스크를 사용자 지정 배경 이미지와 결합합니다.
-
브로드캐스트용 사용자 지정 이미지 소스에 전달합니다.

웹
이 섹션에서는 웹 브로드캐스트 SDK를 사용하여 비디오를 게시 및 구독하는 방법을 이미 잘 알고 있다고 가정합니다.
라이브 스트림의 배경을 사용자 지정 이미지로 바꾸려면 MediaPipe 이미지 Segmenter
배경 교체를 IVS 실시간 스트리밍 웹 브로드캐스트 SDK와 통합하려면 다음이 작업을 수행해야 합니다.
-
MediaPipe와 Webpack을 설치하세요. (이 예에서는 Webpack을 번들러로 사용하지만 원하는 번들러를 사용할 수 있습니다.)
-
index.html
을 생성합니다. -
미디어 요소를 추가하세요.
-
스크립트 태그를 추가하세요.
-
app.js
을 생성합니다. -
사용자 지정 배경 이미지를 불러오세요.
-
ImageSegmenter
의 인스턴스를 만듭니다. -
캔버스로 비디오 피드를 렌더링하세요.
-
배경 교체 로직을 생성하세요.
-
Webpack 구성 파일을 생성하세요.
-
JavaScript 파일을 번들링하세요.
MediaPipe 및 Webpack 설치
시작하려면 @mediapipe/tasks-vision
및 webpack
npm 패키지를 설치하세요. 아래 예제에서는 Webpack을 JavaScript 번들러로 사용합니다. 원하는 경우 다른 번들러를 사용할 수 있습니다.
npm i @mediapipe/tasks-vision webpack webpack-cli
또한 webpack
을 빌드 스크립트로 지정하도록 package.json
을 업데이트해야 합니다.
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" },
index.html 생성
다음으로 HTML 표준 문안을 생성하고 웹 브로드캐스트 SDK를 스크립트 태그로 가져오세요. 다음 코드에서는 사용 중인 브로드캐스트 SDK 버전으로 <SDK version>
을 바꿔야 합니다.
<!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>
미디어 요소 추가
다음으로 본문 태그에 비디오 요소 하나와 캔버스 요소 두 개를 추가합니다. 비디오 요소는 라이브 카메라 피드를 포함하며 MediaPipe 이미지 Segmenter에 대한 입력으로 사용합니다. 첫 번째 캔버스 요소는 브로드캐스트될 피드의 미리보기를 렌더링하는 데 사용됩니다. 두 번째 캔버스 요소는 배경으로 사용할 사용자 지정 이미지를 렌더링하는 데 사용됩니다. 사용자 지정 이미지가 있는 두 번째 캔버스는 최종 캔버스에 프로그래밍 방식으로 픽셀을 복사하기 위한 소스로만 사용되므로 보기에서 숨겨집니다.
<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>
스크립트 태그 추가
스크립트 태그를 추가하여 배경 교체 코드를 포함하는 번들 JavaScript 파일을 로드하고 스테이지에 게시합니다.
<script src="./dist/bundle.js"></script>
app.js 생성
다음으로 JavaScript 파일을 생성하여 HTML 페이지에서 만든 캔버스 및 비디오 요소의 요소 객체를 가져옵니다. ImageSegmenter
및 FilesetResolver
모듈을 가져옵니다. ImageSegmenter
모듈은 분할 작업을 수행하는 데 사용됩니다.
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";
다음으로, 사용자 카메라에서 MediaStream을 검색하는 init()
함수를 생성하고, 카메라 프레임 로드가 완료될 때마다 콜백 함수를 호출합니다. 스테이지에 참여 및 퇴장할 수 있는 버튼에 이벤트 리스너를 추가합니다.
스테이지에 참가할 때는 segmentationStream
이라는 변수를 전달합니다. 캔버스 요소에서 캡처한 비디오 스트림에 배경을 나타내는 사용자 지정 이미지 위에 전경 이미지가 오버레이되어 있습니다. 나중에 이 사용자 지정 스트림을 사용하여 스테이지에 게시할 수 있는 LocalStageStream
의 인스턴스를 생성할 수 있습니다.
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(); }); };
사용자 지정 배경 이미지 로드
init
함수 아래쪽에 initBackgroundCanvas
라는 함수를 호출하는 코드를 추가합니다. 이 함수는 로컬 파일에서 사용자 지정 이미지를 로드하여 캔버스에 렌더링합니다. 다음 단계에서 이 함수를 정의할 것입니다. 사용자 카메라에서 검색한 MediaStream
을 비디오 객체에 할당합니다. 나중에 이 비디오 객체는 이미지 Segmenter로 전달됩니다. 또한 비디오 프레임 로드가 완료될 때마다 호출되도록 콜백 함수로서 이름이 renderVideoToCanvas
인 함수를 설정하세요. 이후 단계에서 이 함수를 정의할 것입니다.
initBackgroundCanvas(); video.srcObject = localCamera; video.addEventListener("loadeddata", renderVideoToCanvas);
로컬 파일에서 이미지를 로드하는 initBackgroundCanvas
함수를 구현해 보겠습니다. 이 예에서는 해변 이미지를 사용자 지정 배경으로 사용합니다. 사용자 지정 이미지가 있는 캔버스는 카메라 피드를 포함한 캔버스 요소의 전경 픽셀과 병합되므로 디스플레이에서 숨겨집니다.
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 인스턴스 생성
다음으로 이미지를 분할하고 결과를 마스크로 반환하는 ImageSegmenter
인스턴스를 생성합니다. ImageSegmenter
의 인스턴스를 생성할 때는 셀카 분할 모델
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, }); };
캔버스로 비디오 피드 렌더링
다음으로, 비디오 피드를 다른 캔버스 요소에 렌더링하는 함수를 생성합니다. Canvas 2D API를 사용하여 전경 픽셀을 추출하려면 비디오 피드를 캔버스로 렌더링해야 합니다. 이 작업을 수행하는 동안 segmentforVideoImageSegmenter
인스턴스로 전달합니다. segmentforVideoreplaceBackground
를 호출하여 배경 교체를 수행합니다.
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); };
배경 교체 로직 생성
사용자 지정 배경 이미지를 카메라 피드의 전경과 병합하여 배경을 대체하는 replaceBackground
함수를 생성하세요. 함수는 먼저 이전에 만든 두 캔버스 요소에서 사용자 지정 배경 이미지와 비디오 피드의 기본 픽셀 데이터를 검색합니다. 그런 다음 ImageSegmenter
에서 제공한 마스크를 반복하여 전경에 있는 픽셀을 나타냅니다. 마스크를 반복하면서 사용자의 카메라 피드가 포함된 픽셀을 해당 배경 픽셀 데이터에 선택적으로 복사합니다. 이 작업이 완료되면 전경을 복사한 최종 픽셀 데이터를 배경으로 변환하고 캔버스에 그립니다.
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
파일은 다음과 같습니다.
/*! 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 구성 파일 생성
이 구성을 Webpack 구성 파일에 추가하여 app.js
을 번들링하면 가져오기 호출이 제대로 작동합니다.
const path = require("path"); module.exports = { entry: ["./app.js"], output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, };
JavaScript 파일 번들링
npm run build
index.html
이 들어 있는 디렉토리에서 간단한 HTTP 서버를 시작하고 localhost:8000
을 열어서 결과를 확인합니다.
python3 -m http.server -d ./
Android
실시간 스트림의 배경을 바꾸기 위해 Google ML Kit
배경 교체를 IVS 실시간 스트리밍 Android 브로드캐스트 SDK와 통합하려면 다음 작업을 수행해야 합니다.
-
CameraX 라이브러리와 Google ML Kit를 설치합니다.
-
표준 문안 변수를 초기화합니다.
-
사용자 지정 이미지 소스를 생성합니다.
-
카메라 프레임을 관리합니다.
-
카메라 프레임을 Google ML Kit로 전달하세요.
-
카메라 프레임 전경을 사용자 지정 배경에 오버레이하세요.
-
새 이미지를 사용자 지정 이미지 소스에 제공하세요.
CameraX 라이브러리 및 Google ML Kit 설치
라이브 카메라 피드에서 이미지를 추출하려면 Android의 CameraX 라이브러리를 사용하세요. CameraX 라이브러리와 Google ML Kit를 설치하려면 모듈의 build.gradle
파일에 다음을 추가하세요. ${camerax_version}
과 ${google_ml_kit_version}
을 각각 최신 버전의 CameraX
implementation "com.google.mlkit:segmentation-selfie:${google_ml_kit_version}" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}"
다음 라이브러리를 가져옵니다.
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
표준 문안 변수 초기화
ImageAnalysis
의 인스턴스와 다음 ExecutorService
의 인스턴스를 초기화합니다.
private lateinit var binding: ActivityMainBinding private lateinit var cameraExecutor: ExecutorService private var analysisUseCase: ImageAnalysis? = null
STREAM_MODE
private val options = SelfieSegmenterOptions.Builder() .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE) .build() private val segmenter = Segmentation.getClient(options)
사용자 지정 이미지 소스 생성
활동의 onCreate
메서드에서 DeviceDiscovery
개체의 인스턴스를 생성하고 사용자 지정 이미지 소스를 생성하세요. 사용자 지정 이미지 소스에서 제공한 Surface
로 사용자 지정 배경 이미지 위에 전경이 오버레이된 최종 이미지를 받게 됩니다. 그런 다음 사용자 지정 이미지 소스를 사용하여 ImageLocalStageStream
의 인스턴스를 생성합니다. 그러면 ImageLocalStageStream
의 인스턴스(이 예제에서는 filterStream
으로 이름이 지정됨)를 스테이지에 게시할 수 있습니다. 스테이지 설정에 대한 지침은 IVS Android 브로드캐스트 SDK 가이드를 참조하세요. 마지막으로 카메라를 관리하는 데 사용할 스레드도 생성하세요.
var deviceDiscovery = DeviceDiscovery(applicationContext) var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2( 720F, 1280F )) var surface: Surface = customSource.inputSurface var filterStream = ImageLocalStageStream(customSource) cameraExecutor = Executors.newSingleThreadExecutor()
카메라 프레임 관리
다음으로 카메라를 초기화하는 함수를 생성합니다. 이 함수는 CameraX 라이브러리를 사용하여 라이브 카메라 피드에서 이미지를 추출합니다. 먼저 cameraProviderFuture
라는 ProcessCameraProvider
인스턴스를 생성합니다. 이 객체는 카메라 공급자를 확보한 미래의 결과를 나타냅니다. 그런 다음 프로젝트에서 이미지를 비트맵으로 로드합니다. 이 예제에서는 해변 이미지를 배경으로 사용하지만 원하는 어떤 이미지라도 사용할 수 있습니다.
그런 다음 cameraProviderFuture
에 리스너를 추가합니다. 카메라를 사용할 수 있게 되거나 카메라 공급자를 구하는 과정에서 오류가 발생하면 이 리스너에 알림이 전송됩니다.
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
로 설정합니다. 이렇게 하면 한 번에 하나의 카메라 프레임만 처리에 전송되도록 할 수 있습니다. 각 개별 카메라 프레임을 비트맵으로 변환하면 픽셀을 추출하여 나중에 사용자 지정 배경 이미지와 결합할 수 있습니다.
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로 전달
다음으로, InputImage
를 생성하여 처리를 위한 Segmenter 인스턴스에 전달합니다. ImageAnalysis
의 인스턴스에서 제공하는 ImageProxy
로 InputImage
를 생성할 수 있습니다. Segmenter에 InputImage
가 제공되면 픽셀이 전경이나 배경에 있을 가능성을 나타내는 신뢰도 점수가 포함된 마스크를 반환합니다. 이 마스크는 너비 및 높이 속성도 제공하며, 이 속성을 사용하여 이전에 로드한 사용자 지정 배경 이미지의 배경 픽셀을 포함하는 새 배열을 생성할 수 있습니다.
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)
카메라 프레임 전경을 사용자 지정 배경에 오버레이
신뢰도 점수가 포함된 마스크, 비트맵의 카메라 프레임, 사용자 지정 배경 이미지의 색상 픽셀을 사용하면 전경을 사용자 지정 배경에 오버레이하는 데 필요한 모든 것을 얻을 수 있습니다. 그 다음 파라미터로 overlayForeground
함수를 호출합니다.
resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
이 함수는 마스크를 반복하고 신뢰도 값을 확인하여 배경 이미지에서 해당 픽셀 색상을 가져올지 아니면 카메라 프레임에서 가져올지 결정합니다. 신뢰도 값이 마스크의 픽셀이 배경에 있을 가능성이 높다는 것을 나타내면 배경 이미지에서 해당 픽셀 색상을 가져오고, 그렇지 않으면 카메라 프레임에서 해당 픽셀 색상을 가져와 전경을 만듭니다. 함수가 마스크 반복을 마치면 새 색상 픽셀 배열을 사용하여 새 비트맵을 생성하고 반환합니다. 이 새 비트맵은 사용자 지정 배경에 오버레이된 전경을 포함하고 있습니다.
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 ) }
새 이미지를 사용자 지정 이미지 소스에 제공
그런 다음 사용자 지정 이미지 소스에서 제공한 Surface
에 새 비트맵을 쓸 수 있습니다. 그러면 스테이지로 브로드캐스트합니다.
resultBitmap = overlayForeground(mask, inputBitmap, mutableBitmap, bgBitmap) canvas = surface.lockCanvas(null); canvas.drawBitmap(resultBitmap, 0f, 0f, null)
카메라 프레임을 가져와서 Segmenter로 전달하고 백그라운드에 오버레이하는 전체 기능은 다음과 같습니다.
@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)) }