p5.js で様々な画像処理を高速・手軽に行える p5.FIP（Fast Image Processing）を紹介したい！

p5.js で使える画像処理用のライブラリが良さそうだったので、紹介記事を書いてみます。

とりあえずカメラ映像に対して、様々な行える画像処理のうちの 1つ「グリッチエフェクト」をやってみたのが以下です。

https://x.com/youtoy/status/1808136221878898754

これ以外にも非常の多くの画像処理を扱えて、なおかつ処理が高速という、とても良さそうなライブラリです。

●prontopablo/p5.FIP: Real-time image processing library for p5.js.
　https://github.com/prontopablo/p5.FIP/tree/main?tab=readme-ov-file

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/41442c70-4549-cfb4-6bd2-03a2dc2e363b.png)

## p5.js で過去に画像処理をやった時の話
これまで、p5.js で画像処理を扱ったことがありました。

いろいろ実現方法はありますが、例えば以下があります。

- ピクセルを取得して、自前で画像処理
- カスタムシェーダーを利用
- OpenCV.js や他の画像処理ライブラリと p5.js を組み合わせる
- SVGフィルターを使う

上記は処理が重たかったり、処理が複雑になりがちだったりしました。

## p5.FIP（Fast Image Processing）
今回紹介する p5.FIP（Fast Image Processing） は、手軽に高速な画像処理を行えるライブラリです。

どんな処理が扱えるかは、公式のサンプルを触ってみると分かりやすいです。

### 全てのフィルタを試せる公式サンプル「All Filters」
実際に試せる、公式サンプルのページは以下です。

●p5.js Web Editor | p5.FIP Examples
　https://editor.p5js.org/prontopablo/collections/MA4R8jvck

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0258ae9d-9db3-9a52-0988-145bff3c4092.png)

4つのサンプルが掲載されており、それぞれ p5.js Web Editor上で開けるので、すぐに試すこともできます。

その中でも「All Filters」というサンプルは、全フィルターをキー操作で切り替えて試せます。

●p5.js Web Editor | All Filters
　https://editor.p5js.org/prontopablo/sketches/2-UYUk2qP

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/40cd5da6-e24e-8701-2f97-8b4789fc1cde.png)

実行後に左右キーを押すと、たくさんのフィルターを切り替えて試せます。

#### 試せるフィルタの種類
コードの中に、利用可能なフィルタの一覧があるので、それを抜粋してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2b7f2017-b4c3-7e20-a305-c1a81ff6c44c.png)

```javascript
let shaderNames = ["Anti-Aliasing", "Bilateral Filter", "Blend", "Bloom", "Box Blur", "Brightness", "Canny Edge Detection", "Cartoon", "Contrast", "CRT", "Deform", "Difference of Gaussian", "Dilate", "Dithering", "Dot", "Duo-tone", "Edge-Preserving Smooth", "Emboss", "Erosion", "Flip", "Gamma", "Gaussian Blur", "Glitch", "Grayscale", "Halftone", "Invert Colors", "Kuwahara", "Laplacian Edge Enhancement", "Linocut", "Mosaic", "Motion Blur", "Pixelate", "Quantization", "Ripple", "Rotate", "Saturation", "Sepia", "Sharpen", "Sketch",  "Sobel Edge Detection", "Solarize", "Static", "Threshold", "Unsharp Masking", "Vignette"];
```

これだけのフィルタを使うことができるのは、とても魅力的です。

また、各フィルタに関する説明は、以下のページの「Reference」の部分で見ることができます。

●p5.FIP
　https://prontopablo.github.io/p5.FIP/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/7057ccd0-207d-0b7c-1570-285df10d3b08.png)

## 高速処理を実現している部分
このライブラリの画像処理はかなり高速なのですが、それを実現している方法は、シェーダーを使っていることになるようです。

ソースコードの一部を、とびとびに抜粋してみます。

```javascript
    createCanvas(600, 600, WEBGL);

    // Apply the shader
    shader(customShaders[currentShaderIndex]);

    // Uniforms that most shaders need
    customShaders[currentShaderIndex].setUniform("texture", layer.color);
    customShaders[currentShaderIndex].setUniform('resolution', [width, height]);
    customShaders[currentShaderIndex].setUniform('uTextureSize', [width, height]);
```

2D の画像に処理を加えているサンプルですが、キャンバスは WEBGLモードの 3D対応のものになっています。

また、シェーダーを使う際にパラメータの受け渡しでおなじみの `setUniform()` がソースコード内で登場しています。

シェーダーの処理は隠蔽されているので、手軽にシェーダーを使ったエフェクトを扱うことができ、良い感じです。

### 一つのフィルタを使う場合の規模感
一つのフィルタを使う場合の、ソースコードの規模感を見てみます。

冒頭で試した内容のもとになっている「グリッチエフェクト」の公式サンプルは以下になります。

●p5.js Web Editor | Glitch
　https://editor.p5js.org/prontopablo/sketches/YZDodModH

```javascript
/* 
   Example sketch to show how the glitch filter works in FIP. 
*/

let layer,
  bird,
  glitch;

function preload() {
    glitch = createShader(fip.defaultVert, fip.glitch); // Load the glitch shader
    bird = loadImage("bird.jpg");
}

function setup() {
    createCanvas(600, 600, WEBGL); // Use WEBGL mode to use the shader
    layer = createFramebuffer(); // Create a framebuffer to draw the image onto (faster p5.js version of createGraphics())
}
  
function draw() {
    background(0);
    
    // Draw an image to a framebuffer 
    layer.begin();
    clear();
    scale(1, -1); // Flip the Y-axis to match the canvas (different coordinate system in framebuffer)
    image(bird, -width / 2, -height / 2, width, height);
    layer.end();
    
    // Apply the shader
    shader(glitch);
    
    // Set the shader uniforms
    glitch.setUniform('glitchIntensity', 0.8); // Set the intensity of the glitch effect
    glitch.setUniform("texture", layer.color); // Set the texture to apply the shader to

    rect(0, 0, width, height); // Draw a rectangle to apply the shader to
    resetShader(); 
}
```

コメント付きで上記の規模感なので、かなりシンプルに利用できる感じだと思います。

また、フィルタのパラメータとして `glitch.setUniform('glitchIntensity', 0.8)` という設定がされていますが、これを増やしてみるとフィルタの効果が分かりやすいです。
例えば、1.8 という値にしてみると、デフォルトの値の時と比べてかなり

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/57fdbc89-4008-7617-f025-6642df853caf.png)

## All Filters のソースコード
最後に、公式サンプル「All Filters」のソースコードを掲載してみます。

全フィルタを羅列しているので、けっこう長めの内容になっています。

```javascript
/* 
   Example sketch to show how the all of the filters works in FIP. 
   Left and right arrow keys cycle filters, up and down arrow keys cycle images.
*/

let layer, layer1, layer2,
  ireland,
  bird,
  currentShaderIndex = 0,
  currentImageIndex = 0,
  images = [],
  customShaders = [];

let shaderNames = ["Anti-Aliasing", "Bilateral Filter", "Blend", "Bloom", "Box Blur", "Brightness", "Canny Edge Detection", "Cartoon", "Contrast", "CRT", "Deform", "Difference of Gaussian", "Dilate", "Dithering", "Dot", "Duo-tone", "Edge-Preserving Smooth", "Emboss", "Erosion", "Flip", "Gamma", "Gaussian Blur", "Glitch", "Grayscale", "Halftone", "Invert Colors", "Kuwahara", "Laplacian Edge Enhancement", "Linocut", "Mosaic", "Motion Blur", "Pixelate", "Quantization", "Ripple", "Rotate", "Saturation", "Sepia", "Sharpen", "Sketch",  "Sobel Edge Detection", "Solarize", "Static", "Threshold", "Unsharp Masking", "Vignette"];

function preload() {
    // Load the shaders during preload
    customShaders.push(createShader(fip.defaultVert, fip.antiAliasing));
    customShaders.push(createShader(fip.defaultVert, fip.bilateral));
    customShaders.push(createShader(fip.defaultVert, fip.blend));
    customShaders.push(createShader(fip.defaultVert, fip.bloom));
    customShaders.push(createShader(fip.defaultVert, fip.boxBlur));
    customShaders.push(createShader(fip.defaultVert, fip.brightness));
    customShaders.push(createShader(fip.defaultVert, fip.cannyEdgeDetection));
    customShaders.push(createShader(fip.defaultVert, fip.cartoon));
    customShaders.push(createShader(fip.defaultVert, fip.contrast));
    customShaders.push(createShader(fip.defaultVert, fip.crt));
    customShaders.push(createShader(fip.defaultVert, fip.deform));
    customShaders.push(createShader(fip.defaultVert, fip.differenceOfGaussian));
    customShaders.push(createShader(fip.defaultVert, fip.dilate));
    customShaders.push(createShader(fip.defaultVert, fip.dithering));
    customShaders.push(createShader(fip.defaultVert, fip.dot));
    customShaders.push(createShader(fip.defaultVert, fip.duoTone));
    customShaders.push(createShader(fip.defaultVert, fip.edgePreservingSmooth));
    customShaders.push(createShader(fip.defaultVert, fip.emboss));
    customShaders.push(createShader(fip.defaultVert, fip.erosion));
    customShaders.push(createShader(fip.defaultVert, fip.flip));
    customShaders.push(createShader(fip.defaultVert, fip.gamma));
    customShaders.push(createShader(fip.defaultVert, fip.gaussianBlur));
    customShaders.push(createShader(fip.defaultVert, fip.glitch));
    customShaders.push(createShader(fip.defaultVert, fip.grayscale));
    customShaders.push(createShader(fip.defaultVert, fip.halftone));
    customShaders.push(createShader(fip.defaultVert, fip.invertColors));
    customShaders.push(createShader(fip.defaultVert, fip.kuwahara));
    customShaders.push(createShader(fip.defaultVert, fip.laplacianEdgeEnhancement));
    customShaders.push(createShader(fip.defaultVert, fip.linocut));
    customShaders.push(createShader(fip.defaultVert, fip.moasic));
    customShaders.push(createShader(fip.defaultVert, fip.motionBlur));
    customShaders.push(createShader(fip.defaultVert, fip.pixelate));
    customShaders.push(createShader(fip.defaultVert, fip.quantization));
    customShaders.push(createShader(fip.defaultVert, fip.ripple));
    customShaders.push(createShader(fip.defaultVert, fip.rotate));
    customShaders.push(createShader(fip.defaultVert, fip.saturation));
    customShaders.push(createShader(fip.defaultVert, fip.sepia));
    customShaders.push(createShader(fip.defaultVert, fip.sharpen));
    customShaders.push(createShader(fip.defaultVert, fip.sketch));
    customShaders.push(createShader(fip.defaultVert, fip.sobelEdgeDetection));
    customShaders.push(createShader(fip.defaultVert, fip.solarize));
    customShaders.push(createShader(fip.defaultVert, fip.static));
    customShaders.push(createShader(fip.defaultVert, fip.threshold));
    customShaders.push(createShader(fip.defaultVert, fip.unsharpMasking));
    customShaders.push(createShader(fip.defaultVert, fip.vignette));
    
    // Load the images during preload
    images[0] = loadImage("ireland.jpg");
    images[1] = loadImage("bird.jpg");
}

function setup() {
    createCanvas(600, 600, WEBGL);
    layer = createFramebuffer();
    layer1 = createFramebuffer();
    layer2 = createFramebuffer();
    noStroke();
  
    console.log("Press the left and right arrow keys to change the shader.");
    console.log("Press the up and down arrow keys to change the image.");
    console.log("Current shader: " + shaderNames[currentShaderIndex]);
}
  
function draw() {
    background(0);
    
    // Draw a scene to a framebuffer
    layer.begin();
    clear();
    lights();
    scale(1, -1);
    image(images[currentImageIndex], -width / 2, -height / 2, width, height);
    layer.end();
    
    // Create a second framebuffer for blending
    layer1.begin();
    clear();
    lights();
    scale(1, -1);
    image(images[0], -width / 2, -height / 2, width, height);
    layer1.end();
    
    // Create a third framebuffer for blending
    layer2.begin();
    clear();
    lights();
    scale(1, -1);
    image(images[1], -width / 2, -height / 2, width, height);
    layer2.end();
    
    // Apply the shader
    shader(customShaders[currentShaderIndex]);

    // Set the uniforms for the shaders
    switch (currentShaderIndex) {
        case 0:
            customShaders[currentShaderIndex].setUniform('strength', 0.9); // Anti-Aliasing
            break;
        case 1:
            customShaders[currentShaderIndex].setUniform('sigmaSpace', 1.0); // Bilateral
            customShaders[currentShaderIndex].setUniform('sigmaColor', 0.8);
            break;
        case 2:
            customShaders[currentShaderIndex].setUniform('texture1', layer1.color); // Blend
            customShaders[currentShaderIndex].setUniform('texture2', layer2.color);
            customShaders[currentShaderIndex].setUniform('mixFactor', 0.5);
            customShaders[currentShaderIndex].setUniform('blendingMode', 0);
            break;
        case 3:
            customShaders[currentShaderIndex].setUniform('intensity', 0.8); // Bloom
            customShaders[currentShaderIndex].setUniform('glow', 1.0);
            break;
        case 4:
            customShaders[currentShaderIndex].setUniform('blurRadius', 3); // Box Blur
            break;
        case 5:
            customShaders[currentShaderIndex].setUniform('brightness', 2.1); // Brightness
            break;
        case 6:
            customShaders[currentShaderIndex].setUniform('thresholdLow', 0.1);  // Canny Edge Detection
            customShaders[currentShaderIndex].setUniform('thresholdHigh', 0.3);
            break;
        case 7:
            customShaders[currentShaderIndex].setUniform('edgeThreshold', 0.1); // Cartoon
            break;
        case 8:
            customShaders[currentShaderIndex].setUniform('contrast', 2.0); // Contrast
            break;
        case 9:
            customShaders[currentShaderIndex].setUniform('thresholdLow', 0.1); // CRT
            customShaders[currentShaderIndex].setUniform('thresholdHigh', 0.3);
            customShaders[currentShaderIndex].setUniform('scanlineWeight', 0.1);
            customShaders[currentShaderIndex].setUniform('brightness', 2.5);
            customShaders[currentShaderIndex].setUniform('distortion', 0.02);
            break;
        case 10:
            customShaders[currentShaderIndex].setUniform('deformationAmount', 0.1); // Deform
            break;
        case 11:
            customShaders[currentShaderIndex].setUniform('radius1', 1.0); // Difference of Gaussian
            customShaders[currentShaderIndex].setUniform('radius2', 2.0);
            break;
        case 13:
            customShaders[currentShaderIndex].setUniform('threshold', 1.0); // Dithering 
            break;
        case 14:
            customShaders[currentShaderIndex].setUniform('dotSize', 0.008); // Dot
            break;
        case 15:
            customShaders[currentShaderIndex].setUniform('tone1', [0.8627, 0.6275, 0.0]); // Duo-tone
            customShaders[currentShaderIndex].setUniform('tone2', [0.4157, 0.0118, 0.5647]);
            break;
        case 16:
            customShaders[currentShaderIndex].setUniform('threshold', 0.2); // Edge-Preserving Smooth
            break;
        case 19:
            customShaders[currentShaderIndex].setUniform("flipHorizontal", true); // Flip
            customShaders[currentShaderIndex].setUniform("flipVertical", true);
            break;
        case 20:
            customShaders[currentShaderIndex].setUniform('gamma', 2.2); // Gamma
            break;
        case 22:
            customShaders[currentShaderIndex].setUniform('glitchIntensity', 0.8); // Glitch
            break;
        case 24:
            customShaders[currentShaderIndex].setUniform('cellSize', 4.0); // Halftone
            customShaders[currentShaderIndex].setUniform('threshold', 0.2);
            break;
        case 27:
            customShaders[currentShaderIndex].setUniform('amount', 5.5); // Laplacian Edge Enhancement
            break;
        case 28:
            customShaders[currentShaderIndex].setUniform('threshold', 0.4); // Linocut
            customShaders[currentShaderIndex].setUniform('inkColor', [0.4, 0.4, 1.0]);
            customShaders[currentShaderIndex].setUniform('paperColor', [1.0, 1.0, 1.0]);
            break;
        case 29:
            customShaders[currentShaderIndex].setUniform('mosaicSize', 12.0); // Mosaic
            break;
        case 31:
            customShaders[currentShaderIndex].setUniform('pixelSize', 0.01); // Pixelate
            break;
        case 32:
            customShaders[currentShaderIndex].setUniform('shades', 4.0); // Quantization
            break;
        case 33:
            customShaders[currentShaderIndex].setUniform('rippleFrequency', 50.0); // Ripple
            customShaders[currentShaderIndex].setUniform('rippleAmplitude', 0.01);
            break;
        case 34:
            customShaders[currentShaderIndex].setUniform("rotationAngle", -45); // Rotate
            break;
        case 35:
            customShaders[currentShaderIndex].setUniform('saturation', 5.5);  // Saturation
            break;
        case 37:
            customShaders[currentShaderIndex].setUniform('sharpness', 1.5); // Sharpen
            break;
        case 38:
            customShaders[currentShaderIndex].setUniform('threshold', 0.2); // Sketch
            break;
        case 39:
            customShaders[currentShaderIndex].setUniform('threshold', 0.2); // Sobel Edge Detection
            break;
        case 40:
            customShaders[currentShaderIndex].setUniform('threshold', 0.5); // Solarize
            break;
        case 41:
            customShaders[currentShaderIndex].setUniform('threshold', 0.2); // Static
            customShaders[currentShaderIndex].setUniform('stippleDensity', 0.99);
            break;
        case 42:
            customShaders[currentShaderIndex].setUniform('threshold', 0.5);  // Threshold
            break;
        case 43:
            customShaders[currentShaderIndex].setUniform('strength', 2.0); // Unsharp Masking
            break;
        case 44:
            customShaders[currentShaderIndex].setUniform('vignetteStrength', 0.3); // Vignette      
            customShaders[currentShaderIndex].setUniform('vignetteFalloff', 1.0);
            customShaders[currentShaderIndex].setUniform('vignetteSign', 1.0);
            customShaders[currentShaderIndex].setUniform('vignetteSize', 1.0);
            break;
        default:
            break;
    }
    
    // Uniforms that most shaders need
    customShaders[currentShaderIndex].setUniform("texture", layer.color);
    customShaders[currentShaderIndex].setUniform('resolution', [width, height]);
    customShaders[currentShaderIndex].setUniform('uTextureSize', [width, height]);

    rect(0, 0, width, height);
    resetShader();
    fill(0);
}

function keyPressed(){
    if (keyCode == RIGHT_ARROW){
        currentShaderIndex ++;
        if (currentShaderIndex >= customShaders.length){
            currentShaderIndex = 0;
        }
        console.log("Current shader: " + shaderNames[currentShaderIndex]);
    } else if (keyCode == LEFT_ARROW){
        currentShaderIndex --;
        if (currentShaderIndex < 0){
            currentShaderIndex = customShaders.length - 1;
        }
        console.log("Current shader: " + shaderNames[currentShaderIndex]);
    } else if (keyCode == UP_ARROW){
      currentImageIndex ++;
      if (currentImageIndex >= images.length){
        currentImageIndex = 0;
      }
    } else if (keyCode == DOWN_ARROW){
        currentImageIndex --;
        if (currentImageIndex < 0){
            currentImageIndex = images.length - 1;
        }
    }
}
```