2017年12月12日火曜日

dotrace.jsの使い方・SVGにおける多色画像のHSLAビット合成的アプローチ

前回作ったdotrace.jsの使い方・SVGにおける多色画像のRGBAビット合成的アプローチRGBA画像をHSLA画像に変換してSVGフィルタで元画像を復元とを組み合わせてみたところ, それはもう素晴らしい出力結果が得られたので公開する.

従来SVGでは実現が困難と考えられてきた多階調の自然画風グラフィックを無理やりベクタ化する際の一つのアプローチとして非常に効果的である.

NOTE:2017/12/13
但し本スクリプトでの出力結果はSVG本来のメリットであるスケール非依存性が失われるため, 使い勝手は悪い.


dotrace.js+jimp+nodeコード


"use strict";
(async () => {
 const Dotrace = require("./dotrace.core.js");
 const Jimp = require("jimp");
 const fn = process.argv[2];
 if(!fn){return;}
 const img = await Jimp.read(fn);
 const bmp = img.bitmap;
 const data = toHSL(Dotrace.normalizePixel(bmp.data));
 let paths = "";
 for(let i = 0; i<4; i++){
  for(let m = 0; m<5; m++){
   const d = data.map((val, j) => i == (j%4) ? val & (1 << (m+3)) : 0);
   const entries = Dotrace.trace(d, bmp.width, bmp.height).entries();
   const entrie = entries.next();
   paths += entrie.done
    ? "" 
    : ((color, val) => 
     `<path transform="translate(${bmp.width * m},${bmp.height * i})" ${i != 3 ? 'fill="#' + Dotrace.formatColor(color) + '"' : 'opacity="' + Dotrace.opacity(color) + '"'} d="${val}"/>
`)(...entrie.value);
  }
 }
 const svg = 
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${bmp.width} ${bmp.height}" width="${bmp.width}" height="${bmp.height}">
<defs>
<filter id="f" x="0" y="0" width="1" height="1" primitiveUnits="objectBoundingBox" color-interpolation-filters="sRGB">
<feOffset in="SourceGraphic" x="0" y="0" width=".2" height="1" dx="0"   result="b1"/>
<feOffset in="SourceGraphic" x="0" y="0" width=".2" height="1" dx="-.2" result="b2"/>
<feOffset in="SourceGraphic" x="0" y="0" width=".2" height="1" dx="-.4" result="b3"/>
<feOffset in="SourceGraphic" x="0" y="0" width=".2" height="1" dx="-.6" result="b4"/>
<feOffset in="SourceGraphic" x="0" y="0" width=".2" height="1" dx="-.8" result="b5"/>
<feComposite in="b1" in2="b2" operator="arithmetic" k2="1" k3="1"/>
<feComposite         in2="b3" operator="arithmetic" k2="1" k3="1"/>
<feComposite         in2="b4" operator="arithmetic" k2="1" k3="1"/>
<feComposite         in2="b5" operator="arithmetic" k2="1" k3="1"/>
<feComponentTransfer result="rgba">
<feFuncR type="linear" slope="1.028225806"/>
<feFuncG type="linear" slope="1.028225806"/>
<feFuncB type="linear" slope="1.028225806"/>
<feFuncA type="linear" slope="1.028225806"/>
</feComponentTransfer>
<feOffset in="rgba" x="0" y="0" width=".2" height=".25" dy="0"    result="r"/>
<feOffset in="rgba" x="0" y="0" width=".2" height=".25" dy="-.25" result="g"/>
<feOffset in="rgba" x="0" y="0" width=".2" height=".25" dy="-.5"  result="b"/>
<feOffset in="rgba" x="0" y="0" width=".2" height=".25" dy="-.75" result="a"/>
<feColorMatrix in="r" values="
 1 0 0 0 0 
 1 0 0 0 0 
 1 0 0 0 0 
 0 0 0 1 0" />
<feComponentTransfer result="h">
 <feFuncR type="table" tableValues="1,1,1,0.5,0,0,0,0,0,0.5,1,1,1" />
 <feFuncB type="table" tableValues="0,0,0,0,0,0.5,1,1,1,1,1,0.5,0" />
 <feFuncG type="table" tableValues="0,0.5,1,1,1,1,1,0.5,0,0,0,0,0" />
</feComponentTransfer>
<feColorMatrix in="g" values="
 0 -0.5 0 0 0.5 
 0  0.5 0 0 0.5 
 0 -0.5 0 0 0.5 
 0  0   0 1 0  " result="s" />
<feColorMatrix in="b" values="
 0 0 1 0 0 
 0 0 1 0 0 
 0 0 1 0 0 
 0 0 0 1 0" result="l" />
<feBlend in="h" in2="l" mode="color" result="hl"/>
<feBlend in="s" in2="hl" mode="saturation" result="hsl"/>
<feComposite in2="a" operator="atop"/>
</filter>
<g id="g" filter="url(#f)">
<rect width="500%" height="300%"/>
<rect fill="none" y="300%" width="500%" height="100%"/>
${paths}</g>
</defs>
<use xlink:href="#g"/>
</svg>`;
 process.stdout.write(svg);

 function toHSL(od){
  const d = new Uint8Array(od.length);
  for(let i = 0, len = od.length/4; i < len; i++){
   const p = i * 4;
   d[p    ] = rgbtoh(od[p], od[p+1], od[p+2]);//h
   d[p + 1] = rgbtos(od[p], od[p+1], od[p+2]);//s
   d[p + 2] = rgbtol(od[p], od[p+1], od[p+2]);//h
   d[p + 3] = od[p + 3];//a
  }
  return d;
 };
 function rgbtoh(r, g, b) {
  const [min, max] = [Math.min, Math.max].map(f => f(r, g, b));
  const diff = max - min, sum = max + min;
  const h = diff != 0 ?
   (
    b == min ? (g - r) / diff + 1 :
     r == min ? (b - g) / diff + 3 :
      (r - b) / diff + 5
   ) * 255 / 6 : 0;
  return Math.floor(h);
 }
 function rgbtos(r, g, b) {
  const [min, max] = [Math.min, Math.max].map(f => f(r, g, b));
  return max - min;
 }
 function rgbtol(r, g, b){
  return 0.3 * r + 0.59 * g + 0.11 * b;
 }
})();

出力結果例

動作原理
  • 元となる画像よりRGBA値を取得し, CSSのmix-blend-mode仕様に基づくHSLA値(0〜255)に変換する
  • HSLAの各要素毎に上位ビットが立っているピクセルを抽出し, ベクタ化する
  • SVGフィルタを用いてビット毎に分解したベクタ図形を算術加算合成する
  • 省いたbit数の分出力値を調整したのち, HSL合成する
メリット
  • 多色表現を行う際のpath要素の数が少なくて済む
    計算上5+5+5(+5)の15(20)要素で(2^5)^3=32768色を表現できる
    これは既にpath要素のみで表現できるキャパシティを超えている
    ※HSL色空間を用いたため, 実際にはもうちょっと少ない
  • ドット絵トレース形式だとsvgz形式で圧縮効率が良い
    元の色を上手く採ると元画像よりもファイルサイズが小さくなることもありうる
  • RGBによる出力よりもグラデーション部が自然に見える 
デメリット
  • グラフィックの表示速度が実用性ギリギリである
    大きな画像には適さない
    ・・・が, 一旦canvas要素に転写すれば二度目以降の描画コストは無視できる.

0 件のコメント:

コメントを投稿