심플 온라인 도구

general

画像フィルター完全ガイド|プロ級エフェクトとアート効果の実装技術

画像フィルターの仕組み、Instagram風フィルター、アート効果、カラーグレーディング、リアルタイム処理、GPUアクセラレーション、カスタムフィルター作成まで、4500字で徹底解説

19분 읽기
画像フィルター完全ガイド|プロ級エフェクトとアート効果の実装技術

画像フィルター完全ガイド

はじめに:画像フィルターの芸術と科学

画像フィルターは、写真を芸術作品に変換する強力なツールです。SNSの普及により、誰もがプロフェッショナルな画像編集を行える時代になりました。本記事では、画像フィルターの技術的な仕組みから、Instagram風エフェクト、アート効果、GPUを活用した高速処理まで、実装技術を包括的に解説します。

第1章:画像フィルターの基礎理論

1.1 カラー変換の数学

RGB色空間での変換行列

class ColorTransform {
  // カラーマトリックス変換
  applyColorMatrix(pixels, matrix) {
    const data = pixels.data;

    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];

      data[i] = r * matrix[0] + g * matrix[1] + b * matrix[2] + matrix[3] * 255;
      data[i + 1] = r * matrix[4] + g * matrix[5] + b * matrix[6] + matrix[7] * 255;
      data[i + 2] = r * matrix[8] + g * matrix[9] + b * matrix[10] + matrix[11] * 255;

      // クランプ処理
      data[i] = Math.max(0, Math.min(255, data[i]));
      data[i + 1] = Math.max(0, Math.min(255, data[i + 1]));
      data[i + 2] = Math.max(0, Math.min(255, data[i + 2]));
    }

    return pixels;
  }

  // 基本フィルター行列
  getFilterMatrices() {
    return {
      grayscale: [
        0.299, 0.587, 0.114, 0,
        0.299, 0.587, 0.114, 0,
        0.299, 0.587, 0.114, 0,
        0, 0, 0, 1
      ],
      sepia: [
        0.393, 0.769, 0.189, 0,
        0.349, 0.686, 0.168, 0,
        0.272, 0.534, 0.131, 0,
        0, 0, 0, 1
      ],
      vintage: [
        0.6, 0.3, 0.1, 0.03,
        0.2, 0.7, 0.1, 0.02,
        0.2, 0.3, 0.5, 0.03,
        0, 0, 0, 1
      ],
      polaroid: [
        1.438, -0.062, -0.062, 0,
        -0.122, 1.378, -0.122, 0,
        -0.016, -0.016, 1.483, 0,
        0, 0, 0, 1
      ]
    };
  }
}

1.2 畳み込みフィルター

エッジ検出とぼかし効果

class ConvolutionFilter {
  applyKernel(imageData, kernel, kernelSize) {
    const pixels = imageData.data;
    const width = imageData.width;
    const height = imageData.height;
    const output = new Uint8ClampedArray(pixels);

    const half = Math.floor(kernelSize / 2);

    for (let y = half; y < height - half; y++) {
      for (let x = half; x < width - half; x++) {
        let r = 0, g = 0, b = 0;

        for (let ky = 0; ky < kernelSize; ky++) {
          for (let kx = 0; kx < kernelSize; kx++) {
            const px = x + kx - half;
            const py = y + ky - half;
            const idx = (py * width + px) * 4;
            const weight = kernel[ky * kernelSize + kx];

            r += pixels[idx] * weight;
            g += pixels[idx + 1] * weight;
            b += pixels[idx + 2] * weight;
          }
        }

        const outputIdx = (y * width + x) * 4;
        output[outputIdx] = r;
        output[outputIdx + 1] = g;
        output[outputIdx + 2] = b;
      }
    }

    imageData.data.set(output);
    return imageData;
  }

  getKernels() {
    return {
      blur: [
        1/9, 1/9, 1/9,
        1/9, 1/9, 1/9,
        1/9, 1/9, 1/9
      ],
      gaussianBlur: [
        1/16, 2/16, 1/16,
        2/16, 4/16, 2/16,
        1/16, 2/16, 1/16
      ],
      sharpen: [
        0, -1, 0,
        -1, 5, -1,
        0, -1, 0
      ],
      edgeDetect: [
        -1, -1, -1,
        -1, 8, -1,
        -1, -1, -1
      ],
      emboss: [
        -2, -1, 0,
        -1, 1, 1,
        0, 1, 2
      ]
    };
  }
}

第2章:Instagram風フィルターの実装

2.1 人気フィルターの再現

Instagram風エフェクト

class InstagramFilters {
  // Clarendon フィルター
  clarendon(canvas) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // コントラスト増加
    this.adjustContrast(imageData, 1.2);

    // 彩度増加
    this.adjustSaturation(imageData, 1.35);

    // シアンのオーバーレイ
    this.applyOverlay(imageData, { r: 127, g: 187, b: 227 }, 0.2);

    ctx.putImageData(imageData, 0, 0);
  }

  // Nashville フィルター
  nashville(canvas) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // 暖色系シフト
    this.applyColorShift(imageData, {
      red: 20,
      green: 10,
      blue: -30
    });

    // コントラスト調整
    this.adjustContrast(imageData, 1.2);

    // ピンクのオーバーレイ
    this.applyOverlay(imageData, { r: 247, g: 218, b: 174 }, 0.3);

    // ヴィンテージ効果
    this.addVignette(canvas);

    ctx.putImageData(imageData, 0, 0);
  }

  // Valencia フィルター
  valencia(canvas) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // 暖色トーン
    const warmMatrix = [
      1.1, 0.05, 0, 0.08,
      0, 1.05, 0.05, 0.08,
      0, 0, 0.95, 0.08,
      0, 0, 0, 1
    ];

    this.applyColorMatrix(imageData, warmMatrix);

    // 彩度とコントラスト
    this.adjustSaturation(imageData, 1.2);
    this.adjustContrast(imageData, 1.08);

    ctx.putImageData(imageData, 0, 0);
  }

  // X-Pro II フィルター
  xpro2(canvas) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // カラーカーブ調整
    this.applyCurves(imageData, {
      red: this.createCurve([0, 0], [100, 150], [255, 255]),
      green: this.createCurve([0, 0], [100, 100], [255, 255]),
      blue: this.createCurve([0, 30], [100, 80], [255, 255])
    });

    // 強いヴィネット
    this.addVignette(canvas, 0.6);

    ctx.putImageData(imageData, 0, 0);
  }

  // ヘルパー関数
  adjustContrast(imageData, factor) {
    const data = imageData.data;
    factor = (factor * 255 - 255) / 255;

    for (let i = 0; i < data.length; i += 4) {
      data[i] = data[i] + (data[i] - 128) * factor;
      data[i + 1] = data[i + 1] + (data[i + 1] - 128) * factor;
      data[i + 2] = data[i + 2] + (data[i + 2] - 128) * factor;
    }
  }

  adjustSaturation(imageData, saturation) {
    const data = imageData.data;

    for (let i = 0; i < data.length; i += 4) {
      const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];

      data[i] = gray + (data[i] - gray) * saturation;
      data[i + 1] = gray + (data[i + 1] - gray) * saturation;
      data[i + 2] = gray + (data[i + 2] - gray) * saturation;
    }
  }

  addVignette(canvas, strength = 0.4) {
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;

    const gradient = ctx.createRadialGradient(
      width / 2, height / 2, 0,
      width / 2, height / 2, Math.sqrt(width * width + height * height) / 2
    );

    gradient.addColorStop(0, `rgba(0,0,0,0)`);
    gradient.addColorStop(0.5, `rgba(0,0,0,${strength * 0.2})`);
    gradient.addColorStop(1, `rgba(0,0,0,${strength})`);

    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, width, height);
  }
}

2.2 VSCOスタイルフィルター

フィルムエミュレーション

class VSCOFilters {
  // フィルムエミュレーション
  filmEmulation(canvas, filmType) {
    const films = {
      'Fuji Velvia': {
        curves: {
          red: [[0, 0], [64, 56], [128, 128], [192, 196], [255, 255]],
          green: [[0, 0], [64, 61], [128, 128], [192, 194], [255, 255]],
          blue: [[0, 0], [64, 68], [128, 128], [192, 188], [255, 255]]
        },
        saturation: 1.4,
        contrast: 1.2
      },
      'Kodak Portra': {
        curves: {
          red: [[0, 0], [64, 70], [128, 135], [192, 195], [255, 255]],
          green: [[0, 0], [64, 65], [128, 128], [192, 190], [255, 255]],
          blue: [[0, 0], [64, 62], [128, 124], [192, 188], [255, 245]]
        },
        saturation: 0.9,
        contrast: 1.05
      },
      'Ilford HP5': {
        grayscale: true,
        curves: {
          all: [[0, 10], [64, 58], [128, 134], [192, 200], [255, 250]]
        },
        grain: true,
        contrast: 1.3
      }
    };

    const film = films[filmType];
    if (!film) return;

    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    if (film.grayscale) {
      this.toGrayscale(imageData);
    }

    this.applyCurvesAdjustment(imageData, film.curves);

    if (film.saturation) {
      this.adjustSaturation(imageData, film.saturation);
    }

    if (film.contrast) {
      this.adjustContrast(imageData, film.contrast);
    }

    if (film.grain) {
      this.addFilmGrain(imageData);
    }

    ctx.putImageData(imageData, 0, 0);
  }

  // フィルムグレイン効果
  addFilmGrain(imageData, intensity = 0.1) {
    const data = imageData.data;

    for (let i = 0; i < data.length; i += 4) {
      const noise = (Math.random() - 0.5) * intensity * 255;

      data[i] = Math.max(0, Math.min(255, data[i] + noise));
      data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
      data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
    }
  }

  // カーブ調整
  applyCurvesAdjustment(imageData, curves) {
    const data = imageData.data;
    const lookupTables = {};

    // ルックアップテーブルの作成
    for (const channel in curves) {
      lookupTables[channel] = this.createLookupTable(curves[channel]);
    }

    for (let i = 0; i < data.length; i += 4) {
      if (lookupTables.red) {
        data[i] = lookupTables.red[data[i]];
      }
      if (lookupTables.green) {
        data[i + 1] = lookupTables.green[data[i + 1]];
      }
      if (lookupTables.blue) {
        data[i + 2] = lookupTables.blue[data[i + 2]];
      }
      if (lookupTables.all) {
        data[i] = lookupTables.all[data[i]];
        data[i + 1] = lookupTables.all[data[i + 1]];
        data[i + 2] = lookupTables.all[data[i + 2]];
      }
    }
  }

  createLookupTable(points) {
    const lut = new Uint8Array(256);

    // スプライン補間
    for (let i = 0; i < 256; i++) {
      lut[i] = this.cubicSplineInterpolate(points, i);
    }

    return lut;
  }
}

第3章:アート効果フィルター

3.1 油絵効果

油絵風レンダリング

class ArtisticFilters {
  oilPainting(canvas, brushSize = 4, intensity = 20) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;
    const width = imageData.width;
    const height = imageData.height;

    const output = new Uint8ClampedArray(data);

    for (let y = brushSize; y < height - brushSize; y++) {
      for (let x = brushSize; x < width - brushSize; x++) {
        const intensityBins = new Array(intensity + 1).fill(null).map(() => ({
          r: 0, g: 0, b: 0, count: 0
        }));

        // 周辺ピクセルを強度ビンに分類
        for (let dy = -brushSize; dy <= brushSize; dy++) {
          for (let dx = -brushSize; dx <= brushSize; dx++) {
            const idx = ((y + dy) * width + (x + dx)) * 4;

            // 強度を計算
            const intensity = Math.floor(
              ((data[idx] + data[idx + 1] + data[idx + 2]) / 3) *
              intensity / 255
            );

            intensityBins[intensity].r += data[idx];
            intensityBins[intensity].g += data[idx + 1];
            intensityBins[intensity].b += data[idx + 2];
            intensityBins[intensity].count++;
          }
        }

        // 最も頻度の高い強度ビンを選択
        let maxBin = intensityBins[0];
        for (const bin of intensityBins) {
          if (bin.count > maxBin.count) {
            maxBin = bin;
          }
        }

        const outputIdx = (y * width + x) * 4;
        if (maxBin.count > 0) {
          output[outputIdx] = maxBin.r / maxBin.count;
          output[outputIdx + 1] = maxBin.g / maxBin.count;
          output[outputIdx + 2] = maxBin.b / maxBin.count;
          output[outputIdx + 3] = 255;
        }
      }
    }

    imageData.data.set(output);
    ctx.putImageData(imageData, 0, 0);
  }

  // 水彩画効果
  watercolor(canvas) {
    const ctx = canvas.getContext('2d');

    // ステップ1: エッジ保持平滑化
    this.bilateralFilter(canvas);

    // ステップ2: 色の単純化
    this.quantizeColors(canvas, 8);

    // ステップ3: 紙テクスチャの追加
    this.addPaperTexture(canvas);

    // ステップ4: エッジの強調
    const edges = this.detectEdges(canvas);
    ctx.globalCompositeOperation = 'multiply';
    ctx.drawImage(edges, 0, 0);
  }

  // ペン画効果
  penSketch(canvas) {
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;

    // グレースケール変換
    const imageData = ctx.getImageData(0, 0, width, height);
    this.toGrayscale(imageData);
    ctx.putImageData(imageData, 0, 0);

    // エッジ検出
    const edges = this.sobelEdgeDetection(canvas);

    // ハッチング効果
    const hatching = this.createHatching(edges);

    // 合成
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, width, height);
    ctx.drawImage(hatching, 0, 0);
  }

  createHatching(edgeCanvas) {
    const ctx = edgeCanvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, edgeCanvas.width, edgeCanvas.height);
    const data = imageData.data;

    const hatchCanvas = document.createElement('canvas');
    hatchCanvas.width = edgeCanvas.width;
    hatchCanvas.height = edgeCanvas.height;
    const hatchCtx = hatchCanvas.getContext('2d');

    hatchCtx.strokeStyle = 'black';
    hatchCtx.lineWidth = 0.5;

    for (let y = 0; y < edgeCanvas.height; y += 2) {
      for (let x = 0; x < edgeCanvas.width; x += 2) {
        const idx = (y * edgeCanvas.width + x) * 4;
        const brightness = data[idx] / 255;

        if (brightness < 0.8) {
          const lineCount = Math.floor((1 - brightness) * 4);

          for (let i = 0; i < lineCount; i++) {
            const angle = (brightness * Math.PI) + (i * Math.PI / 4);
            const dx = Math.cos(angle) * 2;
            const dy = Math.sin(angle) * 2;

            hatchCtx.beginPath();
            hatchCtx.moveTo(x - dx, y - dy);
            hatchCtx.lineTo(x + dx, y + dy);
            hatchCtx.stroke();
          }
        }
      }
    }

    return hatchCanvas;
  }
}

3.2 ポップアート効果

Andy Warhol風エフェクト

class PopArtFilter {
  warholEffect(canvas, colors = 4) {
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;

    // ポスタリゼーション
    const posterized = this.posterize(canvas, colors);

    // カラーパレット
    const palettes = [
      ['#FF00FF', '#00FFFF', '#FFFF00', '#000000'],
      ['#FF0000', '#00FF00', '#0000FF', '#FFFFFF'],
      ['#FFA500', '#FF1493', '#00CED1', '#FFD700'],
      ['#8A2BE2', '#32CD32', '#FF69B4', '#1E90FF']
    ];

    // 4分割グリッド作成
    const gridCanvas = document.createElement('canvas');
    gridCanvas.width = width * 2;
    gridCanvas.height = height * 2;
    const gridCtx = gridCanvas.getContext('2d');

    for (let row = 0; row < 2; row++) {
      for (let col = 0; col < 2; col++) {
        const palette = palettes[row * 2 + col];
        const colored = this.applyColorPalette(posterized, palette);

        gridCtx.drawImage(
          colored,
          col * width,
          row * height,
          width,
          height
        );
      }
    }

    // 元のキャンバスにリサイズして描画
    ctx.drawImage(gridCanvas, 0, 0, width, height);
  }

  posterize(canvas, levels) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    const factor = 255 / (levels - 1);

    for (let i = 0; i < data.length; i += 4) {
      data[i] = Math.round(data[i] / factor) * factor;
      data[i + 1] = Math.round(data[i + 1] / factor) * factor;
      data[i + 2] = Math.round(data[i + 2] / factor) * factor;
    }

    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = canvas.width;
    tempCanvas.height = canvas.height;
    tempCanvas.getContext('2d').putImageData(imageData, 0, 0);

    return tempCanvas;
  }

  applyColorPalette(canvas, palette) {
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = canvas.width;
    tempCanvas.height = canvas.height;
    const ctx = tempCanvas.getContext('2d');

    ctx.drawImage(canvas, 0, 0);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    for (let i = 0; i < data.length; i += 4) {
      const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
      const index = Math.floor(gray * (palette.length - 1) / 255);
      const color = this.hexToRgb(palette[index]);

      data[i] = color.r;
      data[i + 1] = color.g;
      data[i + 2] = color.b;
    }

    ctx.putImageData(imageData, 0, 0);
    return tempCanvas;
  }

  hexToRgb(hex) {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16)
    } : null;
  }
}

第4章:WebGLによる高速フィルター処理

4.1 GPUアクセラレーション

WebGLシェーダーでの実装

class WebGLFilters {
  constructor(canvas) {
    this.gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
    this.setupShaders();
  }

  setupShaders() {
    // 頂点シェーダー
    const vertexShaderSource = `
      attribute vec2 a_position;
      attribute vec2 a_texCoord;
      varying vec2 v_texCoord;

      void main() {
        gl_Position = vec4(a_position, 0, 1);
        v_texCoord = a_texCoord;
      }
    `;

    // フラグメントシェーダー(ブラー効果)
    const blurFragmentShader = `
      precision mediump float;
      uniform sampler2D u_image;
      uniform vec2 u_textureSize;
      uniform float u_blurRadius;
      varying vec2 v_texCoord;

      void main() {
        vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
        vec4 colorSum = vec4(0.0);
        float weightSum = 0.0;

        for (float x = -u_blurRadius; x <= u_blurRadius; x += 1.0) {
          for (float y = -u_blurRadius; y <= u_blurRadius; y += 1.0) {
            float weight = exp(-(x*x + y*y) / (2.0 * u_blurRadius * u_blurRadius));
            colorSum += texture2D(u_image, v_texCoord + onePixel * vec2(x, y)) * weight;
            weightSum += weight;
          }
        }

        gl_FragColor = colorSum / weightSum;
      }
    `;

    this.blurProgram = this.createProgram(vertexShaderSource, blurFragmentShader);
  }

  createProgram(vertexSource, fragmentSource) {
    const gl = this.gl;

    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vertexSource);
    gl.compileShader(vertexShader);

    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fragmentSource);
    gl.compileShader(fragmentShader);

    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    return program;
  }

  applyFilter(image, filterType, params) {
    const gl = this.gl;

    // テクスチャ作成
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

    // プログラム使用
    gl.useProgram(this.blurProgram);

    // ユニフォーム設定
    const textureSizeLocation = gl.getUniformLocation(this.blurProgram, 'u_textureSize');
    gl.uniform2f(textureSizeLocation, image.width, image.height);

    const blurRadiusLocation = gl.getUniformLocation(this.blurProgram, 'u_blurRadius');
    gl.uniform1f(blurRadiusLocation, params.radius || 5.0);

    // 描画
    this.render();
  }

  render() {
    const gl = this.gl;

    // ビューポート設定
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    // クリア
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 描画
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

第5章:カスタムフィルターの作成

5.1 ユーザー定義フィルター

カスタムフィルタービルダー

class CustomFilterBuilder {
  constructor() {
    this.pipeline = [];
  }

  addStep(operation, params) {
    this.pipeline.push({ operation, params });
    return this;
  }

  brightness(value) {
    return this.addStep('brightness', { value });
  }

  contrast(value) {
    return this.addStep('contrast', { value });
  }

  saturation(value) {
    return this.addStep('saturation', { value });
  }

  hue(degrees) {
    return this.addStep('hue', { degrees });
  }

  colorMatrix(matrix) {
    return this.addStep('colorMatrix', { matrix });
  }

  curves(curveData) {
    return this.addStep('curves', { curveData });
  }

  blend(blendImage, mode, opacity) {
    return this.addStep('blend', { blendImage, mode, opacity });
  }

  apply(canvas) {
    const ctx = canvas.getContext('2d');
    let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    for (const step of this.pipeline) {
      imageData = this.executeStep(imageData, step);
    }

    ctx.putImageData(imageData, 0, 0);
  }

  executeStep(imageData, step) {
    const operations = {
      brightness: (data, params) => {
        const factor = params.value;
        for (let i = 0; i < data.length; i += 4) {
          data[i] = Math.min(255, data[i] * factor);
          data[i + 1] = Math.min(255, data[i + 1] * factor);
          data[i + 2] = Math.min(255, data[i + 2] * factor);
        }
      },

      contrast: (data, params) => {
        const factor = (params.value + 100) / 100;
        const intercept = 128 * (1 - factor);

        for (let i = 0; i < data.length; i += 4) {
          data[i] = data[i] * factor + intercept;
          data[i + 1] = data[i + 1] * factor + intercept;
          data[i + 2] = data[i + 2] * factor + intercept;
        }
      },

      hue: (data, params) => {
        const angle = params.degrees * Math.PI / 180;
        const cos = Math.cos(angle);
        const sin = Math.sin(angle);

        for (let i = 0; i < data.length; i += 4) {
          const [h, s, l] = this.rgbToHsl(data[i], data[i + 1], data[i + 2]);
          const newHue = (h + params.degrees / 360) % 1;
          const [r, g, b] = this.hslToRgb(newHue, s, l);

          data[i] = r;
          data[i + 1] = g;
          data[i + 2] = b;
        }
      }
    };

    const operation = operations[step.operation];
    if (operation) {
      operation(imageData.data, step.params);
    }

    return imageData;
  }

  // 色空間変換ヘルパー
  rgbToHsl(r, g, b) {
    r /= 255;
    g /= 255;
    b /= 255;

    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    const l = (max + min) / 2;

    if (max === min) {
      return [0, 0, l];
    }

    const d = max - min;
    const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

    let h;
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
      case g: h = ((b - r) / d + 2) / 6; break;
      case b: h = ((r - g) / d + 4) / 6; break;
    }

    return [h, s, l];
  }

  hslToRgb(h, s, l) {
    let r, g, b;

    if (s === 0) {
      r = g = b = l;
    } else {
      const hue2rgb = (p, q, t) => {
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1/6) return p + (q - p) * 6 * t;
        if (t < 1/2) return q;
        if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
        return p;
      };

      const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
      const p = 2 * l - q;

      r = hue2rgb(p, q, h + 1/3);
      g = hue2rgb(p, q, h);
      b = hue2rgb(p, q, h - 1/3);
    }

    return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
  }
}

// 使用例
const customFilter = new CustomFilterBuilder()
  .brightness(1.2)
  .contrast(1.1)
  .saturation(1.3)
  .hue(10)
  .colorMatrix([
    1.1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 0.9, 0,
    0, 0, 0, 1
  ])
  .apply(canvas);

보안 및 개인정보 보호

모든 처리는 브라우저 내에서 완료되며 데이터는 외부로 전송되지 않습니다. 개인정보나 기밀 데이터도 안심하고 이용할 수 있습니다.

문제 해결

일반적인 문제

  • 작동하지 않음: 브라우저 캐시를 지우고 새로고침
  • 처리 속도 느림: 파일 크기 확인 (권장 20MB 이하)
  • 예상과 다른 결과: 입력 형식 및 설정 확인

문제가 해결되지 않으면 브라우저를 최신 버전으로 업데이트하거나 다른 브라우저를 시도하세요.

まとめ:プロフェッショナルな画像フィルター活用

画像フィルターは、写真を芸術作品に変換する強力なツールです。以下のポイントを押さえることで、効果的な実装を実現できます:

  1. 基礎理論の理解:カラーマトリックスと畳み込みフィルター
  2. 人気エフェクトの実装:Instagram風、VSCO風フィルター
  3. アート効果の活用:油絵、水彩、ペン画効果
  4. パフォーマンス最適化:WebGLによるGPUアクセラレーション
  5. カスタマイズ性:ユーザー定義フィルターの作成

i4uの画像フィルターツールを活用することで、簡単にプロ級のエフェクトを適用できます。

カテゴリ別ツール

他のツールもご覧ください:

関連ツール