画像解析による島の識別とステッチ(ハッチング)表示
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>連結成分の視覚化</title>
<style>
/* 15色のカラフルなスタイルを定義 */
.color1 {
color: #E74C3C;
}
/* 赤 */
.color2 {
color: #8E44AD;
}
/* 紫 */
.color3 {
color: #3498DB;
}
/* 青 */
.color4 {
color: #16A085;
}
/* 緑 */
.color5 {
color: #F39C12;
}
/* オレンジ */
/* 他の色を定義する */
.link {
text-decoration: underline;
}
/* リンクに下線をつけるなど、リンク用のスタイル */
canvas {
border: 1px solid #000;
}
#colorOptions label {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid #ccc;
margin-right: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<br />
<p>
<span class="color1">1</span>
<span class="color2">5</span>
<span class="color3">色</span>
<span class="color4">に</span>
<span class="color5">減</span>
<span class="color1">色</span>
<span class="color2">web</span>
<span class="color3">ア</span>
<span class="color4">プ</span>
<span class="color5">リ</span>
<span class="color1">は</span>
<a href="https://doiworksshop.com/blogs/image-processing/ikcver4_0" class="link">
<span class="color2">こ</span>
<span class="color3">ち</span>
<span class="color4">ら</span>
</a>
</p>
<br />
<input type="file" id="fileInput" accept="image/*">
<canvas id="canvas"></canvas>
<canvas id="canvas2"></canvas>
<div id="colorOptions"></div>
<select id="widthSelect">
<option value="204">204px</option>
<option value="408">408px</option>
<option value="816">816px</option>
</select>
<button id="displayButton">表示</button>
<button id="executeButton">実行</button>
<button id="resetButton">リセット</button>
<button id="drawOnNewCanvasButton">新しいキャンバスに描画</button>
<button id="displayIslandsButton">島を表示</button>
<label for="islandSizeThresholdRange">島の閾値:</label>
<input type="range" id="islandSizeThresholdRange" name="islandSizeThresholdRange" min="0" max="1000" value="20">
<span id="islandSizeThresholdValue"></span>
<label for="spacing">ハッチング間隔:</label>
<input type="range" id="spacing" min="1" max="20" value="5">
<span id="spacingValue">5</span>px
<br>
<label for="length">ハッチング長さ:</label>
<input type="range" id="length" min="1" max="20" value="7">
<span id="lengthValue">7</span>px
<br>
<div id="canvasContainer"></div>
<br />
<br />
<br />
<a
href="https://doiworksshop.com/blogs/html%E3%81%A8javaschtml-%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8B/%E7%94%BB%E5%83%8F%E8%A7%A3%E6%9E%90%E3%81%AB%E3%82%88%E3%82%8B%E5%B3%B6%E3%81%AE%E8%AD%98%E5%88%A5%E3%81%A8%E3%82%B9%E3%83%86%E3%83%83%E3%83%81%E3%83%8F%E3%83%83%E3%83%81%E3%83%B3%E3%82%B0%E8%A1%A8%E7%A4%BA">プログラムコードはこちら</a>
<script>
let originalImage;
let labels = []; // グローバル変数としてlabelsを定義
let islandSizeThreshold = 100; // 初期閾値を設定(適宜調整)
let mergedColorsMapping = {}; // この変数をグローバルスコープで定義
// スライダーの要素を取得
const spacingSlider = document.getElementById('spacing');
const lengthSlider = document.getElementById('length');
const spacingValue = document.getElementById('spacingValue');
const lengthValue = document.getElementById('lengthValue');
// 島の閾値の数値表示を更新する関数
function updateIslandSizeThresholdValue() {
var threshold = document.getElementById('islandSizeThresholdRange').value;
document.getElementById('islandSizeThresholdValue').textContent = threshold;
}
// ハッチング間隔の数値表示を更新する関数
function updateSpacingValue() {
var spacing = document.getElementById('spacing').value;
document.getElementById('spacingValue').textContent = spacing;
}
// ハッチング長さの数値表示を更新する関数
function updateLengthValue() {
var length = document.getElementById('length').value;
document.getElementById('lengthValue').textContent = length;
}
// 各スライダーに対してイベントリスナーを設定
document.getElementById('islandSizeThresholdRange').addEventListener('input', updateIslandSizeThresholdValue);
document.getElementById('spacing').addEventListener('input', updateSpacingValue);
document.getElementById('length').addEventListener('input', updateLengthValue);
// 初期値の表示を更新
updateIslandSizeThresholdValue();
updateSpacingValue();
updateLengthValue();
document.getElementById('islandSizeThresholdRange').addEventListener('input', function () {
document.getElementById('islandSizeThresholdValue').textContent = this.value;
islandSizeThreshold = parseInt(this.value);
});
document.getElementById('fileInput').addEventListener('change', setupImage);
document.getElementById('drawOnNewCanvasButton').addEventListener('click', function () {
// 新しいキャンバスにlabelsを基に描画
if (canvas && labels.length > 0) {
visualizeLabelsOnNewCanvas(labels, canvas.width, canvas.height);
} else {
console.error('labelsが未定義、または空です。先に実行ボタンをクリックしてください。');
}
});
document.getElementById('resetButton').addEventListener('click', function () {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
setupImage();
});
document.getElementById('displayButton').addEventListener('click', function () {
setupImage();
});
document.getElementById('executeButton').addEventListener('click', function () {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const selectedColors = getSelectedColors(mergedColorsMapping);
console.log('セレクトカラーズ:', selectedColors);
labels = labelIslands(imageData, selectedColors, islandSizeThreshold); // labelsを更新
visualizeLabels(ctx, labels, canvas.width, canvas.height);
});
document.getElementById('displayIslandsButton').addEventListener('click', function () {
// スライダーから値を取得
const hatchSpacing = parseInt(document.getElementById('spacing').value, 10);
const hatchLength = parseInt(document.getElementById('length').value, 10);
// 値が正しく取得できているかを確認
console.log('hatchSpacing:', hatchSpacing, 'hatchLength:', hatchLength);
//spacingValue.textContent = hatchSpacing;
// lengthValue.textContent = hatchLength;
if (labels.length > 0) {
displayIslandsOneByOne(labels, canvas.width, canvas.height, hatchSpacing, hatchLength); // スライダーの値を渡す
} else {
console.error('labelsが未定義、または空です。先に実行ボタンをクリックしてください。');
}
});
function setupImage() {
const fileInput = document.getElementById('fileInput');
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const img = new Image();
img.onload = function () {
originalImage = img;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
performResize();
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const colors = extractColors(imageData);
Object.assign(mergedColorsMapping, mergeSimilarColors(colors));
console.log('マージカラーマップ:', mergedColorsMapping);
createColorCheckboxes(mergedColorsMapping);
// originalImageの使用が完了したので、メモリ解放を促すために参照をnullに設定
originalImage = null;
};
img.src = URL.createObjectURL(file);
}
}
function performResize() {
const selectedWidth = parseInt(document.getElementById('widthSelect').value);
const originalWidth = originalImage.width;
const scaleFactor = selectedWidth / originalWidth;
resizeImage(scaleFactor);
}
function resizeImage(scaleFactor) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const resizedImageData = resizeImageBySkippingPixels(imageData, scaleFactor);
canvas.width = resizedImageData.width;
canvas.height = resizedImageData.height;
ctx.putImageData(resizedImageData, 0, 0);
}
function resizeImageBySkippingPixels(imageData, scaleFactor) {
const oldWidth = imageData.width;
const oldHeight = imageData.height;
const oldPixels = imageData.data;
const newWidth = Math.floor(oldWidth * scaleFactor);
const newHeight = Math.floor(oldHeight * scaleFactor);
const newPixels = new Uint8ClampedArray(newWidth * newHeight * 4);
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
const oldX = Math.floor(x / scaleFactor);
const oldY = Math.floor(y / scaleFactor);
const oldIndex = (oldY * oldWidth + oldX) * 4;
const newIndex = (y * newWidth + x) * 4;
newPixels[newIndex] = oldPixels[oldIndex];
newPixels[newIndex + 1] = oldPixels[oldIndex + 1];
newPixels[newIndex + 2] = oldPixels[oldIndex + 2];
newPixels[newIndex + 3] = oldPixels[oldIndex + 3];
}
}
return new ImageData(newPixels, newWidth, newHeight);
}
let errorMargin = 10; // 色の丸めとマージに使用する誤差の範囲を定義
// 画像データから色を抽出する関数
function extractColors(imageData) {
const colors = {};
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// RGB値をそのまま結合してキーとする
const colorKey = joinRGBValues(r, g, b);
colors[colorKey] = (colors[colorKey] || 0) + 1;
}
return colors;
}
// RGB値をそのまま文字列に結合する関数
function joinRGBValues(r, g, b) {
return `${r},${g},${b}`;
}
// 二つの色が類似しているかどうかを判断する関数
function isSimilarColor(color1, color2) {
const [r1, g1, b1] = color1.split(',').map(Number);
const [r2, g2, b2] = color2.split(',').map(Number);
return Math.abs(r1 - r2) <= errorMargin &&
Math.abs(g1 - g2) <= errorMargin &&
Math.abs(b1 - b2) <= errorMargin;
}
// 類似色をマージする関数
function mergeSimilarColors(colors) {
const mergedColors = {};
const mergeMap = {}; // マージされた色と元の色のマッピングを保持
for (const color1 in colors) {
let isMerged = false;
for (const color2 in mergedColors) {
if (isSimilarColor(color1, color2)) {
mergedColors[color2] += colors[color1];
isMerged = true;
if (!mergeMap[color2]) {
mergeMap[color2] = [color2]; // 初期化
}
mergeMap[color2].push(color1);
break;
}
}
if (!isMerged) {
mergedColors[color1] = colors[color1];
mergeMap[color1] = [color1]; // 自分自身を含める
}
}
return { mergedColors, mergeMap };
}
// マージされた色のチェックボックスを生成する関数
function createColorCheckboxes(mergedColorsMapping) {
const container = document.getElementById('colorOptions');
container.innerHTML = ''; // コンテナの既存の内容をクリア
// mergeMapオブジェクト内の各マージされた色に対して処理を行います。
Object.keys(mergedColorsMapping.mergeMap).forEach(function (mergedColor) {
// マージされた色に対応する元の色の配列を取得
const originalColorsArray = mergedColorsMapping.mergeMap[mergedColor];
// 元の色の数を取得
const originalColorsCount = originalColorsArray.length;
// ラベル要素の作成
const label = document.createElement('label');
label.style.cssText = `
background-color: rgb(${mergedColor});
display: flex;
align-items: center;
white-space: nowrap;
margin: 5px;
padding: 5px;
border: 1px solid #ccc;
cursor: pointer;
`;
// チェックボックス要素の作成
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = `rgb(${mergedColor})`; // チェックボックスの値にマージされた色を設定
// チェックボックスをラベルに追加
label.appendChild(checkbox);
// ラベルにマージされた色と、それにマージされた元の色の数を表示するテキストノードを追加
const textNode = document.createTextNode(` rgb(${mergedColor}) - ${originalColorsCount} colors`);
label.appendChild(textNode);
// コンテナにラベルを追加
container.appendChild(label);
});
}
class UnionFind {
constructor(size) {
this.parent = new Array(size);
for (let i = 0; i < size; i++) {
this.parent[i] = i;
}
}
find(x) {
if (this.parent[x] === x) {
return x;
} else {
return this.parent[x] = this.find(this.parent[x]);
}
}
union(x, y) {
let rootX = this.find(x);
let rootY = this.find(y);
if (rootX !== rootY) {
this.parent[rootY] = rootX;
}
}
}
function labelIslands(imageData, selectedColors, islandSizeThreshold) {
const width = imageData.width;
const height = imageData.height;
const data = imageData.data;
const labels = new Array(height).fill(null).map(() => new Array(width).fill(0));
const uf = new UnionFind(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
const pixelColor = [data[index], data[index + 1], data[index + 2]]; // RGB
const alpha = data[index + 3];
if (alpha > 0 && isSelectedColor(pixelColor, selectedColors)) {
let currentLabel = y * width + x; // 一意のラベルを割り当て
labels[y][x] = currentLabel;
if (x > 0 && labels[y][x - 1] !== 0) uf.union(currentLabel, labels[y][x - 1]);
if (y > 0 && labels[y - 1][x] !== 0) uf.union(currentLabel, labels[y - 1][x]);
}
}
}
// ラベルの圧縮と島のサイズ計算
const islandSizes = {};
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (labels[y][x] !== 0) {
const rootLabel = uf.find(labels[y][x]);
labels[y][x] = rootLabel;
islandSizes[rootLabel] = (islandSizes[rootLabel] || 0) + 1;
}
}
}
// サイズ閾値に基づくフィルタリング
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (labels[y][x] !== 0 && islandSizes[labels[y][x]] < islandSizeThreshold) {
labels[y][x] = 0; // 閾値未満の島のラベルをリセット
}
}
}
return labels;
}
function isSelectedColor(pixelColor, selectedColors) {
// ピクセル色を 'rgb(r,g,b)' 形式の文字列に変換
//const colorString = `rgb(${pixelColor[0]},${pixelColor[1]},${pixelColor[2]})`;
const colorString = `${pixelColor[0]},${pixelColor[1]},${pixelColor[2]}`;
// 変換した色が選択された色のリストに含まれるかチェック
// console.log('ピクセル色:', pixelColor);
// console.log('選択された色のリスト:', selectedColors);
return selectedColors.includes(colorString);
}
function visualizeLabels(ctx, labels, width, height) {
const colorPalette = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#00FFFF', '#FF00FF'];
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
ctx.strokeStyle = "black";
ctx.setLineDash([5, 5]);
const islandNumbers = {};
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const label = labels[y][x];
if (label !== 0) {
const colorIndex = (label - 1) % colorPalette.length;
const color = hexToRgb(colorPalette[colorIndex]);
const index = (y * width + x) * 4;
data[index] = color.r;
data[index + 1] = color.g;
data[index + 2] = color.b;
data[index + 3] = 255;
}
}
}
ctx.setLineDash([]);
ctx.putImageData(imageData, 0, 0);
}
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
function getSelectedColors(mergedColorsMapping) {
console.log('マージカラーマップ:getSelectedColors', mergedColorsMapping);
const selectedColors = [];
const checkboxes = document.querySelectorAll('#colorOptions input[type="checkbox"]:checked');
checkboxes.forEach(checkbox => {
// 'rgb(255,140,140)'の形式から'255,140,140'の形式に変換
const colorKey = checkbox.value.match(/\d+/g).join(',');
if (mergedColorsMapping.mergeMap[colorKey]) {
// 見つかった場合はその色に関連する元の色の配列を追加
selectedColors.push(...mergedColorsMapping.mergeMap[colorKey]);
} else {
// mergeMapにマージされた色が見つからない場合は、マージされた色自体を追加
// これは、色が直接選択された場合や、マージせずに単一の色だけが存在する場合に対応
selectedColors.push(checkbox.value);
}
});
return selectedColors;
}
function visualizeLabelsOnNewCanvas(labels, width, height) {
const canvas2 = document.getElementById('canvas2');
const ctx2 = canvas2.getContext('2d');
canvas2.width = width;
canvas2.height = height;
const colorPalette = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#00FFFF', '#FF00FF'];
ctx2.strokeStyle = "black";
ctx2.setLineDash([5, 5]);
const islandNumbers = {};
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const label = labels[y][x];
if (label !== 0) {
if (!islandNumbers[label]) {
ctx2.fillStyle = "black";
ctx2.fillText(label.toString(), x, y);
islandNumbers[label] = true;
}
drawBoundaryDashedLine(ctx2, labels, label, x, y, width, height);
}
}
}
ctx2.setLineDash([]);
}
function drawBoundaryDashedLine(ctx, labels, label, x, y, width, height) {
const isBoundaryPixel = (x, y, label) => {
if (x === 0 || x === width - 1 || y === 0 || y === height - 1) return true;
if (labels[y][x - 1] !== label) return true;
if (labels[y][x + 1] !== label) return true;
if (labels[y - 1][x] !== label) return true;
if (labels[y + 1][x] !== label) return true;
return false;
};
if (isBoundaryPixel(x, y, label)) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + 1, y + 1);
ctx.stroke();
}
}
function displayIslandsOneByOne(labels, width, height, hatchSpacing, hatchLength) {
const canvasContainer = document.getElementById('canvasContainer');
clearCanvasContainer(canvasContainer); // 既存のキャンバスをクリア
// メインキャンバスの初期化
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
canvasContainer.appendChild(canvas); // キャンバスをコンテナに追加
// 各島のサイズを計算
const islandNumbers = {}; // 各島のサイズを格納するオブジェクト
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const label = labels[y][x];
if (label !== 0) {
if (!islandNumbers[label]) {
islandNumbers[label] = 0;
}
islandNumbers[label]++;
}
}
}
const islandSizeThreshold = parseInt(document.getElementById('islandSizeThresholdRange').value); // 閾値を取得
// 島ごとに描画
Object.keys(islandNumbers).forEach(islandLabel => {
if (islandNumbers[islandLabel] >= islandSizeThreshold) {
const coordinates = listUpIslandCoordinates(labels, parseInt(islandLabel));
ctx.save(); // 描画状態を保存
applyClippingPath(ctx, coordinates); // クリッピングパスを適用
applyHatching(ctx, width, height, hatchSpacing, hatchLength); // ハッチングを適用
ctx.restore(); // 描画状態をリストアし、クリッピングパスをリセット
}
});
}
// キャンバスをクリアする関数
function clearCanvasContainer(container) {
container.innerHTML = ''; // container内の全ての要素をクリア
}
// 島のデータを表示する関数
// createCanvasWithIslandDataDirectly関数の実装
function createCanvasWithIslandDataDirectly(labels, width, height, islandLabel, islandSizeThreshold, islandNumbers, hatchSpacing, hatchLength) {
console.log('hatchSpacing:', hatchSpacing, 'hatchLength:', hatchLength);
// 島のサイズが閾値未満なら処理をスキップ
if (islandNumbers[islandLabel] < islandSizeThreshold) {
return null;
}
// メインキャンバスの準備
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// '島'の座標をリストアップ
const coordinates = listUpIslandCoordinates(labels, islandLabel);
// メインキャンバスにクリッピングパスを適用
//applyClippingPath(ctx, coordinates);
// メインキャンバスにハッチングを適用
//applyHatching(ctx, width, height, hatchSpacing, hatchLength); // hatchSpacingとhatchLengthは適宜調整
applyHatchingDirectly(ctx, coordinates, hatchSpacing, hatchLength)
return canvas;
}
function listUpIslandCoordinates(labels, targetLabel) {
const coordinates = [];
for (let y = 0; y < labels.length; y++) {
for (let x = 0; x < labels[y].length; x++) {
if (labels[y][x] === targetLabel) {
coordinates.push({ x, y });
}
}
}
return coordinates;
}
function applyClippingPath(ctx, coordinates) {
ctx.beginPath();
coordinates.forEach(coord => {
ctx.rect(coord.x, coord.y, 1, 1);
});
ctx.clip();
}
function applyHatchingDirectly(ctx, coordinates, hatchSpacing, hatchLength) {
coordinates.forEach(coord => {
const startX = coord.x;
const startY = coord.y;
const endX = startX + hatchLength * Math.cos(Math.PI / 4);
const endY = startY + hatchLength * Math.sin(Math.PI / 4);
// ハッチングの間隔を考慮して、選択された座標にハッチングを適用
if (startX % hatchSpacing === 0 && startY % hatchSpacing === 0) {
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
}
});
}
function applyHatching(ctx, width, height, hatchSpacing, hatchLength) {
for (let y = 0; y < height; y += hatchSpacing) {
for (let x = 0; x < width; x += hatchSpacing) {
const endX = x + hatchLength * Math.cos(Math.PI / 4);
const endY = y + hatchLength * Math.sin(Math.PI / 4);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(endX, endY);
ctx.stroke();
}
}
}
function rgbToHex(r, g, b) {
// 各RGB値を16進数に変換し、必要に応じて先頭に0を追加する
const toHex = (c) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
// RGB値を16進数形式に変換し、先頭に'#'を追加して結果を返す
return "#" + toHex(r) + toHex(g) + toHex(b);
}
</script>
</body>
</html>