LoginSignup
19
18

More than 3 years have passed since last update.

【OpenCV plus Unity】リアルタイムで人物を消す

Last updated at Posted at 2021-04-20

概要

IMG_0090.PNG

左上:カメラ画像 / 右上:マスク画像 / 左下:加工後画像

HumanStencilの取得はARFoundationを、画像の加工にはOpenCV plus Unityを使用しています。

手順としては以下3ステップです。

  1. ARFoundationからカメラ画像を取得する
  2. ARFoundationからHumanStencilを取得する
  3. 画像を加工して人を消す

ソースコード公開しています:https://github.com/AzetaTakuya/InvisibleHuman

環境

  • iPad Pro 11インチ(第2世代)
  • Unity 2021.2.0a8
  • ARFoundation 4.1.0
  • OpenCV plus Unity

実装

1. ARFoundationからカメラ画像を取得する

ARCameraManagerのコールバックから、カメラ画像を取得します

参考:https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/cpu-camera-image.html

[SerializeField] private ARCameraManager CameraManager;
private Texture2D RGB_Texture;

private void Start()
{
    CameraManager.frameReceived += OnARCameraFrameReceived;
}

unsafe void OnARCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
{
    if (!CameraManager.TryAcquireLatestCpuImage(out XRCpuImage image)) return;

    var conversionParams = new XRCpuImage.ConversionParams
    {
        inputRect = new RectInt(0, 0, image.width, image.height),
        outputDimensions = new Vector2Int(image.width / 2, image.height / 2),
        outputFormat = TextureFormat.RGBA32,
        transformation = XRCpuImage.Transformation.MirrorY
    };

    int size = image.GetConvertedDataSize(conversionParams);
    var buffer = new NativeArray<byte>(size, Allocator.Temp);
    image.Convert(conversionParams, new IntPtr(buffer.GetUnsafePtr()), buffer.Length);
    image.Dispose();

    if (RGB_Texture == null)
    {
        var x = conversionParams.outputDimensions.x;
        var y = conversionParams.outputDimensions.y;
        RGB_Texture = new Texture2D(x, y, conversionParams.outputFormat, false);
    }

    RGB_Texture.LoadRawTextureData(buffer);
    RGB_Texture.Apply();

    buffer.Dispose();
}

2. ARFoundationからHuman Stencilを取得する

手順は以下の通りです。

  • AROcclusionManagerのhumanStencilTextureを取得
  • humanStencilTextureをGraphics.Blitで加工し、RenderTextureに出力
  • 出力したRenderTextureをTexture2Dに変換
[SerializeField] private AROcclusionManager OcclusionManager;
[SerializeField] private Material HumanStencil_Material;
private RenderTexture HumanStencil_RT;
private Texture2D Stencil_Texture;

private void Update()
{
    var currentStencil = OcclusionManager.humanStencilTexture;

    if (currentStencil == null) return;

    if (HumanStencil_RT == null)
    {
        HumanStencil_RT = RenderTexture.GetTemporary(currentStencil.width, currentStencil.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
        HumanStencil_RT.Create();
    }
    if (Stencil_Texture == null)
    {
        Stencil_Texture = new Texture2D(currentStencil.width, currentStencil.height);
    }

    Graphics.Blit(currentStencil, HumanStencil_RT, HumanStencil_Material);

    var currentRT = RenderTexture.active;
    RenderTexture.active = HumanStencil_RT;
    Stencil_Texture.ReadPixels(new UnityEngine.Rect(0, 0, Stencil_Texture.width, Stencil_Texture.height), 0, 0);
    Stencil_Texture.Apply();
    RenderTexture.active = currentRT;
}

3. 画像を加工して人を消す!!

手順は以下の通りです。

  • HumanStencil
    • グレースケール化
    • 人物部分拡張
    • 画像サイズ変更
  • カメラ画像
    • サイズ変更
    • 画像反転
  • 出力画像
    • マスク画像を使ったカメラ画像の修正
[SerializeField] private RawImage RGB_Image;
[SerializeField] private RawImage HumanStencil_Image;
[SerializeField] private RawImage Inpaint_Image;

private int width = 640, height = 480;
private CancellationTokenSource tokenSource;

private void Start()
{
    tokenSource = new CancellationTokenSource();
    var cancelToken = tokenSource.Token;
    _ = CaptureLoop(cancelToken);
}

private async Task CaptureLoop(CancellationToken cancelToken)
{
    byte[] dilateArray =
        {   1, 1, 1, 1, 1,
            1, 1, 1, 1, 1,
            1, 1, 1, 1, 1,
            1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, };

    Texture2D stencilViewTexture = new Texture2D(width,height);
    Texture2D rgbViewTexture = new Texture2D(width, height);
    Texture2D inpaintViewTexture = new Texture2D(width, height);

    HumanStencil_Image.texture = stencilViewTexture;
    RGB_Image.texture = rgbViewTexture;
    Inpaint_Image.texture = inpaintViewTexture;

    while (!cancelToken.IsCancellationRequested)
    {
        await Task.Delay(20);

        if (RGB_Texture == null || Stencil_Texture == null) continue;

        using (Mat stencilMat = OpenCvSharp.Unity.TextureToMat(Stencil_Texture))
        using (Mat rgbMat = OpenCvSharp.Unity.TextureToMat(RGB_Texture))
        using (Mat inpaintMat = new Mat())
        {
            #region stencil texture
            Cv2.CvtColor(stencilMat, stencilMat, ColorConversionCodes.BGR2GRAY);
            Cv2.Dilate(stencilMat, stencilMat, InputArray.Create(dilateArray));
            Cv2.Resize(stencilMat, stencilMat, new OpenCvSharp.Size(width, height));
            stencilViewTexture = OpenCvSharp.Unity.MatToTexture(stencilMat, stencilViewTexture);
            #endregion

            #region rgb texture
            Cv2.Resize(rgbMat, rgbMat, new OpenCvSharp.Size(width, height));
            Cv2.Flip(rgbMat, rgbMat, FlipMode.Y);
            rgbViewTexture = OpenCvSharp.Unity.MatToTexture(rgbMat, rgbViewTexture);
            #endregion

            #region inpaint
            Cv2.Inpaint(rgbMat, stencilMat, inpaintMat, 3, InpaintMethod.NS);
            inpaintViewTexture = OpenCvSharp.Unity.MatToTexture(inpaintMat, inpaintViewTexture);
            #endregion

            stencilMat.Dispose();
            rgbMat.Dispose();
            inpaintMat.Dispose();
        }
    }
}

InpaintMethodはNSとTeleaがありますが、NSの方が高速だった為NSを採用しています。

usingステートメントを使用してもMatのメモリが解放されないらしいので、加えてDispose()を読んでいます。実際はDispose()だけで良いと思います。

まとめ

AR×OpenCVは楽しい

参考

19
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18