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

【更新履歴】

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

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

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


【概要】

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

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

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

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

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



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

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Noise Encoder 1.8</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; }
/* GIF保存ボタンのデザイン */
#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.8</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"><br>
seed:<input id="seed" type="number" value="1234"><br>
FPS:<input id="fps" type="number" value="60"><br>
強度:<input id="strength" type="number" step="0.1" value="0.3"><br><br>

<button id="encodeBtn">エンコード</button>
<button id="decodeBtn">復元</button>
<button id="playBtn">再生</button>
<button id="stopBtn">停止</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 base=null;      // 低周波
var w=0,h=0;
var idx=0;
var timer=null;

// ===== 初期化 =====
function resetAll()
{
    frames=[];
    base=null;
    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("saveGifBtn").disabled = true;
}

document.getElementById("mode").onchange = resetAll;

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;
};

// ===== blur(低周波)=====
function blur(data)
{
    var out=new Uint8ClampedArray(data.length);
    for(var y=1;y<h-1;y++)
    for(var x=1;x<w-1;x++)
    {
        var i=(y*w+x)*4;
        for(var c=0;c<3;c++)
        {
            var sum=0;
            for(var dy=-1;dy<=1;dy++)
            for(var dx=-1;dx<=1;dx++)
            {
                var j=((y+dy)*w+(x+dx))*4+c;
                sum+=data[j];
            }
            out[i+c]=sum/9;
        }
        out[i+3]=255;
    }
    return out;
}

// ===== 暗号化モード =====
function encodeSecure(data)
{
    var N=+document.getElementById("frameCount").value;
    var rand=new Random(+document.getElementById("seed").value);

    frames=[];
    base=blur(data);

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

    for(var i=0;i<data.length;i+=4)
    {
        var sumR=0,sumG=0,sumB=0;
        for(var f=0;f<N-1;f++)
        {
            var r=Math.floor(rand.next()*256);
            var g=Math.floor(rand.next()*256);
            var b=Math.floor(rand.next()*256);

            frames[f][i]=r;
            frames[f][i+1]=g;
            frames[f][i+2]=b;
            frames[f][i+3]=255;

            sumR+=r; sumG+=g; sumB+=b;
        }

        frames[N-1][i]   = (data[i] - sumR) & 255;
        frames[N-1][i+1] = (data[i+1] - sumG) & 255;
        frames[N-1][i+2] = (data[i+2] - sumB) & 255;
        frames[N-1][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);
    var currentMode = document.getElementById("mode").value;

    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];
        }

        if (currentMode === "secure") {
            res[i]   = r & 255;
            res[i+1] = g & 255;
            res[i+2] = b & 255;
        } else {
            res[i]   = clamp(Math.round(r / frames.length));
            res[i+1] = clamp(Math.round(g / frames.length));
            res[i+2] = clamp(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 strengthVal = +document.getElementById("strength").value;
    var fpsVal = +document.getElementById("fps").value;
    var currentMode = document.getElementById("mode").value;

    timer=setInterval(function(){
        drawNextPreviewFrame(currentMode, strengthVal);
    }, 1000 / fpsVal);
}

function drawNextPreviewFrame(currentMode, strengthVal) {
    var ctx=preview.getContext("2d");
    preview.width=w; preview.height=h;

    var out=new Uint8ClampedArray(w*h*4);
    var f=frames[idx];

    for(var i=0;i<out.length;i+=4)
    {
        if(currentMode === "secure")
        {
            out[i]   = clamp(base[i]   + f[i] * strengthVal);
            out[i+1] = clamp(base[i+1] + f[i+1] * strengthVal);
            out[i+2] = clamp(base[i+2] + f[i+2] * strengthVal);
        }
        else
        {
            out[i]=f[i];
            out[i+1]=f[i+1];
            out[i+2]=f[i+2];
        }
        out[i+3]=255;
    }

    var img=ctx.createImageData(w,h);
    img.data.set(out);
    ctx.putImageData(img,0,0);

    idx=(idx+1)%frames.length;
}

// ===== GIF保存 (X投稿用) =====
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 delayMs = Math.round(1000 / fpsVal); // FPSからフレーム間の待機時間(ミリ秒)を計算
    var strengthVal = +document.getElementById("strength").value;
    var currentMode = document.getElementById("mode").value;

    // WebWorkerのCORSエラーを回避するため、ワーカーコードをBlob化して読み込むハック
    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: 10, // 1が最高品質、30が低品質(ファイルサイズ小)
            workerScript: workerUrl,
            width: w,
            height: h
        });

        // 描画用のオフスクリーンキャンバスを準備
        var offscreen = document.createElement('canvas');
        offscreen.width = w; offscreen.height = h;
        var oCtx = offscreen.getContext('2d');
        var oImg = oCtx.createImageData(w, h);

        // N枚のフレーム画像を事前に生成して保存
        var precomputedCanvases = [];
        for (var i = 0; i < frames.length; i++) {
            var out = new Uint8ClampedArray(w * h * 4);
            var f = frames[i];
            for (var j = 0; j < out.length; j += 4) {
                if(currentMode === "secure") {
                    out[j]   = clamp(base[j]   + f[j] * strengthVal);
                    out[j+1] = clamp(base[j+1] + f[j+1] * strengthVal);
                    out[j+2] = clamp(base[j+2] + f[j+2] * strengthVal);
                } else {
                    out[j] = f[j]; out[j+1] = f[j+1]; out[j+2] = f[j+2];
                }
                out[j+3] = 255;
            }
            oImg.data.set(out);
            
            // gif.jsに渡すために新しいキャンバスにコピー
            var frameCanvas = document.createElement('canvas');
            frameCanvas.width = w; frameCanvas.height = h;
            frameCanvas.getContext('2d').putImageData(oImg, 0, 0);
            precomputedCanvases.push(frameCanvas);
        }

        // X(Twitter)で確実にループさせるため、約2秒間になるようにフレームを繰り返す
        var targetTotalFrames = 2 * fpsVal; // 例: 60fpsなら合計120フレーム
        var loops = Math.ceil(targetTotalFrames / frames.length);

        for (var r = 0; r < loops; r++) {
            for (var i = 0; i < precomputedCanvases.length; i++) {
                // フレームを追加(delayで速度指定)
                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;
    });
};

// ===== UIイベント =====
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);
    };
    img.src=URL.createObjectURL(e.target.files[0]);
};

document.getElementById("encodeBtn").onclick=function()
{
    if(w===0) return;
    resetAll();

    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("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); }

</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