thumbnail

【Jimp】TypeScriptでブラウザ上の画像処理【ライブラリ使用】

JavaScriptの画像処理ライブラリ「Jimp」をTypeScriptで使い、ブラウザ上で画像処理を行う方法を説明します。Jimpは画像処理のアルゴリズムを知らない人も簡単に画像処理ができますし、独自でカーネルを定義できたりピクセル単位の操作が簡単に行えるので、ある程度のカスタマイズができて拡張性があります。

この記事で説明すること

  • TypeScript & Jimpをブラウザで使うためのセットアップの方法
  • Jimpの基本的な使い方の紹介(用意されているフィルタの説明)
  • Jimpの発展的な使い方の紹介

Jimpの使い方について、TypeScriptのコードと実際の画像処理結果を共に記載します。

(前置き)TypeScriptで画像処理

前置きなので使用方法だけが知りたい方は次の章へ飛ばして下さい。

ライブラリを使わない画像処理

以前「TypeScriptを用いたブラウザ上の画像処理【ライブラリ不使用】」ということで、ライブラリを使わないで画像処理を行う記事を作成しました。興味のある方はご参照下さい。

画像を1次元配列として操作する必要があったり、入力出力周りが面倒だったり、正直かなり画像処理が行いにくい言語です。

Jimpは初心者から上級者まで使える

また、画像処理は大学数学レベルの内容になってくるので、初心者がとっつきにくい分野でもあると思います。

そこでJimpです。行いたい画像処理を関数名で呼び出してパラメータを調整する、という直感的な操作ができます。
用意されているフィルターより複雑な処理をしたくなったら、カーネルを定義したりピクセル単位で操作を行えるので、ある程度カスタマイズも出来ます。また、画像の類似度を計算する機能等もあります。

有名なライブラリであるOpenCVほど複雑な処理はできませんが、画像の簡単な加工や変換だけの用途ならJimpのほうが楽です。

ブラウザ上&TypeScriptでJimpを使う準備

インストールからインポートまで

なにはともあれ、まずはnpmでJimpをインストールします。

npm install --save jimp

TSファイルでimportしますが、ブラウザ上で画像処理が行いたい場合は次のimport先にします。

import Jimp from "jimp/browser/lib/jimp"

ブラウザ上で画像処理を行うコード

今回は、inputフォームで選択したファイルに対してブラウザ上で画像処理を行うことにします。
入力も出力もimgタグとします。
このあたりの決まりはそれぞれでカスタマイズして下さい。

<input type="file" id="file">
<img src="" alt="入力画像" class="inputImage">
<img src="" alt="出力" class="outputImage">
import Jimp from "jimp/browser/lib/jimp"

document.querySelector<HTMLInputElement>('#file')?.addEventListener('change',  e => {
  const target = e.target as HTMLInputElement
  if (target.files !== null)  {
    const file = target.files[0]
    const url = URL.createObjectURL(file)
    document.querySelector<HTMLImageElement>('.inputImage')!.src = url
    Jimp.read(url).then(image => {

      // ここから画像処理を書く
      /////  画像処理  /////
      //ここまで

      image.getBase64(Jimp.MIME_JPEG, (error, img) => {
        document.querySelector<HTMLImageElement>('.outputImage')!.src = img
      })
    })
  }
}, false)

たったのこれだけです。フォーム周りの処理の説明は割愛します。
Jimpで行っている処理は2つです

①画像の読み込み

Jimp.read(url).then(image => {/**画像処理 */})

Jimp.readに画像のURLを指定して読み込みを行っています。Promiseを返すのでthenでつなげており、コールバック関数内で画像処理を行います。コールバックの引数(上のコードだとimage)が処理対象の画像になります。画像はライブラリ固有のJimp型です。

②画像の出力

image.getBase64(Jimp.MIME_JPEG, (error, img) => {
  document.querySelector<HTMLImageElement>('.outputImage')!.src = img
})

このコードは画像をどの形式で出力するかによって変わってくるので、あくまで参考程度にとらえてください。

getBase64で画像のData URIが取得でき、コールバック内でアウトプット用のimgタグのsrc属性にセットしています。これによって、処理結果の画像がimgタグとしてブラウザ上で出力されます。

それでは、実際に画像処理を行います。

Jimpの書式

Jimpのコードは基本的に、

image.フィルタ(パラメータ)

の書式です。フィルタを何個も適用したい場合はチェーンして書きます。

Jimp.read(url).then(image => {

  image.フィルター1()
    .フィルター2()
    .フィルター3()
    
  image.getBase64(Jimp.MIME_JPEG, (error, img) => {
    document.querySelector<HTMLImageElement>('.outputImage')!.src = img
  })
})

基本的なフィルターの説明

それではJimpで予め用意されているフィルターを紹介します。
入力画像は次のお寿司の画像にします。

リサイズ

①contain

画像の大きさを変更します。
入力画像は、指定した幅と高さに収まるようにリサイズされます。足りない部分は黒で埋め尽くされます。

image.contain(幅 , 高さ [, Alignモード, Resizeモード])

結果: image.contain(500,300)

元画像より小さいサイズを指定して、画像を縮小しました。
足りない部分は黒で塗りつぶされています。

Alignモードについて

省略可能なパラメータです。
Alignモードは画像を揃える位置を指定します。例えば Jimp.HORIZONTAL_ALIGN_LEFT;とすると左添えになります。

結果:image.contain(500, 300, Jimp.HORIZONTAL_ALIGN_LEFT)

この他にも揃える位置について次のAlignモードがあります。それぞれの意味は読んで字の如くです。

Jimp.HORIZONTAL_ALIGN_LEFT;
Jimp.HORIZONTAL_ALIGN_CENTER;
Jimp.HORIZONTAL_ALIGN_RIGHT;
 
Jimp.VERTICAL_ALIGN_TOP;
Jimp.VERTICAL_ALIGN_MIDDLE;
Jimp.VERTICAL_ALIGN_BOTTOM;

Resizeモードについて

これも省略可能なパラメータです。リサイズの際のアルゴリズムを指定します。
画像処理に詳しくない方は無視しても構いません。それぞれのアルゴリズムは読んで字の如く。

Jimp.RESIZE_NEAREST_NEIGHBOR;
Jimp.RESIZE_BILINEAR;
Jimp.RESIZE_BICUBIC;
Jimp.RESIZE_HERMITE;
Jimp.RESIZE_BEZIER;

デフォルトはバイリニアです。

②cover

画像の大きさを変更します。
入力画像は、指定した幅と高さいっぱいに埋め尽くされます。比率は一定のままです。

image.cover(幅 , 高さ [, Alignモード, Resizeモード])

結果: image.cover(500, 300)

③resize

画像の大きさを変更します。
入力画像は指定した高さと幅になります。比率も変わります。

image.resize(幅 , 高さ [, Resizeモード])

結果: image.resize(500, 300)

幅、高さの一方のみを指定して、一定の比率でのリサイズもできます。
その際、指定しない方のパラメータはJimp.AUTOと指定します。

④scale

画像の大きさを変更します。
比率は一定で[パラメータ]倍します。

image.scale(倍率 [, Resizeモード])

クロップ

①crop

画像の一部分を切り取ります。

image.crop(x座標, y座標, 幅, 高さ)

結果:image.crop(100, 100, 200, 200)

②autocrop

同じ色の線を自動的にクロップしますが、使い勝手と結果があまり良くないので割愛します。

2枚の画像を重ねた処理(重ねて表示、ブレンド、マスク)

①blit

画像の上に画像を重ねます。

image1.blit(image2, x座標, y座標[, image2のx座標, 2のy座標, 2の幅, 2の高さ])

image2もJimp形式の画像である必要があります。

結果: image.blit(image2, 100, 100)

②composite

2つの画像を合成(ブレンド)します。PhosoShopの描画モードのようなものです。

image.composite(image2, 0, 0, { // image2のx座標とy座標
  mode: Jimp.BLEND_MULTIPLY, //モード
  opacitySource: 1, //image2の透明度
  opacityDest: 1 //image1の透明度
});

お寿司の画像に次のグラデーション画像を合成してみます。

結果

描画モードは他にも次のようなものがあります。

Jimp.BLEND_SOURCE_OVER;
Jimp.BLEND_DESTINATION_OVER;
Jimp.BLEND_MULTIPLY;
Jimp.BLEND_ADD;
Jimp.BLEND_SCREEN;
Jimp.BLEND_OVERLAY;
Jimp.BLEND_DARKEN;
Jimp.BLEND_LIGHTEN;
Jimp.BLEND_HARDLIGHT;
Jimp.BLEND_DIFFERENCE;
Jimp.BLEND_EXCLUSION;

③mask

2枚の画像の平均画素値を求めます。2値画像を使えばマスクできます。

image.mask(image2, x座標, y座標)

例として次の画像でマスクしてみます。

結果: image.mask(image2, 0, 0)

変形(反転、回転)

① flip / mirror

左右、上下に反転します。mirrorはflipのエイリアスです。

image.flip(左右に反転するか: boolean, 上下に反転するか: boolean)   

結果:image.flip(false, true)

② rotate

画像を時計回りに回転します。

image.rotate(角度, リサイズするか: boolean) 

第2パラメータをtrueにすると回転した画像がリサイズされます。
falseにするとリサイズされず、回転によりはみ出た部分は切り取られます。

結果:image.rotate(30, true)

色変換(明度、コントラスト、ディザリング、グレースケール、ネガポジ反転、正規化)

① brightness

明度を変更します。パラメータは-1 ~ 1で指定し、0より大きい場合は明るく、小さい場合は暗くなります。

image.brightness(パラメータ)

結果:image.brightness(0.6)

② contrast

コントラストを変更します。パラメータは-1 ~ 1で指定し、0より大きい場合はコントラストが強く、0より小さい場合は弱くなります。

image.contrast(パラメータ)

結果:image.contrast(0.6)

③ disher565

ディザリングをして、色空間を16bitに縮小します。

image.dither565()

④ greyscale

グレースケール画像に変換します。

image.greyscale()

結果:

⑤ invert

ネガポジ反転します。

image.invert()

⑥ normalize

画像のチャネルを正規化します。

image.normalize()

これだけ?と思われるかもしれませんが、他の色変換処理は勝手が違うので、次章で説明します。

透明度変換

① fade / opacity

画像の透過度を設定します。パラメータは0 〜1の範囲を取ります。

image.fade(パラメータ)

結果:image.fade(0.5)

② opaque

画像を不透明にします。

image.opaque()

ぼかし

① gaussian

ガウシアンフィルタをかけます。ただし処理はとても遅いです。

image.gaussian(ピクセル値)

結果:image.gaussian(6)

② blur

ガウシアンフィルタより高速にぼかします。

image.blur(ピクセル値)

エフェクト(ポスタリゼーション、セピア、モザイク)

① posterize

ポスタリゼーションします。パラメータでレベルを指定します。

image.posterize(レベル: number)

結果: image.posterize(4)

② sepia

セピアにします。

image.sepia()

結果:

③ pixelate

ピクセレーションします。いわゆるモザイク処理です。部分的にモザイクをかけることも可能です。

image.pixelate(サイズ [,x座標, y座標, 幅, 高さ])

結果: image.pixelate(20, 100, 100, 300, 300)

画質の操作

① quality

JPEGの画質を変更できます。パラメータは0 ~ 100を取ります。

image.quality(パラメータ)

結果:image.quality(1)

様々な色変換を詳細に行う

colorメソッドを用いると、色の変換を細かく調整できます。

image.color([
  { apply: 'hue', params: [-90] },
  { apply: 'lighten', params: [50] },
  { apply: 'xor', params: ['#06D'] }
])

colorにはapplyとparamsをキーに持つオブジェクトの配列を渡します。
applyには行いたい処理をstring型で、paramsにはパラメータを配列で渡します。

applyで指定できる処理は以下のとおりです。

applyparams説明
lighten0 ~ 100明るくする
brighten0 ~ 100明るくする
darken0 ~ 100暗くする
desaturate0 ~ 100彩度を下げる
saturate0 ~ 100彩度を上げる
greyscaleグレースケールにする
spin-360 ~ 360色相を回転させる
hue-360 ~ 360spinのエイリアス
mix色(hex), 0 ~ 100指定した色でオーバーレイ。数値は透明度。
tint0 ~ 100白とmixしたのと同じ
shade0 ~ 100黒とmixしたのと同じ
xor色(hex)指定した色とxor演算
red0 ~ 赤を強める
green0 ~ 緑を強める
blue0 ~青を強める

前章の説明にない画像処理を行った結果をいくつか載せます。

① 色相の回転

結果 { apply: ‘hue’ , params:[60] }

② 彩度の変更

結果: { apply: ‘saturate’, params: [50] }

③ RGB成分を強く

結果: { apply: ‘blue’, params: [80] }

空間フィルタリング(畳み込み)

カーネルを指定して空間フィルタリング(畳み込み演算)ができます。
カーネルは3 x 3の2次元配列で指定します。

image.convolute(3 x 3の2次元配列)

具体的な使用例として、上で行っていない処理を2つほど紹介します。

① シャープ

結果: image.convolute([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])

② エンボス

結果: image.convolute([[-2, -1, 0], [-1, 1, 1], [0, 1, 2]])

ピクセル値の操作

ピクセルレベルの操作もできます。

scanメソッドを使い画像をラスタスキャンすることで、ピクセルレベルでRGBα値を操作できます。

image.scan(開始位置x, 開始位置y, 走査幅, 走査高, コールバック)

scanする範囲を引数で指定できます。また、コールバックでスキャン中の画素に対してアクセスします。

image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) {
  // 画像をピクセルレベルで左上から右下まで走査(ラスタスキャン)
  // x, y は画像上のピクセルの2次元的な位置
  // idx は、画素を1列に並べたときの、1次元的な画素位置
  
  // idx番目のとき、R、G、B、αへは以下でアクセスできる
  let R = this.bitmap.data[idx + 0];  
  let G = this.bitmap.data[idx + 1];
  let B = this.bitmap.data[idx + 2];
  let alpha = this.bitmap.data[idx + 3];

});  

ここで、上のコードのように、image.bitmap.widthで画像幅、image.bitmap.heightで画像の高さ、 image.bitmap.dataで画素にアクセスできます。

コールバック内での画素へのアクセス方法はImageData型と同じです。

(参考)ImageData型

その他のトピック

以上で一通りの画像処理を紹介しましたが、Jimpでは次のようなこともできます。

  • 画像の生成
  • pHashアルゴリズムによる画像の類似度の計算
  • テキスト文字

興味のある方は、公式のドキュメントをお読みください

まとめ

JavaScriptの画像処理ライブラリ「Jimp」をTypeScriptで使い、ブラウザ上で画像処理を行う方法を説明し、実際の処理結果とともに記載しました。紹介したとおり簡単に画像処理が行なえますし、空間フィルタリングやピクセル走査も簡単に行えます。基本的な画像処理のみ行いたいなら十分な機能を備えているでしょう。