離散ウェーブレット変換を使った情報圧縮・電子透かし

離散ウェーブレット変換のコードができたので、これを使っていろいろ遊んでみます。

ウェーブレット逆変換

離散ウェーブレット変換では移動平均と移動差分で値を求めていったので、その値を足したり引いたりすれば元の値が求めれます。

そこで、ウェーブレット変換して、ウェーブレット値を操作してから、ウェーブレット逆変換することで、いろいろな信号処理をすることができます。

情報圧縮

ウェーブレット値を絶対値が大きいものだけ残しても全体的な形は変わらないので、情報圧縮に使えます。
こんな感じで上位1/4だけ残して、あとはぜんぶ0にしてしまいます。

Double thirdQ = flatten.stream()
        .map(d -> Math.abs(d)).sorted()
        .skip(flatten.size() * 3 / 4).findFirst().orElse(0.);
for (int i =0; i < flatten.size(); ++i) {
    if (Math.abs(flatten.get(i)) < thirdQ) {
        flatten.set(i, 0.);
    }
}

それでも、だいたいの形は残っています。


1/8だけにすると、だいぶ形が崩れてくるけど。

ここで、聴覚や視覚の特性を利用して、聴こえてない・見えてない成分を捨てれば、MP3やJPEGのような圧縮になります。MP3やJPEGでは離散コサイン変換が使われてますが、JPEG2000ではウェーブレット変換が使われています。

電子透かし(ステガノグラフィ)

もうひとつ、ウェーブレット変換を使ってデータに情報を埋め込む電子透かし(ステガノグラフィ)をやってみます。
こんな感じで、ウェーブレット値に適当に情報を混ぜ込んでいきます。

    String msg = "Hello world";
    for (int i = 0; i < msg.length(); ++i){
        flatten.set(i * 16 + 64, msg.charAt(i) / 32_768.);
    }

こうしてウェーブレット逆変換をしても、ほとんど元のデータと違いがわからなくなります。

もういちどウェーブレット変換すれば、埋め込まれた情報が取り出せます。
これを利用して、データに電子透かしを入れたりします。

ソース

MP3の読み込みにはMP3SPIが必要なのでcom.googlecode.soundlibs:mp3spi:1.9.5.4あたりをdependencyに突っ込んでおく必要があります。

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import javax.sound.sampled.*;
import javax.swing.*;

public class DiscreteWavelet {
    public static void main(String[] args) throws Exception {
        AudioInputStream ais = AudioSystem.getAudioInputStream(new File(
                "C:\\Music\\Kiko Loureiro\\No Gravity\\"
                        //+ "07 - Tapping Into My Dark Tranquility.mp3"));
                        + "08 - Moment Of Truth.mp3"));
        AudioFormat format = ais.getFormat();
        
        AudioFormat decodedFormat = new AudioFormat(
                AudioFormat.Encoding.PCM_SIGNED,
                format.getSampleRate(),
                16,
                format.getChannels(),
                format.getFrameSize(),
                format.getFrameRate(),
                false); // little endian
        AudioInputStream decoded = AudioSystem.getAudioInputStream(decodedFormat, ais);
        
        double[] data = new double[1024];
        byte[] buf = new byte[4];
        
        // 4秒とばす(曲の冒頭はおもしろくない)
        for(int i = 0; i < decodedFormat.getSampleRate() * 4
                && decoded.read(buf, 0, buf.length) != -1; ++i) {}

        // 読み込み
        for (int i = 0; i < data.length && decoded.read(buf, 0, buf.length) != -1; ++i) {
            int left = buf[1] * 256 + buf[0];
            data[i] = left / 32_768.;
        }
        
        // 離散ウェーブレット変換
        List<List<Double>> wavlets = new ArrayList<>();
        List<Double> v = Arrays.stream(data).mapToObj(Double::valueOf)
                .collect(Collectors.toList());
        while(v.size() > 1) {
            List<Double> wavlet = new ArrayList<>();
            List<Double> next = new ArrayList<>();
            for (int i = 0; i < v.size(); i += 2) {
                wavlet.add((v.get(i) - v.get(i + 1)) / 2); // 移動差分がwavlet値
                next.add((v.get(i) + v.get(i + 1)) / 2); // 移動平均をつぎにまわす
            }
            wavlets.add(wavlet);
            v = next;
        }
        wavlets.add(v);
        
        Collections.reverse(wavlets);
        List<Double> flatten = wavlets.stream().flatMap(List::stream).collect(Collectors.toList());
        /* 電子透かし
        String msg = "Hello world";
        for (int i = 0; i < msg.length(); ++i){
            flatten.set(i * 16 + 64, msg.charAt(i) / 32_768.);
        }
        */
        /* 圧縮
        Double thirdQ = flatten.stream()
                .map(d -> Math.abs(d)).sorted()
                .skip(flatten.size() * 3 / 4).findFirst().orElse(0.);
        for (int i =0; i < flatten.size(); ++i) {
            if (Math.abs(flatten.get(i)) < thirdQ) {
                flatten.set(i, 0.);
            }
        }
        */

        // ウェーブレット逆変換
        int idx = 1;
        List<Double> invert = Collections.singletonList(flatten.get(0));
        while(idx < flatten.size()) {
            List<Double> next = new ArrayList<>();
            for (int i = 0; i < invert.size(); ++i) {
                double d1 = invert.get(i);
                double d2 = flatten.get(idx);
                next.add(d1 + d2);
                next.add(d1 - d2);
                idx++;
            }
            invert = next;
        }        
        
        // 描画
        BufferedImage image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, 600, 420);
        g.setColor(Color.BLACK);
        
        // 元データ
        GeneralPath org = new GeneralPath();
        org.moveTo(30, 100);
        for (int i = 0; i < data.length; ++i) {
            int x = i / 2 + 30;
            org.lineTo(x, -data[i] * 100 + 100);
        }
        g.draw(org);
        g.drawLine(30, 100, data.length / 2 + 30, 100);
        
        // 逆変換
        GeneralPath inv = new GeneralPath();
        inv.moveTo(30, 300);
        for (int i = 0; i < invert.size(); ++i) {
            int x = i / 2 + 30;
            inv.lineTo(x, -invert.get(i) * 100 + 300);
        }
        g.draw(inv);
        g.drawLine(30, 300, invert.size() / 2 + 30, 300);        
        
        // ウィンドウ表示
        JFrame f = new JFrame("MP3 wave");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setSize(600, 450);
        f.add(new JLabel(new ImageIcon(image)));
        f.setVisible(true);
    }
}