Simple Tools Hub - Simple Online Tools

general

画像解析ツール完全ガイド|AI画像認識・品質評価・メタデータ分析の実装技術

AI画像認識、オブジェクト検出、画質評価、色彩分析、EXIF解析、顔認識、テキスト抽出まで、最新の画像解析技術と実装方法を4500字で徹底解説

25 min read
画像解析ツール完全ガイド|AI画像認識・品質評価・メタデータ分析の実装技術

画像解析ツール完全ガイド

はじめに:画像解析の革命的進化

現代の画像解析技術は、AI・機械学習の発達により、人間の認識能力を上回る精度を実現しています。医療画像診断、自動運転、品質管理、セキュリティなど、あらゆる分野で活用されています。本記事では、最新の画像解析技術から実践的な実装方法まで、包括的に解説します。

第1章:AI画像認識の基礎技術

1.1 畳み込みニューラルネットワーク(CNN)

画像認識の基本アーキテクチャ

const tf = require('@tensorflow/tfjs-node');

class ImageClassifier {
  constructor() {
    this.model = null;
    this.classes = [];
  }

  async loadPretrainedModel(modelPath) {
    // MobileNetV2モデルの読み込み
    this.model = await tf.loadLayersModel(modelPath);

    // ImageNetクラスラベル
    this.classes = await this.loadClassLabels();

    return { success: true, classes: this.classes.length };
  }

  async classifyImage(imagePath, topK = 5) {
    // 画像の前処理
    const imageTensor = await this.preprocessImage(imagePath);

    // 推論実行
    const predictions = await this.model.predict(imageTensor);
    const probabilities = await predictions.data();

    // 上位K個の結果を取得
    const results = this.getTopKResults(probabilities, topK);

    // メモリ解放
    imageTensor.dispose();
    predictions.dispose();

    return results;
  }

  async preprocessImage(imagePath) {
    const fs = require('fs');
    const imageBuffer = fs.readFileSync(imagePath);

    // 画像をテンソルに変換
    let imageTensor = tf.node.decodeImage(imageBuffer, 3);

    // リサイズ(224x224)
    imageTensor = tf.image.resizeBilinear(imageTensor, [224, 224]);

    // 正規化(0-1)
    imageTensor = imageTensor.div(255.0);

    // バッチ次元を追加
    imageTensor = imageTensor.expandDims(0);

    return imageTensor;
  }

  getTopKResults(probabilities, topK) {
    const results = [];

    // 確率値とインデックスをペアにする
    for (let i = 0; i < probabilities.length; i++) {
      results.push({
        classIndex: i,
        className: this.classes[i],
        probability: probabilities[i],
        confidence: Math.round(probabilities[i] * 100 * 100) / 100
      });
    }

    // 確率順にソート
    results.sort((a, b) => b.probability - a.probability);

    return results.slice(0, topK);
  }
}

1.2 オブジェクト検出

YOLO(You Only Look Once)の実装

class ObjectDetector {
  constructor() {
    this.model = null;
    this.anchors = null;
    this.classes = null;
  }

  async loadYOLOModel(modelPath) {
    this.model = await tf.loadLayersModel(modelPath);

    // YOLOv5のアンカー設定
    this.anchors = [
      [10, 13], [16, 30], [33, 23],      // 小さいオブジェクト用
      [30, 61], [62, 45], [59, 119],     // 中サイズ用
      [116, 90], [156, 198], [373, 326]  // 大きいオブジェクト用
    ];

    // COCOデータセットクラス
    this.classes = [
      'person', 'bicycle', 'car', 'motorcycle', 'airplane',
      'bus', 'train', 'truck', 'boat', 'traffic light',
      // ... 80クラス
    ];
  }

  async detectObjects(imagePath, confidenceThreshold = 0.5) {
    const imageTensor = await this.preprocessImageForYOLO(imagePath);

    // YOLO推論
    const predictions = await this.model.predict(imageTensor);

    // 後処理:バウンディングボックスとNMS
    const detections = await this.postprocessYOLO(
      predictions,
      confidenceThreshold
    );

    return detections;
  }

  async postprocessYOLO(predictions, threshold) {
    const boxes = [];
    const scores = [];
    const classIds = [];

    const outputData = await predictions.data();
    const outputShape = predictions.shape;

    // グリッドセルごとの処理
    for (let i = 0; i < outputShape[1]; i++) {
      for (let j = 0; j < outputShape[2]; j++) {
        const offset = (i * outputShape[2] + j) * outputShape[3];

        // オブジェクト存在確率
        const objectness = outputData[offset + 4];

        if (objectness > threshold) {
          // バウンディングボックス座標
          const x = outputData[offset + 0];
          const y = outputData[offset + 1];
          const w = outputData[offset + 2];
          const h = outputData[offset + 3];

          // クラス確率
          const classProbs = [];
          for (let k = 5; k < outputShape[3]; k++) {
            classProbs.push(outputData[offset + k]);
          }

          const maxClassProb = Math.max(...classProbs);
          const classId = classProbs.indexOf(maxClassProb);
          const finalScore = objectness * maxClassProb;

          if (finalScore > threshold) {
            boxes.push([x - w/2, y - h/2, x + w/2, y + h/2]);
            scores.push(finalScore);
            classIds.push(classId);
          }
        }
      }
    }

    // Non-Maximum Suppression
    const indices = await tf.image.nonMaxSuppression(
      tf.tensor2d(boxes),
      tf.tensor1d(scores),
      100, // max_output_size
      0.4  // iou_threshold
    );

    const selectedIndices = await indices.data();
    const results = [];

    for (const idx of selectedIndices) {
      results.push({
        class: this.classes[classIds[idx]],
        confidence: scores[idx],
        bbox: boxes[idx],
        classId: classIds[idx]
      });
    }

    return results;
  }
}

第2章:画像品質評価

2.1 客観的品質評価メトリクス

PSNR、SSIM、MSEの実装

class ImageQualityAssessment {
  // Peak Signal-to-Noise Ratio
  calculatePSNR(originalImage, compressedImage) {
    const mse = this.calculateMSE(originalImage, compressedImage);

    if (mse === 0) return Infinity; // 完全に同じ画像

    const maxPixelValue = 255; // 8bit画像の場合
    const psnr = 20 * Math.log10(maxPixelValue / Math.sqrt(mse));

    return psnr;
  }

  // Mean Squared Error
  calculateMSE(image1, image2) {
    if (image1.length !== image2.length) {
      throw new Error('Images must have the same dimensions');
    }

    let sumSquaredDiff = 0;

    for (let i = 0; i < image1.length; i += 4) { // RGBA
      const r1 = image1[i], g1 = image1[i+1], b1 = image1[i+2];
      const r2 = image2[i], g2 = image2[i+1], b2 = image2[i+2];

      sumSquaredDiff += Math.pow(r1 - r2, 2) +
                        Math.pow(g1 - g2, 2) +
                        Math.pow(b1 - b2, 2);
    }

    return sumSquaredDiff / (image1.length * 3 / 4); // RGB channels only
  }

  // Structural Similarity Index
  calculateSSIM(image1, image2, windowSize = 11) {
    const window = this.createGaussianKernel(windowSize);
    const c1 = Math.pow(0.01 * 255, 2);
    const c2 = Math.pow(0.03 * 255, 2);

    // 画像を重複するウィンドウに分割
    const windows1 = this.extractWindows(image1, windowSize);
    const windows2 = this.extractWindows(image2, windowSize);

    let totalSSIM = 0;
    let windowCount = 0;

    for (let i = 0; i < windows1.length; i++) {
      const w1 = windows1[i];
      const w2 = windows2[i];

      // 統計量を計算
      const mu1 = this.calculateMean(w1);
      const mu2 = this.calculateMean(w2);
      const mu1Sq = mu1 * mu1;
      const mu2Sq = mu2 * mu2;
      const mu1Mu2 = mu1 * mu2;

      const sigma1Sq = this.calculateVariance(w1, mu1);
      const sigma2Sq = this.calculateVariance(w2, mu2);
      const sigma12 = this.calculateCovariance(w1, w2, mu1, mu2);

      // SSIM計算
      const ssim = ((2 * mu1Mu2 + c1) * (2 * sigma12 + c2)) /
                   ((mu1Sq + mu2Sq + c1) * (sigma1Sq + sigma2Sq + c2));

      totalSSIM += ssim;
      windowCount++;
    }

    return totalSSIM / windowCount;
  }

  // Visual Information Fidelity (VIF)
  calculateVIF(referenceImage, testImage) {
    // ウェーブレット変換
    const refWavelets = this.dwtTransform(referenceImage);
    const testWavelets = this.dwtTransform(testImage);

    let numerator = 0;
    let denominator = 0;

    // 各サブバンドでVIFを計算
    for (let scale = 0; scale < refWavelets.length; scale++) {
      const refSub = refWavelets[scale];
      const testSub = testWavelets[scale];

      // 自然シーンモデルパラメータ推定
      const sigmaRef = this.estimateVariance(refSub);
      const sigmaTest = this.estimateVariance(testSub);
      const sigmaNoise = Math.abs(sigmaTest - sigmaRef);

      // 情報量計算
      const info1 = this.calculateInformation(sigmaRef, sigmaNoise);
      const info2 = this.calculateInformation(sigmaTest, sigmaNoise);

      numerator += info2;
      denominator += info1;
    }

    return denominator > 0 ? numerator / denominator : 0;
  }

  // ブラー検出
  detectBlur(imageData, threshold = 100) {
    const grayImage = this.convertToGrayscale(imageData);

    // Laplacianフィルタでエッジを強調
    const laplacianKernel = [
      0, -1, 0,
      -1, 4, -1,
      0, -1, 0
    ];

    const edges = this.applyConvolution(grayImage, laplacianKernel, 3);

    // エッジ強度の分散を計算
    const variance = this.calculateVariance(edges, this.calculateMean(edges));

    return {
      isBlurry: variance < threshold,
      blurScore: variance,
      quality: variance > threshold * 2 ? 'sharp' :
               variance > threshold ? 'acceptable' : 'blurry'
    };
  }

  // ノイズレベル推定
  estimateNoiseLevel(imageData) {
    const grayImage = this.convertToGrayscale(imageData);

    // ハイパスフィルタでノイズを抽出
    const highPassKernel = [
      -1, -1, -1,
      -1, 8, -1,
      -1, -1, -1
    ];

    const noise = this.applyConvolution(grayImage, highPassKernel, 3);

    // ノイズの標準偏差を計算
    const mean = this.calculateMean(noise);
    const variance = this.calculateVariance(noise, mean);
    const standardDeviation = Math.sqrt(variance);

    return {
      noiseLevel: standardDeviation,
      quality: standardDeviation < 10 ? 'low_noise' :
               standardDeviation < 25 ? 'moderate_noise' : 'high_noise'
    };
  }
}

2.2 知覚品質評価

人間の視覚特性を考慮した評価

class PerceptualQualityAssessment {
  // LPIPS (Learned Perceptual Image Patch Similarity)
  async calculateLPIPS(image1, image2) {
    // 深層学習モデルを使用した知覚距離計算
    const model = await this.loadLPIPSModel();

    const tensor1 = this.preprocessForLPIPS(image1);
    const tensor2 = this.preprocessForLPIPS(image2);

    const distance = await model.predict([tensor1, tensor2]);
    const lpipsScore = await distance.data();

    return lpipsScore[0];
  }

  // Human Visual System (HVS) モデル
  calculateHVSMetric(image1, image2) {
    // CSF (Contrast Sensitivity Function) を適用
    const csf1 = this.applyCSF(image1);
    const csf2 = this.applyCSF(image2);

    // 視覚的重み付け
    const weights = this.calculateVisualWeights(image1.width, image1.height);

    let weightedDifference = 0;
    let totalWeight = 0;

    for (let i = 0; i < csf1.length; i++) {
      const diff = Math.abs(csf1[i] - csf2[i]);
      const weight = weights[i];

      weightedDifference += diff * weight;
      totalWeight += weight;
    }

    return weightedDifference / totalWeight;
  }

  applyCSF(imageData) {
    // フーリエ変換
    const fftData = this.fft2d(imageData);

    // CSF重み付け
    const width = imageData.width;
    const height = imageData.height;
    const result = new Float32Array(width * height);

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const u = x < width/2 ? x : x - width;
        const v = y < height/2 ? y : y - height;

        // 空間周波数
        const freq = Math.sqrt(u*u + v*v) / Math.max(width, height);

        // CSF関数(人間の視覚感度)
        const csf = this.csfFunction(freq);

        const idx = y * width + x;
        result[idx] = fftData[idx] * csf;
      }
    }

    // 逆フーリエ変換
    return this.ifft2d(result, width, height);
  }

  csfFunction(frequency) {
    // Campbell-Robson CSF モデル
    const a = 2.6;
    const b = 0.0192;
    const c = 0.114;
    const d = 1.1;

    if (frequency === 0) return 1.0;

    const csf = a * Math.exp(-b * frequency) - c * Math.exp(-d * frequency);
    return Math.max(0, Math.min(1, csf));
  }

  // JND (Just Noticeable Difference) 計算
  calculateJND(imageData) {
    const grayImage = this.convertToGrayscale(imageData);
    const jndMap = new Float32Array(grayImage.length);

    for (let i = 0; i < grayImage.length; i++) {
      const luminance = grayImage[i];

      // Weber-Fechner法則に基づくJND
      const jnd = this.weberFechnerJND(luminance);

      // マスキング効果を考慮
      const localVariance = this.calculateLocalVariance(grayImage, i);
      const maskingFactor = this.maskingFunction(localVariance);

      jndMap[i] = jnd * maskingFactor;
    }

    return jndMap;
  }

  weberFechnerJND(luminance) {
    // JND = T(I) = a * I^b (a=0.5, b=0.3 for typical viewing conditions)
    const a = 0.5;
    const b = 0.3;

    return a * Math.pow(luminance / 255, b);
  }
}

第3章:画像内容分析

3.1 顔検出・認識

顔検出とランドマーク抽出

const faceapi = require('@vladmandic/face-api');

class FaceAnalyzer {
  async initialize() {
    // モデルの読み込み
    await faceapi.nets.ssdMobilenetv1.loadFromDisk('./models');
    await faceapi.nets.faceLandmark68Net.loadFromDisk('./models');
    await faceapi.nets.faceRecognitionNet.loadFromDisk('./models');
    await faceapi.nets.ageGenderNet.loadFromDisk('./models');
    await faceapi.nets.faceExpressionNet.loadFromDisk('./models');
  }

  async analyzeFaces(imagePath) {
    const image = await this.loadImage(imagePath);

    // 総合的な顔解析
    const detections = await faceapi
      .detectAllFaces(image)
      .withFaceLandmarks()
      .withFaceDescriptors()
      .withAgeAndGender()
      .withFaceExpressions();

    const results = [];

    for (let i = 0; i < detections.length; i++) {
      const detection = detections[i];

      const faceData = {
        id: i,
        bbox: {
          x: detection.detection.box.x,
          y: detection.detection.box.y,
          width: detection.detection.box.width,
          height: detection.detection.box.height
        },
        confidence: detection.detection.score,
        landmarks: this.extractLandmarks(detection.landmarks),
        age: Math.round(detection.age),
        gender: detection.gender,
        genderProbability: detection.genderProbability,
        expressions: this.processExpressions(detection.expressions),
        descriptor: Array.from(detection.descriptor),
        faceQuality: await this.assessFaceQuality(detection)
      };

      results.push(faceData);
    }

    return {
      faceCount: results.length,
      faces: results,
      imageMetadata: {
        width: image.width,
        height: image.height
      }
    };
  }

  extractLandmarks(landmarks) {
    return {
      jawOutline: landmarks.getJawOutline().map(p => ({x: p.x, y: p.y})),
      leftEyebrow: landmarks.getLeftEyeBrow().map(p => ({x: p.x, y: p.y})),
      rightEyebrow: landmarks.getRightEyeBrow().map(p => ({x: p.x, y: p.y})),
      noseBridge: landmarks.getNose().map(p => ({x: p.x, y: p.y})),
      leftEye: landmarks.getLeftEye().map(p => ({x: p.x, y: p.y})),
      rightEye: landmarks.getRightEye().map(p => ({x: p.x, y: p.y})),
      mouth: landmarks.getMouth().map(p => ({x: p.x, y: p.y}))
    };
  }

  processExpressions(expressions) {
    const sorted = Object.entries(expressions)
      .map(([emotion, probability]) => ({
        emotion,
        probability: Math.round(probability * 100),
        confidence: probability > 0.5 ? 'high' :
                   probability > 0.3 ? 'medium' : 'low'
      }))
      .sort((a, b) => b.probability - a.probability);

    return {
      dominant: sorted[0],
      all: sorted
    };
  }

  async assessFaceQuality(detection) {
    const landmarks = detection.landmarks;

    // 顔の向きを計算
    const pose = this.calculateFacePose(landmarks);

    // ブラー検出
    const blur = await this.detectFaceBlur(detection.detection.box);

    // 照明品質
    const lighting = this.assessLighting(detection.detection.box);

    // 解像度
    const resolution = detection.detection.box.width * detection.detection.box.height;

    let qualityScore = 100;

    // ペナルティ適用
    if (Math.abs(pose.yaw) > 15) qualityScore -= 20;
    if (Math.abs(pose.pitch) > 15) qualityScore -= 15;
    if (blur.isBlurry) qualityScore -= 30;
    if (lighting.quality === 'poor') qualityScore -= 25;
    if (resolution < 10000) qualityScore -= 20; // 100x100未満

    return {
      score: Math.max(0, qualityScore),
      factors: {
        pose: pose,
        blur: blur,
        lighting: lighting,
        resolution: {
          pixels: resolution,
          quality: resolution > 40000 ? 'high' :
                  resolution > 10000 ? 'medium' : 'low'
        }
      }
    };
  }

  calculateFacePose(landmarks) {
    // 3Dモデルポイント(標準顔)
    const modelPoints = [
      [0.0, 0.0, 0.0],        // 鼻先
      [0.0, -330.0, -65.0],   // 顎
      [-225.0, 170.0, -135.0], // 左目尻
      [225.0, 170.0, -135.0],  // 右目尻
      [-150.0, -150.0, -125.0], // 左口角
      [150.0, -150.0, -125.0]   // 右口角
    ];

    // 対応する2Dランドマーク
    const imagePoints = [
      landmarks.getNose()[3],    // 鼻先
      landmarks.getJawOutline()[8], // 顎
      landmarks.getLeftEye()[3],  // 左目尻
      landmarks.getRightEye()[0], // 右目尻
      landmarks.getMouth()[0],    // 左口角
      landmarks.getMouth()[6]     // 右口角
    ];

    // PnP (Perspective-n-Point) で姿勢推定
    const rotation = this.solvePnP(modelPoints, imagePoints);

    // オイラー角に変換
    return {
      yaw: rotation.yaw,    // 左右の向き
      pitch: rotation.pitch, // 上下の向き
      roll: rotation.roll    // 傾き
    };
  }
}

3.2 テキスト認識(OCR)

Tesseract.jsを使用したOCR実装

const Tesseract = require('tesseract.js');

class TextRecognizer {
  constructor() {
    this.worker = null;
  }

  async initialize(language = 'jpn+eng') {
    this.worker = await Tesseract.createWorker();
    await this.worker.loadLanguage(language);
    await this.worker.initialize(language);

    // OCRパラメータ設定
    await this.worker.setParameters({
      tessedit_pageseg_mode: Tesseract.PSM.AUTO,
      preserve_interword_spaces: '1',
      tessedit_char_whitelist: null // 全文字許可
    });
  }

  async recognizeText(imagePath, options = {}) {
    const {
      preprocess = true,
      confidenceThreshold = 60,
      languages = 'jpn+eng',
      whitelist = null,
      blacklist = null
    } = options;

    let processedImage = imagePath;

    if (preprocess) {
      processedImage = await this.preprocessImage(imagePath);
    }

    // OCR実行
    const { data } = await this.worker.recognize(processedImage);

    // 結果の後処理
    const results = this.processOCRResults(data, confidenceThreshold);

    return results;
  }

  async preprocessImage(imagePath) {
    const sharp = require('sharp');
    const outputPath = imagePath.replace(/\.[^.]+$/, '_preprocessed.png');

    await sharp(imagePath)
      // グレースケール変換
      .grayscale()
      // コントラスト強化
      .normalize()
      // シャープ化
      .sharpen()
      // ノイズ除去
      .blur(0.3)
      // 二値化
      .threshold(128)
      // 解像度向上(必要に応じて)
      .resize(null, null, {
        kernel: 'cubic',
        withoutEnlargement: false
      })
      .png()
      .toFile(outputPath);

    return outputPath;
  }

  processOCRResults(data, confidenceThreshold) {
    const words = data.words.filter(word =>
      word.confidence >= confidenceThreshold
    );

    const lines = this.groupWordsIntoLines(words);
    const paragraphs = this.groupLinesIntoParagraphs(lines);

    return {
      text: data.text.trim(),
      confidence: data.confidence,
      words: words.map(word => ({
        text: word.text,
        confidence: word.confidence,
        bbox: word.bbox,
        baseline: word.baseline
      })),
      lines: lines,
      paragraphs: paragraphs,
      statistics: {
        wordCount: words.length,
        lineCount: lines.length,
        paragraphCount: paragraphs.length,
        averageConfidence: words.reduce((sum, w) => sum + w.confidence, 0) / words.length
      }
    };
  }

  groupWordsIntoLines(words) {
    const lines = [];
    let currentLine = [];

    words.sort((a, b) => a.bbox.y0 - b.bbox.y0);

    for (const word of words) {
      if (currentLine.length === 0) {
        currentLine.push(word);
      } else {
        const lastWord = currentLine[currentLine.length - 1];
        const verticalDistance = Math.abs(word.bbox.y0 - lastWord.bbox.y0);

        if (verticalDistance < lastWord.bbox.y1 - lastWord.bbox.y0) {
          // 同じ行
          currentLine.push(word);
        } else {
          // 新しい行
          lines.push({
            text: currentLine.map(w => w.text).join(' '),
            words: currentLine,
            bbox: this.calculateBoundingBox(currentLine)
          });
          currentLine = [word];
        }
      }
    }

    if (currentLine.length > 0) {
      lines.push({
        text: currentLine.map(w => w.text).join(' '),
        words: currentLine,
        bbox: this.calculateBoundingBox(currentLine)
      });
    }

    return lines;
  }

  // 表形式データの検出と抽出
  async extractTableData(imagePath) {
    const results = await this.recognizeText(imagePath, {
      preprocess: true,
      confidenceThreshold: 70
    });

    // 表の構造を推定
    const tableStructure = this.analyzeTableStructure(results.lines);

    if (tableStructure.isTable) {
      return this.extractTableCells(results.lines, tableStructure);
    }

    return null;
  }

  analyzeTableStructure(lines) {
    // 行の垂直配置を分析
    const yPositions = lines.map(line => line.bbox.y0).sort((a, b) => a - b);
    const rowSpacing = [];

    for (let i = 1; i < yPositions.length; i++) {
      rowSpacing.push(yPositions[i] - yPositions[i-1]);
    }

    const avgRowSpacing = rowSpacing.reduce((a, b) => a + b, 0) / rowSpacing.length;
    const consistentSpacing = rowSpacing.filter(spacing =>
      Math.abs(spacing - avgRowSpacing) < avgRowSpacing * 0.3
    ).length > rowSpacing.length * 0.7;

    // カラムの検出
    const wordPositions = [];
    lines.forEach(line => {
      line.words.forEach(word => {
        wordPositions.push(word.bbox.x0);
      });
    });

    const columns = this.detectColumns(wordPositions);

    return {
      isTable: consistentSpacing && columns.length > 1,
      rows: lines.length,
      columns: columns.length,
      columnBoundaries: columns
    };
  }

  detectColumns(xPositions) {
    xPositions.sort((a, b) => a - b);

    // クラスタリングでカラム境界を検出
    const clusters = [];
    let currentCluster = [xPositions[0]];

    for (let i = 1; i < xPositions.length; i++) {
      if (xPositions[i] - xPositions[i-1] < 50) { // 50px以内は同じクラスタ
        currentCluster.push(xPositions[i]);
      } else {
        clusters.push(currentCluster);
        currentCluster = [xPositions[i]];
      }
    }

    clusters.push(currentCluster);

    // 各クラスタの代表値(中央値)
    return clusters.map(cluster => {
      cluster.sort((a, b) => a - b);
      const mid = Math.floor(cluster.length / 2);
      return cluster.length % 2 === 0
        ? (cluster[mid - 1] + cluster[mid]) / 2
        : cluster[mid];
    });
  }
}

第4章:色彩・形状分析

4.1 色彩分析

カラーパレット抽出と分析

const chroma = require('chroma-js');

class ColorAnalyzer {
  extractDominantColors(imageData, k = 5) {
    // K-means クラスタリングで主要色を抽出
    const pixels = this.getPixelData(imageData);
    const clusters = this.kmeansClustering(pixels, k);

    const colors = clusters.map(cluster => {
      const centroid = cluster.centroid;
      const color = chroma.rgb(centroid[0], centroid[1], centroid[2]);

      return {
        rgb: centroid,
        hex: color.hex(),
        hsl: color.hsl(),
        lab: color.lab(),
        percentage: cluster.points.length / pixels.length * 100,
        pixelCount: cluster.points.length
      };
    });

    return colors.sort((a, b) => b.percentage - a.percentage);
  }

  kmeansClustering(pixels, k, maxIterations = 100) {
    // 初期中心点をランダムに選択
    let centroids = this.initializeCentroids(pixels, k);
    let clusters = [];

    for (let iter = 0; iter < maxIterations; iter++) {
      // クラスタ初期化
      clusters = centroids.map(centroid => ({
        centroid: centroid.slice(),
        points: []
      }));

      // 各ピクセルを最も近いクラスタに割り当て
      for (const pixel of pixels) {
        let minDistance = Infinity;
        let closestCluster = 0;

        for (let i = 0; i < centroids.length; i++) {
          const distance = this.euclideanDistance(pixel, centroids[i]);
          if (distance < minDistance) {
            minDistance = distance;
            closestCluster = i;
          }
        }

        clusters[closestCluster].points.push(pixel);
      }

      // 中心点を更新
      let converged = true;
      for (let i = 0; i < clusters.length; i++) {
        if (clusters[i].points.length === 0) continue;

        const newCentroid = this.calculateCentroid(clusters[i].points);

        if (this.euclideanDistance(centroids[i], newCentroid) > 1) {
          converged = false;
        }

        centroids[i] = newCentroid;
        clusters[i].centroid = newCentroid;
      }

      if (converged) break;
    }

    return clusters.filter(cluster => cluster.points.length > 0);
  }

  analyzeColorHarmony(colors) {
    const harmonies = {
      monochromatic: false,
      analogous: false,
      complementary: false,
      triadic: false,
      tetradic: false
    };

    if (colors.length < 2) return harmonies;

    const hues = colors.map(color => color.hsl[0]);

    // モノクロマティック (色相差15度以内)
    const maxHueDiff = Math.max(...hues) - Math.min(...hues);
    harmonies.monochromatic = maxHueDiff <= 15;

    // 類似色 (隣接する色相、30度以内)
    harmonies.analogous = this.checkAnalogous(hues);

    // 補色 (180度差)
    harmonies.complementary = this.checkComplementary(hues);

    // 三角配色 (120度間隔)
    harmonies.triadic = this.checkTriadic(hues);

    // 四角配色 (90度間隔)
    harmonies.tetradic = this.checkTetradic(hues);

    return harmonies;
  }

  analyzeColorTemperature(colors) {
    let warmCount = 0;
    let coolCount = 0;
    let totalWeight = 0;

    colors.forEach(color => {
      const hue = color.hsl[0];
      const weight = color.percentage;

      // 暖色: 0-60度 (赤-黄)、300-360度 (マゼンタ-赤)
      if ((hue >= 0 && hue <= 60) || (hue >= 300 && hue <= 360)) {
        warmCount += weight;
      }
      // 寒色: 180-240度 (シアン-青)
      else if (hue >= 180 && hue <= 240) {
        coolCount += weight;
      }

      totalWeight += weight;
    });

    const warmRatio = warmCount / totalWeight;
    const coolRatio = coolCount / totalWeight;

    return {
      temperature: warmRatio > coolRatio ? 'warm' :
                  coolRatio > warmRatio ? 'cool' : 'neutral',
      warmRatio: warmRatio,
      coolRatio: coolRatio,
      balance: Math.abs(warmRatio - coolRatio) < 0.2 ? 'balanced' :
               warmRatio > coolRatio ? 'warm-dominant' : 'cool-dominant'
    };
  }
}

4.2 形状・パターン分析

エッジ検出とシェイプ分析

class ShapeAnalyzer {
  // Canny エッジ検出
  detectEdges(imageData, lowThreshold = 50, highThreshold = 100) {
    const grayImage = this.convertToGrayscale(imageData);

    // ガウシアンブラー適用
    const blurred = this.gaussianBlur(grayImage, 1.4);

    // 勾配計算 (Sobel オペレータ)
    const gradients = this.calculateGradients(blurred);

    // 非最大値抑制
    const suppressed = this.nonMaximumSuppression(gradients);

    // ダブル閾値処理
    const edges = this.doubleThresholding(suppressed, lowThreshold, highThreshold);

    // ヒステリシス追跡
    const finalEdges = this.hysteresisTracking(edges);

    return finalEdges;
  }

  // ハフ変換による直線検出
  detectLines(edgeImage, threshold = 100) {
    const width = edgeImage.width;
    const height = edgeImage.height;
    const diagonal = Math.sqrt(width * width + height * height);

    // ρ-θ空間でのアキュムレータ
    const rhoMax = Math.ceil(diagonal);
    const thetaMax = 180;
    const accumulator = Array(2 * rhoMax).fill(null)
      .map(() => Array(thetaMax).fill(0));

    // エッジピクセルに対してハフ変換実行
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const idx = y * width + x;
        if (edgeImage.data[idx] > 0) {

          // 各角度θに対してρを計算
          for (let theta = 0; theta < thetaMax; theta++) {
            const radian = theta * Math.PI / 180;
            const rho = x * Math.cos(radian) + y * Math.sin(radian);
            const rhoIdx = Math.round(rho + rhoMax);

            if (rhoIdx >= 0 && rhoIdx < 2 * rhoMax) {
              accumulator[rhoIdx][theta]++;
            }
          }
        }
      }
    }

    // 閾値を超える直線を検出
    const lines = [];
    for (let rho = 0; rho < 2 * rhoMax; rho++) {
      for (let theta = 0; theta < thetaMax; theta++) {
        if (accumulator[rho][theta] > threshold) {
          lines.push({
            rho: rho - rhoMax,
            theta: theta,
            votes: accumulator[rho][theta],
            // 直線の端点を計算
            points: this.calculateLineEndpoints(rho - rhoMax, theta, width, height)
          });
        }
      }
    }

    return lines.sort((a, b) => b.votes - a.votes);
  }

  // 円検出 (ハフ変換)
  detectCircles(edgeImage, minRadius = 10, maxRadius = 100, threshold = 50) {
    const width = edgeImage.width;
    const height = edgeImage.height;

    // 3次元アキュムレータ (x, y, r)
    const circles = [];

    for (let r = minRadius; r <= maxRadius; r += 2) {
      const accumulator = Array(height).fill(null)
        .map(() => Array(width).fill(0));

      // エッジピクセルから可能な中心点を投票
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const idx = y * width + x;
          if (edgeImage.data[idx] > 0) {

            // 円周上の点から中心への投票
            for (let angle = 0; angle < 360; angle += 10) {
              const radian = angle * Math.PI / 180;
              const cx = Math.round(x - r * Math.cos(radian));
              const cy = Math.round(y - r * Math.sin(radian));

              if (cx >= 0 && cx < width && cy >= 0 && cy < height) {
                accumulator[cy][cx]++;
              }
            }
          }
        }
      }

      // 閾値を超える円を検出
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          if (accumulator[y][x] > threshold) {
            circles.push({
              centerX: x,
              centerY: y,
              radius: r,
              votes: accumulator[y][x],
              confidence: accumulator[y][x] / (2 * Math.PI * r * 0.1)
            });
          }
        }
      }
    }

    // 重複する円を除去
    const filteredCircles = this.removeDuplicateCircles(circles);

    return filteredCircles.sort((a, b) => b.confidence - a.confidence);
  }

  // コーナー検出 (Harris Corner Detector)
  detectCorners(imageData, threshold = 0.01, windowSize = 3) {
    const grayImage = this.convertToGrayscale(imageData);
    const width = imageData.width;
    const height = imageData.height;

    // 勾配計算
    const gradients = this.calculateGradients(grayImage);
    const Ix = gradients.x;
    const Iy = gradients.y;

    const corners = [];
    const k = 0.04; // Harris定数

    // 各ピクセルでHarrisレスポンスを計算
    for (let y = windowSize; y < height - windowSize; y++) {
      for (let x = windowSize; x < width - windowSize; x++) {
        let Ixx = 0, Iyy = 0, Ixy = 0;

        // ウィンドウ内の勾配を累積
        for (let dy = -windowSize; dy <= windowSize; dy++) {
          for (let dx = -windowSize; dx <= windowSize; dx++) {
            const idx = (y + dy) * width + (x + dx);
            const ix = Ix[idx];
            const iy = Iy[idx];

            Ixx += ix * ix;
            Iyy += iy * iy;
            Ixy += ix * iy;
          }
        }

        // Harris行列の固有値近似
        const det = Ixx * Iyy - Ixy * Ixy;
        const trace = Ixx + Iyy;
        const response = det - k * trace * trace;

        if (response > threshold) {
          corners.push({
            x: x,
            y: y,
            response: response,
            strength: response > threshold * 10 ? 'strong' : 'weak'
          });
        }
      }
    }

    // 非最大値抑制
    return this.nonMaximumSuppressionCorners(corners, 5);
  }

  // テクスチャ分析 (LBP: Local Binary Pattern)
  analyzeTexture(imageData, radius = 1, neighbors = 8) {
    const grayImage = this.convertToGrayscale(imageData);
    const width = imageData.width;
    const height = imageData.height;

    const lbpImage = new Uint8Array(width * height);
    const histogram = new Array(Math.pow(2, neighbors)).fill(0);

    for (let y = radius; y < height - radius; y++) {
      for (let x = radius; x < width - radius; x++) {
        const centerIdx = y * width + x;
        const centerValue = grayImage[centerIdx];

        let lbpValue = 0;

        // 近傍ピクセルを円形にサンプリング
        for (let i = 0; i < neighbors; i++) {
          const angle = 2 * Math.PI * i / neighbors;
          const nx = x + radius * Math.cos(angle);
          const ny = y + radius * Math.sin(angle);

          // バイリニア補間
          const neighborValue = this.bilinearInterpolation(
            grayImage, nx, ny, width, height
          );

          if (neighborValue >= centerValue) {
            lbpValue |= (1 << i);
          }
        }

        lbpImage[centerIdx] = lbpValue;
        histogram[lbpValue]++;
      }
    }

    // テクスチャ特徴量を計算
    const features = this.calculateTextureFeatures(histogram);

    return {
      lbpImage: lbpImage,
      histogram: histogram,
      features: features
    };
  }

  calculateTextureFeatures(histogram) {
    const total = histogram.reduce((a, b) => a + b, 0);
    const normalizedHist = histogram.map(val => val / total);

    // 統計的特徴量
    let energy = 0;
    let entropy = 0;
    let contrast = 0;

    for (let i = 0; i < normalizedHist.length; i++) {
      const p = normalizedHist[i];
      if (p > 0) {
        energy += p * p;
        entropy -= p * Math.log2(p);
      }

      // コントラスト計算(隣接パターン間の差)
      for (let j = i + 1; j < normalizedHist.length; j++) {
        const diff = this.hammingDistance(i, j);
        contrast += diff * p * normalizedHist[j];
      }
    }

    return {
      energy: energy,      // エネルギー(均一性の指標)
      entropy: entropy,    // エントロピー(複雑さの指標)
      contrast: contrast,  // コントラスト(変化の激しさ)
      uniformity: energy,  // 均一性
      complexity: entropy  // 複雑さ
    };
  }
}

Security and Privacy

All processing is done within your browser, and no data is sent externally. You can safely use it with personal or confidential information.

Troubleshooting

Common Issues

  • Not working: Clear browser cache and reload
  • Slow processing: Check file size (recommended under 20MB)
  • Unexpected results: Verify input format and settings

If issues persist, update your browser to the latest version or try a different browser.

まとめ:次世代画像解析技術の展開

画像解析技術は、AI・機械学習の進歩により、人間の知覚を超える精度と速度を実現しています。以下のポイントを押さえることで、効果的な画像解析システムを構築できます:

  1. AI技術の活用:CNN、YOLO、Transformerモデルの適切な選択
  2. 品質評価の多角化:客観的・知覚的指標の組み合わせ
  3. リアルタイム処理:GPU活用と最適化アルゴリズム
  4. データ統合:画像情報とメタデータの総合分析
  5. 継続的学習:新しいモデルとデータセットの活用

i4uの画像解析ツールを活用することで、簡単に高度な画像解析を実行できます。

カテゴリ別ツール

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

関連ツール