学習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>

コメント