
ブログをNext.js14(App Router)でリファクタリングしました
この記事では、ライブラリを一切用いず、TypeScriptを用いてブラウザ上で画像処理をする方法を紹介します。また、画像処理をする方法を解説するだけでなく、実際にコードを掲載して画像処理をした結果や、紹介したコードによるデモも掲載しています。JavaScriptしかご存じない方も、型の定義を無視すればJavaScriptのコードとして読み進めることが可能です。
この記事を読むには「デジタル画像処理」の基礎知識(画素値の変換や空間フィルタリング程度で可)が必要です。そこで、
画像処理の知識が無い方でも簡単に画像処理が行える方法を別記事にしました!
ただし、ライブラリを使わずCSSを駆使する方法ですので、表現の幅は限られています。(その分非常に簡単)
★ライブラリを使えば、デジタル画像処理の知識がなくても様々な画像処理が可能です!
Jimpという画像処理ライブラリで、誰でも簡単に画像処理ができる方法をまとめています。
以下で紹介する方法はライブラリを一切使わないため、自分で画素値にアクセスして0から画像処理のアルゴリズムを書いていくことになります。
ブラウザ上で画像処理をする一連のプロセスは次のようになります。
図で表すと、次のようなイメージです。
結構まどろっこしい手順を踏みます。7ステップ示しましたが、実際に画像処理をするのは⑥です。
一度処理する前の画像を④でcanvasに出力する必要があります。
処理前のcanvasタグと処理後のcanvasタグを共通のものとすれば、処理前の画像は実際に表示されませんが。
重要なポイントは★印をつけた⑤と⑥のステップです。
TypeScript (及びJavaScript)で処理する画像データはImageData型になります。
続いて、このImageData型について解説していきます。
公式ドキュメントに詳しい記述はありますが、ここでは最低限必要なポイントに絞って説明します。
ImageData型のプロパティ
画像本体はImageData型のdataプロパティにピクセル値の配列として格納されています。
ピクセル値は、すべてのピクセルのRGBαの4値を1列に並べた1次元配列となっています。
つまり、4値 x 幅 x 高さの長さを持つ1次元配列となります。画素値は通常通り0~255の256値を取ります。
図でまとめると、次のようになります。
ImageData型について解説したので、実際のコードと画像処理の結果を見ていきます。
前節で説明した7つのステップの順に説明していきます。
入力画像と出力画像を別々に表示したいのであれば2つ用意します。
出力画像だけ欲しい場合は入力と出力のcanvasが同じで構いません。
今回は画像処理の結果の前後を比べたいので、2つcanvasタグを用意します。
<canvas id="canvas-in"></canvas>
<canvas id="canvas-out"></canvas>
用意しただけではスクリプトで扱えないので、スクリプト側で参照します。
const canvasIn = document.querySelector<HTMLCanvasElement>('#canvas-in')!
const canvasOut = document.querySelector<HTMLCanvasElement>('#canvas-out')!
今回はcanvasサイズを256 x 256ピクセルとします。
const SIZE = 256
canvasIn.width = SIZE
canvasIn.height = SIZE
canvasOut.width = SIZE
canvasOut.height = SIZE
文字通り、Imageクラスのインスタンスを作成します。
let image = new Image()
Imageのインスタンスのsrcプロパティに画像のパスを代入するだけで、画像の読み込みが行われます。
image.src = 'hoge.jpg'
Imageのインスタンスにイベントリスナを設定し、ロードが終わったらcanvasタグに画像を出力します。
なお、出力の際にCanvasRenderingContext2Dインターフェースを利用します。
長い名前のインターフェースですがコードは単純で、canvas要素のプロパティとして、canvas.getContext('2d')
と呼び出すだけです。
canvasの説明ではないのでCanvasRenderingContext2Dインターフェースについてはこれ以上は深入りしません。
2Dのレンダリング関係とだけ覚えておいて、今回は決り文句として利用するだけでいいです。
//CanvasRenderingContext2Dインターフェース
const ctxSrc = canvasIn.getContext('2d')!
//画像をロードし終えたら
image.addEventListener('load', () => {
//画像を描画
ctxSrc.drawImage(image, 0, 0)
})
drawImage()メソッドの意味ですが、
第1引数はImageオブジェクト、第2引数と第3引数でcanvasエリアのどの座標から画像を描画するかを指定します。
今回はcanvasエリア目一杯に画像を表示したいので、一番左上の原点(0, 0)を指定しています。
drawImageの他の使い方は、公式ドキュメントをご参照下さい。
いよいよ本題のコードです。
最初に述べた通り、画像処理を実際に行うのはImageData型のオブジェクトに対してでした。
canvas.getImageData()
でcanvasの画像をImageDataオブジェクトとして取得できます(画像処理を行う対象データ)。
画像読み込み後に行う必要があるので、前節のイベントリスナの中に書きます。
let srcImage: ImageData
image.addEventListener('load', () => {
ctxSrc.drawImage(image, 0, 0)
//前節の続き
srcImage = ctxSrc.getImageData(0, 0, SIZE, SIZE) //★
})
なお、getImageData()の引き数は、canvasから読み込む座標の位置と大きさを表しています。
今回はcanvas全体を読み込むのでこのようなコードとなっております。
ImageDataの各要素、すなわち各ピクセルについて変換処理を行います。
その前に、出力画像用のImageDataオブジェクトを別に用意する必要があります。
という手順を踏みます。
次のコードのようになります。
const ctxDir = canvasOut.getContext('2d')!
let outImage = ctxOut.createImageData(width, height) //ImageDataの作成
それでは、実際に画像処理を行うコードを1つ書きます。
今回は処理が単純な「グレースケール画像への変換」を行いたいと思います。
グレースケール画像の輝度の求め方はいくつかありますが、今回は次の公式を使用します。
輝度 = 0.299 * R + 0.587 * G + 0.144 * B
この輝度を出力画像のR、G、Bにそれぞれ同じ値としてセットすればグレースケール画像が得られます。
ImageData型が1次元の配列なので、ピクセルを走査する際に少し工夫が必要です。RGBαRGBαと連続して並んでいるので、各画素に4要素ずつ飛ばしてアクセスしていきます。
入力ImageDataをグレースケール変換して、画像処理の結果を返す関数のコードは次のようになります。
const grayScaleFilter = (image: ImageData) => {
const width = image.width
const height = image.height
const data = image.data
let outImage = ctxOut.createImageData(width, height)
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
const pos = 4 * (i * width + j)
const R = data[pos]
const G = data[pos + 1]
const B = data[pos + 2]
const lightness = 0.299 * R + 0.587 * G + 0.144 * B
outImage.data[pos] = lightness
outImage.data[pos + 1] = lightness
outImage.data[pos + 2] = lightness
outImage.data[pos + 3] = data[pos + 3]
}
}
return outImage
}
最後に、出力用のcanvasに処理した画像を描画します。putImage()関数を使います。
ctxOut.putImageData(outImage, 0, 0)
以上で画像が完了です。
実際にこのコードを適用した結果、次のようになりました。
シャープフィルタを用いたサンプルも掲載します。
なおこのコードは効率よりも、カーネルで畳み込みをしている部分が分かりやすいように可読性を重視しております。
const sharpnessFilter = (image: ImageData, ctxOut: CanvasRenderingContext2D) => {
const kernel = [
[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]
]
return spaceFilter(image, kernel, ctxOut)
}
const spaceFilter = (image: ImageData, kernel: number[][], ctxOut: CanvasRenderingContext2D) => {
const width = image.width
const height = image.height
const data = image.data
let outImage = ctxOut.createImageData(width, height)
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
const pos = 4 * (i * width + j)
let R = 0
let G = 0
let B = 0
for (let k = 0; k < 3; k++) {
for (let l = 0; l < 3; l++) {
if (!(i === 0 || i === height - 1 || j === 0 || j === width - 1)) {
const kpos = 4 * (((i - 1) + k ) * width + ((j -1) + l))
R += data[kpos] * kernel[k][l]
G += data[kpos + 1] * kernel[k][l]
B += data[kpos + 2] * kernel[k][l]
} else {
R += 255
G += 255
B += 255
}
}
}
outImage.data[pos] = R
outImage.data[pos + 1] = G
outImage.data[pos + 2] = B
outImage.data[pos + 3] = data[pos + 3]
}
}
return outImage
}
以下のようになりました。
codepenでデモを作りました。
デモで行っている画像処理は次の一覧のとおりです。
以上、ブラウザ上でライブラリを用いず画像処理を行う方法について解説しました。
全画素を並べた1次元配列なので、Pythonなどと比べて圧倒的に処理が面倒くさい印象です。
今回は画素値のみの変換でしたが、行列を使った変換などのコードは少々骨が折れそうです。
ですが、TSやJSによる画像処理はサーバを介さずにブラウザ上のみで画像処理が出来る恩恵があります。
最新の記事
カテゴリー一覧
アーカイブ
目次