この記事はCyberAgent Developers Advent Calendar 2021 13日目の記事です。

AI事業本部 ロボットサービス事業部の西( @kohtaro24 )です。

普段はコミュニケーションロボットを活用したプロダクト開発などに取り組んでいますが、弊社のゼミ制度を利用してHCIゼミメンバーとしても活動しており、今回はその活動の中で得られた知見をご紹介します。

まずはこちらの映像を御覧ください。

 

ディスプレイを左から見たときには男性のアバター、右から見たときには女性のアバターが見えています。

レンチキュラーか?」と思われた方もいらっしゃるかもしれませんが、これは何の加工もしていない一般的なディスプレイです。液晶ディスプレイが元々持っている”ある特性”を利用しています。

液晶ディスプレイの特性

一般的に流通している液晶ディスプレイにはTN(Twisted Nematic)、VA(Vertical Alignment)、IPS(In-Plane-Switching)などの駆動方式が採用されており、上の映像で使用していたのはTN方式のディスプレイです。

TN方式のディスプレイは安価で消費電力も低く一般への普及率も高いのですが、覗き込む角度によって映像のコントラストが変化してしまう特性があります。これはTwisted Nematicという名の通り液晶分子のねじれを利用して映像を映しているためのようです。一昔前はテレビなどでも広く採用されていたので、ご存じの方も多いかもしれません。

TN液晶の弱点と見なされがちなこの特性ですが、これを逆手にとると先に紹介した映像のようなものを作れます。以下でやり方を解説していきます。

※手法の大部分はEnabling Concurrent Dual Views on Common LCD Screens(Microsoft Research, CHI 2012)で紹介されたものです。

 

2つの視野角のコントラスト差を計測する

ある角度から覗き込んだときの色の見え方(A)と、反対側の角度から覗き込んだときの色の見え方(B)をそれぞれ確認します。鏡を使うのが手っ取り早くてよいと思います。

AとBで見られるコントラストは非対称的であり、Aでは同じ色に見えていたものがBでは異なる色に見えることが起こり得ます。

例えば以下の映像では少し薄い赤をディスプレイに表示していますが、ディスプレイに見える映像はほぼ同じ濃さであるのに対して、鏡越しに見える映像では異なる濃さに見えることがわかります。

こんな感じで、RGBごとに鏡越しに見た時のみ濃さが異なって見える組み合わせ(min, maxのペア)を確認していきます。このmin, maxの幅はできるだけ大きいほうが良いです。このようなカラースライダーを用意すると便利でしょう。


 

私の例では以下のようになりました。

  • R=(195, 225)
  • G=(172, 204)
  • B=(223, 255)

試しに単色のスケール画像を作ってみましょう。

import os
import cv2
import numpy as np

color = 'r'
color_min = 195
color_max = 225
jpg_image_path = 'original_image.jpg'

fig = cv2.imread(jpg_image_path)
height, width, channels = fig.shape[:3]
img = np.zeros((height, width, channels))

original_max_grayscale_pix = None
original_min_grayscale_pix = None

for row in range(img.shape[0]):
    for col in range(img.shape[1]):
        rgb_pix = fig[row][col]
        grayscale_pix = sum(rgb_pix) / 3
        if original_max_grayscale_pix is None or original_max_grayscale_pix < grayscale_pix:
            original_max_grayscale_pix = grayscale_pix
        if original_min_grayscale_pix is None or original_min_grayscale_pix > grayscale_pix:
            original_min_grayscale_pix = grayscale_pix

for row in range(img.shape[0]):
    for col in range(img.shape[1]):
        rgb_pix = fig[row][col]
        grayscale_pix = sum(rgb_pix) / 3
        render_pix = color_min + (grayscale_pix - original_min_grayscale_pix) * (color_max - color_min) / (
                    original_max_grayscale_pix - original_min_grayscale_pix)
        if color == 'r':
            img[row][col] = (0, 0, render_pix)
        elif color == 'g':
            img[row][col] = (0, render_pix, 0)
        elif color == 'b':
            img[row][col] = (render_pix, 0, 0)

cv2.imwrite(f"images/dist_{color}_{color_min}_{color_max}.jpg", img)

雑なコードでアレですが、オリジナル画像を一度グレースケール変換して本来のピクセルの濃度範囲を取得し、指定した色の濃度範囲(195~225)に線形変換しています。

生成された単色スケール画像をTN液晶で確認してみましょう。

なんかちょっとコワイですが、片方の角度(鏡)からしか見えない映像が出来上がっていますね!

めでたしめでたしと言いたいところですが、やはり単色ではないカラー画像を作りたいですよね。

計測した色を組み合わせて8色カラー画像を生成する

RGBそれぞれで計測した色の濃度範囲を組み合わせて、8色のカラーパレットを作ります。

class ColorPairs:
    r_min = 195
    r_max = 225
    g_min = 172
    g_max = 204
    b_min = 223
    b_max = 255
def put_color_palette(color_pairs: ColorPairs):
    color_length = 8
    height = 1
    width = color_length
    channels = 3

    img = np.zeros((height, width, channels))

    img[0][0] = (color_pairs.b_min, color_pairs.g_min, color_pairs.r_min)
    img[0][1] = (color_pairs.b_min, color_pairs.g_min, color_pairs.r_max)
    img[0][2] = (color_pairs.b_min, color_pairs.g_max, color_pairs.r_min)
    img[0][3] = (color_pairs.b_min, color_pairs.g_max, color_pairs.r_max)
    img[0][4] = (color_pairs.b_max, color_pairs.g_min, color_pairs.r_min)
    img[0][5] = (color_pairs.b_max, color_pairs.g_min, color_pairs.r_max)
    img[0][6] = (color_pairs.b_max, color_pairs.g_max, color_pairs.r_min)
    img[0][7] = (color_pairs.b_max, color_pairs.g_max, color_pairs.r_max)

    cv2.imwrite('color_palette.bmp', img)

これで生成されたパレットがこちら。8色の組み合わせができていますね。

この8色のパレットを使ってカラーマッピング(ディザリング)画像を作ります。ImageMagickの-mapを使うと簡単に実現でき、-ditherオプションにFloydSteinbergを指定することで、8色しかない色の階調を滑らかに表現してくれます。

convert ./original_image.jpg -dither FloydSteinberg -map color_palette.bmp ./8_mapped_image.jpg

生成された画像を見てみましょう。

ちゃんとカラーになっていて、しかも片方の角度からしか見えないままになっています!

応用編①:反対側の視野角にも別の画像を表示する

最初に紹介した映像では、もう片方の視野角からも別の画像が見えていましたね。

これをやるのは簡単で、今度は逆にディスプレイ越しに見た時のみ濃さが異なって見える組み合わせ(min, maxのペア)を探し、今までの手順に習います。

私の例では以下のようになりました。

  • R=(1, 106)
  • G=(1, 109)
  • B=(1, 135)

この組み合わせを使って再度8色カラー画像を作成したら、先程用意したもう一つの視野角の画像の上に重ね、2つの画像を高速に交互に出し分け続けます。そうすることで、人間の目からは2つの画像が重なって見えるようになります。ですが一方の視野角から見た時もう一方の視野角の画像は薄くて見えないため、結果として片方の画像のみが鮮明に見える、という仕組みです。

<html>
<head>
    ...
    <style>
        .hide {
            opacity: 0;
        }
    </style>
</head>
<body>
<div class="container-fluid">
    <div class="row justify-content-around">
        <img id="figA" src="images/8_woman_r_195_225_g_172_204_b_223_255.jpg"
             style="width: 100%; transform:rotate(90deg);"/>
        <img id="figB" src="images/8_man_r_1_106_g_1_109_b_1_135.jpg"
             style="width: 100%; transform:rotate(90deg); position: absolute"/>
    </div>
</div>
<script>
    const target = $('#figB');

    switchingTimer(target);

    function switchingTimer(target) {
        if (target.hasClass('hide')) {
            target.removeClass('hide');
        } else {
            target.addClass('hide');
        }
        setTimeout("switchingTimer(target)", 16); // 60fps
    }
</script>
</body>
</html>

カメラでも撮ることができます。

応用編②:アプリケーションを作る

今回紹介したトリックはディスプレイを介するコンテンツ全般に適用できるので、一方向からしか見えないアニメーションやアプリケーションのUIを作ることもできます。

2021年12月に開催されたWISS2021にて、以下のようなアプリケーションをお披露目しました。

 

題して「ディスプレイ1つで2人を同時に接客できちゃうバーチャル接客システム」です。双方向のアニメーション表示に加え、立ち位置のセンシングと指向性スピーカー(特定の方向にだけ音を聞かせる技術)を組み合わせています。

これによって映像のみならず音声までも視聴者の立ち位置によって同時に出し分けることが可能になります。夢が広がりますね。

 

まとめと課題

TN液晶の特性を逆手に取った「視野角で変化する映像の作り方」をご紹介しました。

個人的にはローテクノロジーの使いみちの再発見という感じで非常に面白いと感じてるのですが、課題は色々あります。

①使える色が限定されてしまう

カラー画像を作れるとはいえ、やはり8色だと細かな表現は厳しくなってきます。オリジナルの画像の色配分によっては綺麗にディザリングできず色抜けがひどかったり、反対側からも見えてしまったりします。オリジナル画像の配色を調整する→ディザリングを試す→液晶に映して確認という反復的なチューニング作業が発生しがちです…

②(双方向に画像を表示する場合)映像がチカチカする

高速で交互に画像を出し続けてるのでそりゃそうなるよねという感じですが、一応チカチカを行わない代替策として重ねる方の画像のピクセルを格子状に透過させるなどのやり方があります。リアルタイムに変化するコンテンツでやろうとすると大変そうだったので今回は試しませんでした。

という感じで色々難しさもありますが、とても面白いハックだと思うので皆さんも家にTN液晶があれば試してみてください!

2015年に北陸先端科学技術大学院大学 知識科学研究科を修了。株式会社Speeeにてエンジニアとして事業開発に従事したのち2019年にサイバーエージェントへ中途入社。 普段はロボットサービス事業部のテックリードとして、遠隔ビデオ通話システム/人物トラッキングエンジン/ロボットの対話フロー制御基盤/ロボットの動作シミュレーターの開発など、マルチスタックに活動中。