画像解析による島の識別とステッチ(ハッチング)表示

<!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"></span>
        <span class="color2"></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>