OpenCV.js で画像を縦に結合する

ウマの某ツールが残念なことに公開停止になってしまったので、それまで開発していた自作ツールを引っ張り出してきて、せっかくだから OpenCV.js 化しようといろいろ勉強を始めた。

とりあえず、複数の画像ファイルをシンプルに縦に結合するコードが以下。

画像幅が異なる場合、一つ目の画像に合わせるようにアスペクト比を維持したまま拡大縮小する。

長々と書いてしまったが、要点は run() 関数。

OpenCV.js には「結合する」ような単純な関数は無いようで、結合後の領域を持った画像を準備し、そこに貼り付け先座標をズラしながら元画像群をコピーする、とするようだ。

下の stackblitz でお試し可能。

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>OpenCV.js Concat Images(Vertical)</title>
    <script
      async
      src="https://docs.opencv.org/master/opencv.js"
      onload="initialize();"
      type="text/javascript"
    ></script>
  </head>
  <body>
    <h3>OpenCV.js Concat Images(Vertical)</h3>

    <div style="display: flex; flex-direction: column; gap: 4px">
      <div style="display: flex; width: 100%">
        <div style="display: flex; flex-direction: column; flex: 1">
          <input
            multiple
            type="file"
            id="inputImage"
            onchange="loadImageFromIdAsync('inputImage');"
          />
        </div>
      </div>

      <button onclick="run()">RUN!</button>

      <div
        id="logs"
        style="display: flex; flex-direction: row; gap: 4px; flex-wrap: wrap"
      ></div>
    </div>

    <script type="text/javascript" src="script.js"></script>
  </body>
</html>

script.js

function initialize() {
  if (typeof cv === 'undefined') {
    setTimeout(initialize, 50);
    return;
  }
  console.log('OpenCV.js is ready.');
}

const sources = [];
const destructions = [];

async function loadImageFromIdAsync(inputId) {
  const inputImage = document.getElementById(inputId);
  for (const file of inputImage.files) {
    const img = await loadImageFromFileAsync(file);
    sources.push(img);
    addImage(img, '画像');
  }
}

function loadImageFromFileAsync(file) {
  return new Promise((resolve) => {
    const img = new Image();

    img.src = URL.createObjectURL(file);
    img.onload = () => {
      const mat = cv.imread(img);
      destructions.push(mat);
      resolve(mat);
    };
  });
}

let no = 1;
function addImage(image, label) {
  const divLogs = document.getElementById('logs');
  const div = document.createElement('div');
  div.style.cssText = `display: flex; flex-direction: column; width:250px; margin-bottom: 5px;`;

  const span = document.createElement('span');
  span.innerText = `${no}. ${label ?? ''}`;
  div.appendChild(span);

  const canvas = document.createElement('canvas');
  canvas.style.cssText = `width:100%; border: 1px solid black`;
  div.appendChild(canvas);

  divLogs.appendChild(div);
  cv.imshow(canvas, image);
  no++;
}

function run() {
  const type = sources[0].type();
  const maxCol = sources.reduce((pre, cur) => Math.max(pre, cur.cols), 0);

  const resizedSources = sources.reduce((resizedSources, src) => {
    const scale = Math.max(1, maxCol / src.cols);
    const newHeight = Math.round(src.rows * scale);
    const newWidth = Math.round(src.cols * scale);
    const resizedSrc = new cv.Mat();
    destructions.push(resizedSrc);

    cv.resize(
      src,
      resizedSrc,
      new cv.Size(newWidth, newHeight),
      0,
      0,
      cv.INTER_LINEAR
    );

    resizedSources.push(resizedSrc);

    return resizedSources;
  }, []);

  const joinHeight = resizedSources.reduce((pre, cur) => pre + cur.rows, 0);

  const dst = new cv.Mat(joinHeight, maxCol, type);
  destructions.push(dst);

  let rowInc = 0;
  for (src of resizedSources) {
    src.copyTo(dst.rowRange(rowInc, rowInc + src.rows));
    rowInc += src.rows;
  }

  addImage(dst, 'RESULT');

  for (d of destructions) {
    d.delete();
  }
}