画像解析ツール完全ガイド|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画像アップスケーラーの使い方から、プロレベルの活用テクニックまで徹底解説します。