学習AIから画像を保護する「NoiseEncoder」

【更新履歴】

 ・2026/3/24 バージョン1.0公開。
          (中略)
 ・2026/3/24 バージョン1.9公開。

※1.7以降は、テストブラウザ専用です。↓

※1.8以降は、オフラインでは動作しません。
 (これはGif生成ライブラリを参照しているためです。)


【概要】

・自分で描いた画像を公開したいけど、
 AIに学習されるのはイヤだ!
 という場合があると思います。

・このツールでは、
 加工したアニメpngなどを生成して、
 クローリングや、スクレーピングから
 あなたの画像を保護します。

・フレームごとの画像は、学習しにくいように
 特殊な加工が施されています。 

画像
暗号化モード
画像
公開モード

サンプル出力はこちら。(webM)↓

・Gifだと、フィルタが強すぎて見えにくい。。
・webM形式は、noteでもXでも対応していません。


・ダウンロードされる方はこちら。↓


1. コア技術:時間的暗号化(Temporal Multiplexing)と残像現象


・このツールの基礎となるのは、
 人間の目の「残像現象(Persistence of Vision: PoV)」です。

・画像をN枚のフレームに分割し、
 それぞれに強烈な「相補的ノイズ
 (足し合わせるとプラスマイナスゼロになるノイズ)」を付与します。

・これを50FPS以上の高速で切り替えて再生すると、
 人間の網膜上でノイズが平均化(相殺)され、
 グレーのフィルターの奥に元の画像が鮮明に浮かび上がります。

・一方、AIのクローラーは動画を
 「静止画の連続」として1フレームずつ処理するため、
 抽出されるのはノイズで完全に破壊された画像データのみとなり、
 元画像の構図や色彩の学習を物理的に不可能にします。

2. AIを完全に欺く「高度構造的ノイズ(Adversarial Noise)」


・単純な砂嵐(ランダムノイズ)は、
 現代のAI(Stable Diffusionなど)にとっては
 最も除去が得意な対象です。

・そこで本ツールでは、
 AIが「意味のある模様」として誤認し、
 除去できなくなる『敵対的構造ノイズ』を
 動的に生成・合成しています。

空間的破壊(チェッカーボード&モアレ):

・ピクセルを2x2のブロック単位に分割し、
 フレームごとにチェッカーボード状の間引きや
 波紋状のモアレ模様を発生させます。

・AIはこれを「画像本来の輪郭線やテクスチャ」だと誤認し、
 学習データが致命的に汚染されます。

色彩情報の破壊(動的色相シフト):

・各フレームのRGBチャンネルごとに位相を
 60度ズラしたカラーノイズを乗せています。

・人間が高速で見た際には
 本来の肌色や風景の色に平均化されますが、
 AIが1フレームを抜き出すと「白飛び」「黒潰れ」
 「極端な赤やシアンへの変色」が起きており、
 正しいカラーパレットの抽出を阻止します。

3. 数学的に証明された「100%の完全復元性(Zero-Sum)」


・本ツールの最大のブレイクスルーは、
 どれほど強烈なノイズを乗せても、
 「N枚のフレームのピクセル値を単純平均するだけで、
 1ピクセルの狂いもなく100%元画像に復元できる」
 という数学的完全性を保証している点です。

・小数点以下の計算で生じる丸め誤差や、
 0未満・255超過によるクリッピング(色潰れ)を回避するため、
 独自の「限界スケール算出」と
 「誤差分配ロジック」を搭載。
 
・これにより、暗号化と復号の間で
 画質の劣化(ロス)が一切発生しません。

4. その他


クリアな視覚体験チューニング:

・ノイズの空間周波数や振幅を緻密に調整し、
 液晶ディスプレイ特有の「色の濁り」を抑制。

SNSネイティブな出力:

・X(旧Twitter)などでのFPS制限や
 色潰れを回避する安全装置を搭載し、
 最適なWebMおよび最高品質のディザリングGIFとして
 ワンクリックでエクスポート可能です。


・ソースコードはこちら。↓

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Noise Encoder 1.9</title>

<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>

<style>
body { font-family: sans-serif; background-color: #f9f9f9; padding: 10px; }
h2 { margin-top: 0; }
#row { 
    display: flex; 
    flex-wrap: wrap; 
    gap: 15px; 
    align-items: flex-start; 
}
.panel { 
    display: flex; 
    flex-direction: column; 
    align-items: center; 
    flex: 1; 
    min-width: 200px; 
    background: #fff;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
}
.panel b { margin-bottom: 10px; }
.controls { margin-top: 10px; } 
canvas { 
    border: 1px solid #ccc; 
    width: 100%; 
    height: auto; 
    max-width: 350px; 
    background: #eee;
}
input, button, select { margin-bottom: 5px; }

#saveWebmBtn { background-color: #4CAF50; color: white; border: none; padding: 5px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; }
#saveWebmBtn:hover { background-color: #45a049; }
#saveWebmBtn:disabled { background-color: #aaa; cursor: not-allowed; }

#saveGifBtn { background-color: #1DA1F2; color: white; border: none; padding: 5px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; }
#saveGifBtn:hover { background-color: #0c85d0; }
#saveGifBtn:disabled { background-color: #aaa; cursor: not-allowed; }
</style>

</head>
<body>

<h2>Noise Encoder 1.9</h2>

<input type="file" id="fileInput"><br><br>

方式:
<select id="mode">
<option value="secure">暗号化モード (高度構造的ノイズ・高クリア版)</option>
<option value="public">公開モード</option>
</select><br>

フレーム数:<input id="frameCount" type="number" value="8" min="2" max="20"><br>
seed:<input id="seed" type="number" value="1234"><br>
FPS:<input id="fps" type="number" value="50" min="1" max="60"> (※GIFの限界は実質50FPSです)<br>
強度:<input id="strength" type="range" min="0.1" max="2.0" step="0.1" value="0.8"> <span id="strVal">0.8</span><br><br>

<button id="encodeBtn">エンコード</button>
<button id="decodeBtn">復元</button>
<button id="playBtn">再生</button>
<button id="stopBtn">停止</button>
<button id="saveWebmBtn" disabled>WebMで保存</button>
<button id="saveGifBtn" disabled>GIFで保存 (X投稿用)</button>

<hr>

<div id="row">

    <div class="panel">
        <b>元画像</b>
        <canvas id="original"></canvas>
    </div>

    <div class="panel">
        <b>フレーム</b>
        <canvas id="frameView"></canvas>
        <div class="controls">
            <button onclick="prev()">前</button>
            <button onclick="next()">次</button>
            <span id="frameIndex"></span>
        </div>
    </div>

    <div class="panel">
        <b>再生</b>
        <canvas id="preview"></canvas>
    </div>

    <div class="panel">
        <b>復元</b>
        <canvas id="decoded"></canvas>
    </div>

</div>

<script>

// ===== 状態 =====
var frames=[];
var w=0,h=0;
var idx=0;
var timer=null;

// ===== 初期化 =====
function resetAll()
{
    frames=[];
    idx=0;
    clearInterval(timer);

    [frameView, preview, decoded].forEach(c=>{
        var ctx=c.getContext("2d");
        ctx.clearRect(0,0,c.width,c.height);
    });
    document.getElementById("frameIndex").innerText = "";
    document.getElementById("saveWebmBtn").disabled = true;
    document.getElementById("saveGifBtn").disabled = true;
}

function clamp(v){ return Math.max(0,Math.min(255,v)); }

function Random(s){this.s=s;}
Random.prototype.next=function(){
    this.s=(this.s*1664525+1013904223)>>>0;
    return this.s/4294967296;
};

// ===== 究極暗号化モード (見やすさ大幅改善版) =====
function encodeSecure(data)
{
    var N = +document.getElementById("frameCount").value;
    var rand = new Random(+document.getElementById("seed").value);
    var strengthVal = +document.getElementById("strength").value;

    frames = [];
    for(var f=0; f<N; f++) {
        frames[f] = new Uint8ClampedArray(data.length);
    }

    for(var i=0; i<data.length; i+=4)
    {
        var x = (i / 4) % w;
        var y = Math.floor((i / 4) / w);

        // 【改善点1】視覚的濁りを防ぐため、ノイズの空間単位を2x2ピクセルのブロックにする
        // これにより液晶やGIF圧縮時の隣接ピクセルの色混ざりを防ぐ
        var bx = Math.floor(x / 2);
        var by = Math.floor(y / 2);

        for (var c = 0; c < 3; c++) {
            var V = data[i + c];
            var O = [];
            var sumO = 0;

            for (var j = 0; j < N; j++) {
                // 【改善点2】波長を長くし、モアレのチラつきを滑らかにする
                var spatial1 = Math.sin(bx * 0.2 + j * 1.3) * Math.cos(by * 0.2 - j * 1.7);
                var spatial2 = Math.sin(bx * 0.05 - j * 0.8) * Math.cos(by * 0.05 + j * 1.1);
                
                // 2x2単位のチェッカーボードノイズ
                var checker = ((bx + by + j) % 2 === 0 ? 1 : -1) * 0.8; 
                
                // 【改善点3】色相シフトのズレ幅を120度から60度(1.047)に狭める。
                // これにより、AIには狂った色に見えるが、人間が平均化した際にグレーに濁る(彩度が落ちる)のを防ぐ
                var colorPhase = Math.sin(j * 2.3 + c * 1.047); 
                
                var randNoise = (rand.next() - 0.5) * 1.5; 

                // 振幅のベース係数を抑え、全体的なクリアさを確保
                var val = (spatial1 + spatial2 + checker + colorPhase + randNoise) * 60 * strengthVal;
                O.push(val);
                sumO += val;
            }

            var meanO = sumO / N;
            for (var j = 0; j < N; j++) O[j] -= meanO;

            var S = 1.0;
            for (var j = 0; j < N; j++) {
                var target = V + O[j];
                if (target > 255) S = Math.min(S, (255 - V) / O[j]);
                else if (target < 0) S = Math.min(S, (0 - V) / O[j]);
            }

            var frameVals = [];
            var sumFrameVals = 0;
            for (var j = 0; j < N; j++) {
                var finalVal = Math.round(V + O[j] * S);
                frameVals.push(finalVal);
                sumFrameVals += finalVal;
            }

            // 完全復元の為の丸め誤差補正
            var error = (V * N) - sumFrameVals;
            var attempts = 0;
            while (error !== 0 && attempts < 10) {
                for (var j = 0; j < N && error !== 0; j++) {
                    if (error > 0 && frameVals[j] < 255) { frameVals[j]++; error--; }
                    else if (error < 0 && frameVals[j] > 0) { frameVals[j]--; error++; }
                }
                attempts++;
            }

            for (var j = 0; j < N; j++) {
                frames[j][i + c] = frameVals[j];
            }
        }
        for (var j = 0; j < N; j++) frames[j][i + 3] = 255;
    }
}

// ===== 公開モード =====
function encodePublic(data)
{
    var N=+document.getElementById("frameCount").value;
    var rand=new Random(+document.getElementById("seed").value);

    frames=[];
    for(var f=0;f<N;f++) {
        var frame=new Uint8ClampedArray(data.length);
        for(var i=0;i<data.length;i+=4) {
            var n=(rand.next()-0.5)*80;
            frame[i]=clamp(data[i]+n);
            frame[i+1]=clamp(data[i+1]+n);
            frame[i+2]=clamp(data[i+2]+n);
            frame[i+3]=255;
        }
        frames.push(frame);
    }
}

// ===== 復元 =====
function decode()
{
    if(!frames.length) return;
    var res=new Uint8ClampedArray(frames[0].length);

    for(var i=0;i<res.length;i+=4)
    {
        var r=0,g=0,b=0;
        for(var f=0;f<frames.length;f++) {
            r+=frames[f][i];
            g+=frames[f][i+1];
            b+=frames[f][i+2];
        }
        res[i]   = Math.round(r / frames.length);
        res[i+1] = Math.round(g / frames.length);
        res[i+2] = Math.round(b / frames.length);
        res[i+3] = 255;
    }

    var ctx=decoded.getContext("2d");
    decoded.width=w; decoded.height=h;
    var img=ctx.createImageData(w,h);
    img.data.set(res);
    ctx.putImageData(img,0,0);
}

// ===== 表示 =====
function showFrame(i)
{
    if(!frames.length) return;
    var ctx=frameView.getContext("2d");
    frameView.width=w; frameView.height=h;
    var img=ctx.createImageData(w,h);
    img.data.set(frames[i]);
    ctx.putImageData(img,0,0);
    document.getElementById("frameIndex").innerText=(i+1)+"/"+frames.length;
}

// ===== 再生 =====
function play()
{
    if(!frames.length) return;
    clearInterval(timer);
    var fpsVal = +document.getElementById("fps").value;
    timer=setInterval(drawNextPreviewFrame, 1000 / fpsVal);
}

function drawNextPreviewFrame() {
    var ctx=preview.getContext("2d");
    preview.width=w; preview.height=h;
    var img=ctx.createImageData(w,h);
    img.data.set(frames[idx]);
    ctx.putImageData(img,0,0);
    idx=(idx+1)%frames.length;
}

// ===== UX改善: パラメータ変更時に自動で再エンコード/再生 =====
function triggerEncode() {
    if(w>0) {
        document.getElementById("encodeBtn").click();
        document.getElementById("playBtn").click();
    }
}
document.getElementById("mode").onchange = function() { resetAll(); triggerEncode(); };
document.getElementById("frameCount").onchange = triggerEncode;
document.getElementById("seed").onchange = triggerEncode;

document.getElementById("strength").oninput = function() {
    document.getElementById("strVal").innerText = this.value;
};
document.getElementById("strength").onchange = triggerEncode;

document.getElementById("fps").onchange = function() {
    if(w>0) document.getElementById("playBtn").click();
};

// ===== ボタンイベント =====
document.getElementById("fileInput").onchange=function(e)
{
    resetAll();
    if (!e.target.files.length) return;

    var img=new Image();
    img.onload=function(){
        w=img.width; h=img.height;
        var ctx=original.getContext("2d");
        original.width=w; original.height=h;
        ctx.drawImage(img,0,0);
        triggerEncode(); 
    };
    img.src=URL.createObjectURL(e.target.files[0]);
};

document.getElementById("encodeBtn").onclick=function()
{
    if(w===0) return;
    var ctx=original.getContext("2d");
    var data=ctx.getImageData(0,0,w,h).data;

    if(document.getElementById("mode").value==="secure") encodeSecure(data);
    else encodePublic(data);

    showFrame(0);
    document.getElementById("saveWebmBtn").disabled = false;
    document.getElementById("saveGifBtn").disabled = false;
};

document.getElementById("decodeBtn").onclick=decode;
document.getElementById("playBtn").onclick=play;
document.getElementById("stopBtn").onclick=function(){ clearInterval(timer); };

function prev(){ idx=(idx-1+frames.length)%frames.length; showFrame(idx); }
function next(){ idx=(idx+1)%frames.length; showFrame(idx); }

// ===== WebM録画・保存 =====
document.getElementById("saveWebmBtn").onclick = function() {
    if(!frames.length) return;
    clearInterval(timer);
    
    var fpsVal = +document.getElementById("fps").value;
    var currentMode = document.getElementById("mode").value;
    var stream = preview.captureStream(fpsVal);
    var recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
    var chunks = [];
    
    recorder.ondataavailable = function(e) { if (e.data.size > 0) chunks.push(e.data); };
    recorder.onstop = function() {
        var blob = new Blob(chunks, { type: 'video/webm' });
        var url = URL.createObjectURL(blob);
        var a = document.createElement("a");
        a.href = url;
        a.download = "encoded_noise_" + currentMode + ".webm";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        
        var btn = document.getElementById("saveWebmBtn");
        btn.innerText = "WebMで保存";
        btn.disabled = false;
        alert("WebMの保存が完了しました。");
    };
    
    var btn = document.getElementById("saveWebmBtn");
    btn.innerText = "録画中...";
    btn.disabled = true;
    
    recorder.start();
    timer = setInterval(drawNextPreviewFrame, 1000 / Math.min(fpsVal, 60));
    setTimeout(function() { clearInterval(timer); recorder.stop(); }, 3000);
};

// ===== GIF保存 (最高品質・FPS安全化) =====
document.getElementById("saveGifBtn").onclick = function() {
    if(!frames.length) return;
    
    var btn = this;
    btn.innerText = "エンコーダ読込中...";
    btn.disabled = true;
    clearInterval(timer);

    var fpsVal = +document.getElementById("fps").value;
    var currentMode = document.getElementById("mode").value;

    var safeGifFps = Math.min(fpsVal, 50); 
    var delayMs = Math.round(1000 / safeGifFps); 

    fetch('https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js')
    .then(response => response.text())
    .then(workerText => {
        var blob = new Blob([workerText], {type: 'application/javascript'});
        var workerUrl = URL.createObjectURL(blob);

        var gif = new GIF({
            workers: 2,
            quality: 1, 
            dither: 'FloydSteinberg',
            workerScript: workerUrl,
            width: w,
            height: h
        });

        var precomputedCanvases = [];
        for (var i = 0; i < frames.length; i++) {
            var frameCanvas = document.createElement('canvas');
            frameCanvas.width = w; frameCanvas.height = h;
            var oCtx = frameCanvas.getContext('2d');
            var oImg = oCtx.createImageData(w, h);
            oImg.data.set(frames[i]);
            oCtx.putImageData(oImg, 0, 0);
            precomputedCanvases.push(frameCanvas);
        }

        var targetTotalFrames = 2 * safeGifFps;
        var loops = Math.ceil(targetTotalFrames / frames.length);

        for (var r = 0; r < loops; r++) {
            for (var i = 0; i < precomputedCanvases.length; i++) {
                gif.addFrame(precomputedCanvases[i], {delay: delayMs, copy: true});
            }
        }

        gif.on('progress', function(p) {
            btn.innerText = "生成中... " + Math.round(p * 100) + "%";
        });

        gif.on('finished', function(blob) {
            var url = URL.createObjectURL(blob);
            var a = document.createElement("a");
            a.href = url;
            a.download = "noise_encoded_" + currentMode + ".gif";
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);

            btn.innerText = "GIFで保存 (X投稿用)";
            btn.disabled = false;
        });

        gif.render(); 
    })
    .catch(err => {
        console.error(err);
        alert("GIFエンコーダの読み込みに失敗しました。インターネット接続を確認してください。");
        btn.innerText = "GIFで保存 (X投稿用)";
        btn.disabled = false;
    });
};

</script>

</body>
</html>


いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
学習AIから画像を保護する「NoiseEncoder」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1