画像解析ツール完全ガイド|AI画像認識・品質評価・メタデータ分析の実装技術
AI画像認識、オブジェクト検出、画質評価、色彩分析、EXIF解析、顔認識、テキスト抽出まで、最新の画像解析技術と実装方法を4500字で徹底解説
画像解析ツール完全ガイド
はじめに:画像解析の革命的進化
現代の画像解析技術は、AI・機械学習の発達により、人間の認識能力を上回る精度を実現しています。医療画像診断、自動運転、品質管理、セキュリティなど、あらゆる分野で活用されています。本記事では、最新の画像解析技術から実践的な実装方法まで、包括的に解説します。
💡 技術統計: Google Vision API 2024では、画像内オブジェクト検出精度95.3%、テキスト認識精度98.7%を達成しており、商用利用での実用レベルに到達しています。
第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 // 複雑さ
};
}
}
보안 및 개인정보 보호
모든 처리는 브라우저 내에서 완료되며 데이터는 외부로 전송되지 않습니다. 개인정보나 기밀 데이터도 안심하고 이용할 수 있습니다.
문제 해결
일반적인 문제
- 작동하지 않음: 브라우저 캐시를 지우고 새로고침
- 처리 속도 느림: 파일 크기 확인 (권장 20MB 이하)
- 예상과 다른 결과: 입력 형식 및 설정 확인
문제가 해결되지 않으면 브라우저를 최신 버전으로 업데이트하거나 다른 브라우저를 시도하세요.
まとめ:次世代画像解析技術の展開
画像解析技術は、AI・機械学習の進歩により、人間の知覚を超える精度と速度を実現しています。以下のポイントを押さえることで、効果的な画像解析システムを構築できます:
- AI技術の活用:CNN、YOLO、Transformerモデルの適切な選択
- 品質評価の多角化:客観的・知覚的指標の組み合わせ
- リアルタイム処理:GPU活用と最適化アルゴリズム
- データ統合:画像情報とメタデータの総合分析
- 継続的学習:新しいモデルとデータセットの活用
i4uの画像解析ツールを活用することで、簡単に高度な画像解析を実行できます。
カテゴリ別ツール
他のツールもご覧ください:
関連ツール
관련 기사
OCR 도구 완벽 가이드 2025|이미지에서 고정밀 텍스트 추출
이미지와 PDF에서 즉시 텍스트 추출. 일본어, 영어, 중국어, 한국어를 지원하는 고정밀 OCR 도구. 명함 데이터화, 문서 디지털화, 스캔 문서 편집에 최적. 브라우저 완결형으로 개인정보 보호.
2025年最新!AIブログアイデアジェネレーターの選び方と活用法완벽 가이드
ブログのネタ切れに悩むあなたへ。AIブログアイデアジェネレーターを使って無限のコンテンツアイデアを生み出す方法を、実例とともに徹底解説します。
2025年最新!AI画像アップスケーラー완벽 가이드|低解像度画像を高画質化する方法
古い写真や低解像度画像を最新のAI技術で高画質化。無料で使えるi4u AI画像アップスケーラーの使い方から、プロレベルの活用テクニックまで徹底解説します。