画像の色覚シミュレーション改良版
JavaScript
/* VisionSimulator */ /* Copyright (c) 2018 Kazunori Asada */ /* Released under the MIT license */ /* https://github.com/asada0/ChromaticVisionSimulator/blob/master/LICENSE.txt */ /* */色覚シミュレーションツールVisionSimulator
基準となる色を選択してください:
通常の色
プロタノピア
デューテラノピア
トリタノピア
画像の色覚シミュレーション
画像を読み込む:
シミュレーション結果のサムネイル
オリジナル
プロタノピア
デューテラノピア
トリタノピア
拡大表示する画像を選択:
拡大表示
プログラム
/* VisionSimulator */
/* Copyright (c) 2018 Kazunori Asada */
/* Released under the MIT license */
/* https://github.com/asada0/ChromaticVisionSimulator/blob/master/LICENSE.txt */
/* */
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>色覚シミュレーションツールVisionSimulator</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 2rem;
}
/* 色ブロック用スタイル */
.color-display {
width: 100px;
height: 100px;
border: 1px solid #ccc;
text-align: center;
line-height: 100px;
font-weight: bold;
}
.section {
margin-bottom: 2rem;
}
/* 色選択部分の横並び */
.color-container {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.color-item {
text-align: center;
}
/* サムネイル用キャンバスの横並び */
.canvas-container {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.canvas-item {
text-align: center;
}
/* サムネイルとして表示するキャンバスは横幅を固定し、高さはアスペクト比維持 */
.thumbnail-canvas {
width: 150px;
height: auto;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<h1>色覚シミュレーションツールVisionSimulator</h1>
<!-- カラーピッカーによる色シミュレーション -->
<div class="section">
<p>基準となる色を選択してください:</p>
<input type="color" id="colorPicker" value="#ff0000">
</div>
<!-- 色シミュレーション結果(色ブロック) -->
<div class="section color-container">
<div class="color-item">
<p>通常の色</p>
<div id="normalColor" class="color-display"></div>
</div>
<div class="color-item">
<p>プロタノピア</p>
<div id="protanopiaColor" class="color-display"></div>
</div>
<div class="color-item">
<p>デューテラノピア</p>
<div id="deuteranopiaColor" class="color-display"></div>
</div>
<div class="color-item">
<p>トリタノピア</p>
<div id="tritanopiaColor" class="color-display"></div>
</div>
</div>
<!-- 画像のシミュレーション -->
<div class="section">
<h2>画像の色覚シミュレーション</h2>
<p>画像を読み込む:</p>
<input type="file" id="imageInput" accept="image/*">
<!-- サムネイル表示:4つのキャンバスを縮小して横並びに -->
<p>シミュレーション結果のサムネイル</p>
<div class="canvas-container">
<div class="canvas-item">
<p>オリジナル</p>
<canvas id="originalCanvas" class="thumbnail-canvas"></canvas>
</div>
<div class="canvas-item">
<p>プロタノピア</p>
<canvas id="protoCanvas" class="thumbnail-canvas"></canvas>
</div>
<div class="canvas-item">
<p>デューテラノピア</p>
<canvas id="deuteranopiaCanvas" class="thumbnail-canvas"></canvas>
</div>
<div class="canvas-item">
<p>トリタノピア</p>
<canvas id="tritanopiaCanvas" class="thumbnail-canvas"></canvas>
</div>
</div>
<!-- ラジオボタンで拡大表示する画像を選択 -->
<div>
<p>拡大表示する画像を選択:</p>
<label><input type="radio" name="simType" value="original" checked> オリジナル</label>
<label><input type="radio" name="simType" value="protanopia"> プロタノピア</label>
<label><input type="radio" name="simType" value="deuteranopia"> デューテラノピア</label>
<label><input type="radio" name="simType" value="tritanopia"> トリタノピア</label>
</div>
<!-- 拡大表示用キャンバス -->
<div>
<p>拡大表示</p>
<canvas id="displayCanvas" style="border:1px solid #000;"></canvas>
</div>
</div>
<script>
// --- 基本の色変換関数 ---
function hexToRgb(hex) {
hex = hex.replace(/^#/, '');
if (hex.length === 3) {
hex = hex.split('').map(h => h + h).join('');
}
const intVal = parseInt(hex, 16);
return { r: (intVal >> 16) & 255, g: (intVal >> 8) & 255, b: intVal & 255 };
}
function rgbToHex(r, g, b) {
const toHex = x => {
const hex = Math.round(x).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return '#' + toHex(r) + toHex(g) + toHex(b);
}
// --- 色覚シミュレーション用関数 ---
// 以下の計算式は、[ChromaticVisionSimulator](https://github.com/asada0/ChromaticVisionSimulator) の手法(sRGB→LMS, LMS→sRGB)を参考にしています。
// sRGB → LMS 変換(リニアな RGB 値として扱う前提)
function rgbToLms(r, g, b) {
return {
L: 0.31399022 * r + 0.63951294 * g + 0.04649755 * b,
M: 0.15537241 * r + 0.75789446 * g + 0.08670142 * b,
S: 0.01775239 * r + 0.10944209 * g + 0.87256922 * b
};
}
// LMS → sRGB 変換
function lmsToRgb(L, M, S) {
return {
r: 5.47221206 * L - 4.6419601 * M + 0.16963708 * S,
g: -1.1252419 * L + 2.29317094 * M - 0.1678952 * S,
b: 0.02980165 * L - 0.19318073 * M + 1.16364789 * S
};
}
// プロタノピア:L (長波長) チャネルが欠損 → L 成分を他のチャネルから補完
function simulateProtanopia(r, g, b) {
const lms = rgbToLms(r, g, b);
// 欠損する L 成分を M と S の線形結合で補完(ここでは 0.7, 0.3 を例とする)
const L_new = 0.7 * lms.M + 0.3 * lms.S;
const rgbNew = lmsToRgb(L_new, lms.M, lms.S);
return rgbNew;
}
// デューテラノピア:M (中波長) チャネルが欠損 → M 成分を他のチャネルから補完
function simulateDeuteranopia(r, g, b) {
const lms = rgbToLms(r, g, b);
const M_new = 0.7 * lms.L + 0.3 * lms.S;
const rgbNew = lmsToRgb(lms.L, M_new, lms.S);
return rgbNew;
}
// トリタノピア:S (短波長) チャネルが欠損 → S 成分を他のチャネルから補完
function simulateTritanopia(r, g, b) {
const lms = rgbToLms(r, g, b);
const S_new = 0.7 * lms.L + 0.3 * lms.M;
const rgbNew = lmsToRgb(lms.L, lms.M, S_new);
return rgbNew;
}
// --- カラーピッカーによる色表示 ---
function updateColors() {
const colorPicker = document.getElementById('colorPicker');
const hexColor = colorPicker.value;
const rgb = hexToRgb(hexColor);
// 通常の色
const normalDiv = document.getElementById('normalColor');
normalDiv.style.backgroundColor = hexColor;
normalDiv.textContent = hexColor.toUpperCase();
// プロタノピア
const proto = simulateProtanopia(rgb.r, rgb.g, rgb.b);
const protoHex = rgbToHex(proto.r, proto.g, proto.b);
const protoDiv = document.getElementById('protanopiaColor');
protoDiv.style.backgroundColor = protoHex;
protoDiv.textContent = protoHex.toUpperCase();
// デューテラノピア
const deuto = simulateDeuteranopia(rgb.r, rgb.g, rgb.b);
const deutoHex = rgbToHex(deuto.r, deuto.g, deuto.b);
const deuteranopiaDiv = document.getElementById('deuteranopiaColor');
deuteranopiaDiv.style.backgroundColor = deutoHex;
deuteranopiaDiv.textContent = deutoHex.toUpperCase();
// トリタノピア
const trito = simulateTritanopia(rgb.r, rgb.g, rgb.b);
const tritoHex = rgbToHex(trito.r, trito.g, trito.b);
const tritanopiaDiv = document.getElementById('tritanopiaColor');
tritanopiaDiv.style.backgroundColor = tritoHex;
tritanopiaDiv.textContent = tritoHex.toUpperCase();
}
document.getElementById('colorPicker').addEventListener('input', updateColors);
updateColors();
// --- 画像処理 ---
// 各画素ごとにシミュレーション関数を適用
function applySimulationToImageData(imageData, simulationFn) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i+1], b = data[i+2];
const converted = simulationFn(r, g, b);
data[i] = Math.min(255, Math.max(0, converted.r));
data[i+1] = Math.min(255, Math.max(0, converted.g));
data[i+2] = Math.min(255, Math.max(0, converted.b));
}
return imageData;
}
document.getElementById('imageInput').addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const width = img.width, height = img.height;
// サムネイル用キャンバスの内部解像度は画像サイズに合わせる
const canvases = [
document.getElementById('originalCanvas'),
document.getElementById('protoCanvas'),
document.getElementById('deuteranopiaCanvas'),
document.getElementById('tritanopiaCanvas')
];
canvases.forEach(canvas => {
canvas.width = width;
canvas.height = height;
});
// オリジナル画像を描画
const originalCtx = document.getElementById('originalCanvas').getContext('2d');
originalCtx.drawImage(img, 0, 0, width, height);
const originalData = originalCtx.getImageData(0, 0, width, height);
// プロタノピアの処理
const protoCtx = document.getElementById('protoCanvas').getContext('2d');
let protoImageData = new ImageData(new Uint8ClampedArray(originalData.data), width, height);
protoImageData = applySimulationToImageData(protoImageData, simulateProtanopia);
protoCtx.putImageData(protoImageData, 0, 0);
// デューテラノピアの処理
const deuteranopiaCtx = document.getElementById('deuteranopiaCanvas').getContext('2d');
let deuteranopiaImageData = new ImageData(new Uint8ClampedArray(originalData.data), width, height);
deuteranopiaImageData = applySimulationToImageData(deuteranopiaImageData, simulateDeuteranopia);
deuteranopiaCtx.putImageData(deuteranopiaImageData, 0, 0);
// トリタノピアの処理
const tritanopiaCtx = document.getElementById('tritanopiaCanvas').getContext('2d');
let tritanopiaImageData = new ImageData(new Uint8ClampedArray(originalData.data), width, height);
tritanopiaImageData = applySimulationToImageData(tritanopiaImageData, simulateTritanopia);
tritanopiaCtx.putImageData(tritanopiaImageData, 0, 0);
// 画像読み込み後、現在選択中のラジオボタンに合わせた拡大表示を更新
updateDisplay();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
// --- 拡大表示 ---
function updateDisplay() {
const selectedValue = document.querySelector('input[name="simType"]:checked').value;
let sourceCanvas;
if (selectedValue === "original") {
sourceCanvas = document.getElementById('originalCanvas');
} else if (selectedValue === "protanopia") {
sourceCanvas = document.getElementById('protoCanvas');
} else if (selectedValue === "deuteranopia") {
sourceCanvas = document.getElementById('deuteranopiaCanvas');
} else if (selectedValue === "tritanopia") {
sourceCanvas = document.getElementById('tritanopiaCanvas');
}
if (!sourceCanvas) return;
const displayCanvas = document.getElementById('displayCanvas');
const dCtx = displayCanvas.getContext('2d');
// 拡大倍率(例として2倍)
const scaleFactor = 2;
displayCanvas.width = sourceCanvas.width * scaleFactor;
displayCanvas.height = sourceCanvas.height * scaleFactor;
dCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height);
dCtx.drawImage(sourceCanvas, 0, 0, displayCanvas.width, displayCanvas.height);
}
document.querySelectorAll('input[name="simType"]').forEach(radio => {
radio.addEventListener('change', updateDisplay);
});
</script>
</body>
</html>
ディスカッション
コメント一覧
まだ、コメントがありません