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

コメント