Created
February 11, 2025 15:11
-
-
Save xl1/094dba690019d0e0473a1cc18ef8a074 to your computer and use it in GitHub Desktop.
Image stitching demo (created by ChatGPT o3-mini-high)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>OpenCV.js 画像ステッチング プロトタイプ</title> | |
<style> | |
body { font-family: sans-serif; } | |
canvas { border: 1px solid #ccc; margin-top: 10px; } | |
</style> | |
<!-- OpenCV.js の読み込み。URLはバージョン 4.x 系の場合 --> | |
<script async src="https://docs.opencv.org/4.x/opencv.js" onload="onOpenCvReady();"></script> | |
</head> | |
<body> | |
<h2>OpenCV.js 画像ステッチング プロトタイプ</h2> | |
<p> | |
画像 1: <input type="file" id="imageInput1" accept="image/*"><br> | |
画像 2: <input type="file" id="imageInput2" accept="image/*"> | |
</p> | |
<button id="stitchButton">画像を合成する</button> | |
<p id="status">OpenCV.js のロードを待っています…</p> | |
<canvas id="canvasOutput"></canvas> | |
<script type="text/javascript"> | |
// OpenCV.js のロード完了を待つためのフラグ | |
let cv; | |
let cvReady = false; | |
async function onOpenCvReady() { | |
cv = await window.cv | |
cvReady = true; | |
document.getElementById('status').innerHTML = 'OpenCV.js のロードが完了しました。'; | |
} | |
// FileReader を用いて画像ファイルを読み込み、Image オブジェクトとして返す | |
function loadImage(fileInput, callback) { | |
let file = fileInput.files[0]; | |
if (!file) return; | |
let reader = new FileReader(); | |
reader.onload = function(e) { | |
let img = new Image(); | |
img.onload = function() { | |
callback(img); | |
} | |
img.src = e.target.result; | |
} | |
reader.readAsDataURL(file); | |
} | |
// ボタン押下時の処理 | |
document.getElementById('stitchButton').addEventListener('click', function() { | |
if (!cvReady) { | |
alert('OpenCV.js がまだロードされていません!'); | |
return; | |
} | |
let input1 = document.getElementById('imageInput1'); | |
let input2 = document.getElementById('imageInput2'); | |
if (input1.files.length === 0 || input2.files.length === 0) { | |
alert('2 枚の画像ファイルを選択してください。'); | |
return; | |
} | |
// 並列に画像を読み込み | |
loadImage(input1, function(img1) { | |
loadImage(input2, function(img2) { | |
stitchImages(img1, img2); | |
}); | |
}); | |
}); | |
// 2 枚の画像を読み込み、ORB で特徴抽出・マッチング、ホモグラフィ求めて合成する関数 | |
function stitchImages(img1, img2) { | |
// 画像を一旦 Canvas に描画して cv.imread で読み込めるようにする | |
let canvas1 = document.createElement('canvas'); | |
let canvas2 = document.createElement('canvas'); | |
canvas1.width = img1.width; canvas1.height = img1.height; | |
canvas2.width = img2.width; canvas2.height = img2.height; | |
let ctx1 = canvas1.getContext('2d'); | |
let ctx2 = canvas2.getContext('2d'); | |
ctx1.drawImage(img1, 0, 0); | |
ctx2.drawImage(img2, 0, 0); | |
// cv.Mat に変換 | |
let mat1 = cv.imread(canvas1); | |
let mat2 = cv.imread(canvas2); | |
// グレースケール変換 | |
let gray1 = new cv.Mat(); | |
let gray2 = new cv.Mat(); | |
cv.cvtColor(mat1, gray1, cv.COLOR_RGBA2GRAY); | |
cv.cvtColor(mat2, gray2, cv.COLOR_RGBA2GRAY); | |
// ORB による特徴点検出と記述子抽出 | |
let orb = new cv.ORB(); // ※ OpenCV.js のバージョンによっては cv.ORB_create() の形式の場合もあります | |
let keypoints1 = new cv.KeyPointVector(); | |
let keypoints2 = new cv.KeyPointVector(); | |
let descriptors1 = new cv.Mat(); | |
let descriptors2 = new cv.Mat(); | |
orb.detectAndCompute(gray1, new cv.Mat(), keypoints1, descriptors1); | |
orb.detectAndCompute(gray2, new cv.Mat(), keypoints2, descriptors2); | |
// BFMatcher(Hamming 距離)を用いてマッチング | |
let bf = new cv.BFMatcher(cv.NORM_HAMMING, true); | |
let matches = new cv.DMatchVector(); | |
bf.match(descriptors1, descriptors2, matches); | |
// マッチング結果から上位 N 件(ここでは 50 件)を選択 | |
let goodMatches = []; | |
for (let i = 0; i < matches.size(); i++) { | |
goodMatches.push(matches.get(i)); | |
} | |
goodMatches.sort((a, b) => a.distance - b.distance); | |
let numGoodMatches = Math.min(50, goodMatches.length); | |
// マッチした特徴点から対応点の配列を作成 | |
let srcPoints = []; | |
let dstPoints = []; | |
for (let i = 0; i < numGoodMatches; i++) { | |
let kp1 = keypoints1.get(goodMatches[i].queryIdx).pt; | |
let kp2 = keypoints2.get(goodMatches[i].trainIdx).pt; | |
// 画像1側の座標、画像2側の座標 | |
srcPoints.push(kp1.x); | |
srcPoints.push(kp1.y); | |
dstPoints.push(kp2.x); | |
dstPoints.push(kp2.y); | |
} | |
// cv.matFromArray(row, col, type, array) で変換 | |
let srcMat = cv.matFromArray(numGoodMatches, 1, cv.CV_32FC2, srcPoints); | |
let dstMat = cv.matFromArray(numGoodMatches, 1, cv.CV_32FC2, dstPoints); | |
// RANSAC を用いてホモグラフィ行列を求める | |
let mask = new cv.Mat(); | |
let homography = cv.findHomography(dstMat, srcMat, cv.RANSAC, 5, mask); | |
// 結果画像用のキャンバスサイズを決定(ここでは横に並べられるように大きめに) | |
let resultWidth = mat1.cols + mat2.cols; | |
let resultHeight = Math.max(mat1.rows, mat2.rows); | |
let result = new cv.Mat(); | |
cv.warpPerspective(mat2, result, homography, new cv.Size(resultWidth, resultHeight)); | |
// mat1 を結果画像にコピー(座標は (0,0) とする) | |
let roi = result.roi(new cv.Rect(0, 0, mat1.cols, mat1.rows)); | |
mat1.copyTo(roi); | |
roi.delete(); | |
// 合成結果を画面上の Canvas に表示 | |
cv.imshow('canvasOutput', result); | |
// 使用したリソースを解放 | |
mat1.delete(); mat2.delete(); | |
gray1.delete(); gray2.delete(); | |
orb.delete(); keypoints1.delete(); keypoints2.delete(); | |
descriptors1.delete(); descriptors2.delete(); | |
bf.delete(); matches.delete(); | |
srcMat.delete(); dstMat.delete(); | |
mask.delete(); homography.delete(); result.delete(); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment