読者です 読者をやめる 読者になる 読者になる

画像の見た目の変化を抑えつつ任意の二値画像データを埋め込む

この記事はぱくとま Advent Calendar 2016の10日目の記事です。

はじめに

まずはじめに断っておくが、私はぱくとま氏について何も知らないしツイッターのアカウントもフォローしていないという状態である。そんな私が何故このアドベントカレンダーに寄稿するに至ったかというと、実は数日前にこんなやりとりがあった。

寄稿に至るツイッターバトル

遡ること4日、私(_96N_)に対してpotato4d氏によりこのアドベントカレンダーに寄稿するよう持ちかけられた。

唐突である。
本人について何も知らない人間にいきなりアドベントカレンダーを書くよう要求する行為は違法だと思う。

人間性を盾に反撃する私。

何でも貫く矛で突き刺してくるpotato4d氏。

破壊された人間性。

なんて話しているうちに本アドベントカレンダーに寄稿することになったわけだが、生憎私はぱくとま氏について何も知らないので書くことが無い。参考にしようと思ってアドベントカレンダーを遡り記事を読んでみたが、九割九分九厘ぱくとま氏をdisるだけの記事で参考にならないし涙を禁じ得ない。リアルで知り合いのowl_8は「記事のどこかに'ぱくとま'って入ってたらどんなこと書いてもいいよ」なんて言っていたのできっと適当なのだろう。大体potato4d氏が全部悪い。なので今回はぱくとま氏とはほぼ無関係の記事になっている。許して欲しい。

画像にデータを埋め込むということだが難しいことは特にないので気楽に読んでいただけると幸甚である。


画像の見た目を変えずにデータを埋め込むよ

今回の方法では濃淡画像の8bitのLSB(Least Significant Bit)、つまり最下位ビットに二値画像を埋め込む。具体的に言うと、濃淡画像の最下位ビットの画素値を二値画像の画素値で置き換える。話を簡単にするために今回は濃淡画像を用いるが、考え方はフルカラー画像でも同じである。まずは以下の2枚の画像を見て欲しい。


f:id:cool_on:20161210005347p:plainf:id:cool_on:20161210005350p:plain


一見何ら違いは無い絵である。
ところが、実は右側の画像には以下のような二値画像のデータが埋め込まれている。
f:id:cool_on:20161210005658p:plain
画像見比べてもわからんぞ!と思われるかもしれないが、次に述べる簡単なトリックがある。

濃淡画像に二値画像を埋め込む

まず濃淡画像と二値画像について軽く触れておく。濃淡画像とは1画素あたり256階調(0〜255)で表現されるグレースケールの画像であり、1画素で8bitを要する。二値画像とは1画素あたり2階調(任意の二色を0,1に割り当てる、一般的に白と黒)で表現される画像であり、1画素で1bitを要する。二値画像は任意の二色から表現されるが本質的には0と1のみのデータで、出力時に0と1に対する色の割当を行っているだけである。

次の二枚は私の大好きな日野茜ちゃんと、そこから取り出した最下位ビットの二値を白と黒に割り当てた二値画像である。

f:id:cool_on:20161210014130p:plain f:id:cool_on:20161210014133p:plain

見ていただくと分かると思うが、最下位ビットはほぼノイズでしか無い。つまり最下位ビットは元画像に対してほとんど影響を与えないということである。実際、最下位ビットは画素値に対して±1程度の影響しか与えないので変化させても肉眼では捉えられない。また、最下位ビットが元画像にほとんど影響を与えないということは、最下位ビットに任意のデータを与えても元画像はほとんど変化しないということである。これが最下位ビットにデータを埋め込む理由である。

それでは種明かしも終えたので実際に埋め込んでいこう。前提として濃淡画像と二値画像の画素数は同じであるとする。手順的には

  • 埋め込み先の濃淡画像の最下位ビット成分を除去する(各画素の最下位ビットの値を0にする)
  • 埋め込みたい二値画像の各画素の値をハイディング先の濃淡画像の対応する画素に加算する。

たったこれだけである。最下位ビットを書き換えてしまうのでこの方法は非可逆である点だけ注意したい。
以下はPython2.7とOpenCV2.4.9を用いて実際に埋め込むコードである。

#!/usr/bin/env
# coding:utf-8

import cv2
import numpy as np

#濃淡画像、二値画像の読み込み
img = cv2.imread('pakutoma.png', 0)  #グレースケールで読み込む
img1 = cv2.imread('kuso.png')  #0を0に、1を255に割り当てて表現することで擬似的に二値画像として扱っている。

#まずは濃淡画像から最下位ビットの成分を除去
for col in img:
  for comp in col:
    if comp%2:
      comp -= 1

#次に濃淡画像の最下位ビットに二値画像の画素値を加算
for col, col1 in zip(img, img1):
  for  comp, comp1 in zip(col, col1):
    if comp1!=0:
      comp += 1

cv2.imwrite('result.png', img)


濃淡画像pakutoma.pngに二値画像kuso.pngを埋め込んでいる。
下の左の濃淡画像に右の二値画像を埋め込んだ形となる。


f:id:cool_on:20161210024826p:plain f:id:cool_on:20161210024842p:plain


このようにしてデータを埋めこんだ画像データがたまにCTFの問題として与えられるので頭の隅に入れておくとよい。仕組み上画像ではなく任意のデータを二進数に変換し行列に並べたものを埋め込むこともできる。データの取り出しは最下位ビットから二値画像を作るだけで良い。


おわりに

全く知らない人に挟まれて記事を書くという行為には厳しいものがある。先人達がひたすら「ぱくとまはクソ」と仰ってたのでそこだけ使わせて貰ったが、どうか許して欲しい。この記事を書いてる途中、3秒に一回くらいのペースで日本語の勉強をやり直したい衝動に駆られた。

次回はcueikusuta氏のようです。是非とも濃厚なぱくとま氏の記事にしてあげて欲しい。

ぱくとまはクソ。