画像の色覚シミュレーション改良版

JavaScript

/* VisionSimulator */ /* Copyright (c) 2018 Kazunori Asada */ /* Released under the MIT license */ /* https://github.com/asada0/ChromaticVisionSimulator/blob/master/LICENSE.txt */ /* */ 色覚シミュレーションツールVisionSimulator

色覚シミュレーションツール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>

参考

色のシミュレータ > ホーム

JavaScript

Posted by eightban