Can I retrieve the count of pixels below a certain threshold value using WebGL/OpenGL?

class WebGLProcessor {
    initializeGLContext = (canvas, version) => {
        canvas.width = window.innerWidth * 0.99;
        canvas.height = window.innerHeight * 0.85;
        var gl = canvas.getContext(version ? 'webgl' : 'webgl2');
        const ext = gl.getExtension("EXT_color_buffer_float");
        if (!ext) {
            console.log("Unable to render to floating point textures.");
        }
        gl.clearColor(0, 0, 0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
        gl.lineWidth(0.5);
        return gl;
    };

    clearBuffer = (gl) => {
        gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
    };

    createShader = (gl, type, shaderText) => {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, shaderText);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            console.error(gl.getShaderInfoLog(shader));
        }
        return shader;
    };

    createProgram = (gl, vertexShader, fragmentShader) => {
        var program = gl.createProgram();
        gl.attachShader(program, this.createShader(gl, gl.VERTEX_SHADER, document.getElementById(vertexShader).text.trim()));
        gl.attachShader(program, this.createShader(gl, gl.FRAGMENT_SHADER, document.getElementById(fragmentShader).text.trim()));
        gl.linkProgram(program);
        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            console.error(gl.getProgramInfoLog(program));
        }
        return program;
    };

    bindVertexBuffer = (gl, relatedVertices) => {
        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(relatedVertices), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        return buffer;
    };

    bindTextureBuffer = (gl, img, aspectRatio) => {
        var texBuffer = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texBuffer);
        if (img.width) {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
        } else {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, aspectRatio.width, aspectRatio.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, img);
        }
        // set filtering options
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return texBuffer;
    };

    prepareFramebuffer = (gl, width, height, type, filter) => {
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        if (type === gl.UNSIGNED_BYTE) {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, type, null);
        } else {
            const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
            console.log(`Can ${status === gl.FRAMEBUFFER_COMPLETE ? "" : "NOT "}render to R32`);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, width, height, 0, gl.RED, type, null);
        }
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        const framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
        return {texture: texture, framebuffer: framebuffer};
    };

    transferDataToGPU = (gl, program, linkedVariable, buffer, dimensions) => {
        var vertices = gl.getAttribLocation(program, linkedVariable);
        gl.enableVertexAttribArray(vertices);
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.vertexAttribPointer(vertices, dimensions, gl.FLOAT, gl.FALSE, 0, 0);
        return vertices;
    };

    transferBufferDataToGPU = (gl, buffer, vertices, dimensions) => {
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.vertexAttribPointer(vertices, dimensions, gl.FLOAT, gl.FALSE, 0, 0);
    };

    transferTextureToGPU = (gl, tex, index) => {
        gl.activeTexture(gl.TEXTURE0 + index);
        gl.bindTexture(gl.TEXTURE_2D, tex);
    };

    activateTextureByIndex = (gl, program, gpuRef, gpuTextureIndex) => {
        gl.useProgram(program);
        gl.uniform1i(gl.getUniformLocation(program, gpuRef), gpuTextureIndex);
    }
};

var gl, processor, pseudoImg, img;
var rectCoords = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0];
var texCoords = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let pixelCount = 0;

document.addEventListener('DOMContentLoaded', () => {
    processor = new WebGLProcessor();
    gl = processor.initializeGLContext(document.getElementById('canvas')); 
    var showProgram = processor.createProgram(gl, 'new-vs', 'show-fs');

    

I am currently developing a project that involves calculating the count of pixels with values less than a specified number. For example:

var totalPixels = imageWidth * imageHeight;
var pixelCount = 0;
var thresholdValue = 229;
for (var i = 0; i < totalPixels; i++) {
    var currentPixelValue = buffer[i];
    if (currentPixelValue < thresholdValue) pixelCount++;
}
console.log('Pixel Count:', pixelCount);

In the above code snippet, I am computing the pixel count for all pixels with values below 229. Although it works fine, it can be time-consuming. Is there a way to perform this computation on the GPU? I understand that using loops in the GPU is not recommended, but I'm curious whether it's feasible. If so, could you provide some pseudocode?

Edit: On reviewing this example, I noticed that we can calculate the count for a smaller image like 256 x 1. However, how can we achieve the same for images with dimensions img.width and img.height?

<script id="max-fs" type="not-js">
precision mediump float;

uniform sampler2D u_texture;

void main() {
  vec4 maxColor = vec4(0);

  // For a 256x1 texture, iterate over every pixel
  for (int i = 0; i < 256; ++i) {
    // Calculate center points of pixels
    vec2 uv = vec2((float(i) + 0.5) / 256.0, 0.5);

    // Get the maximum value of the pixel
    maxColor = max(maxColor, texture2D(u_texture, uv));
  }

  gl_FragColor = maxColor;
}
</script>

Although my shader appears as follows, it does not produce the desired outcome:

<script id="pixelCounter-fs" type="not-js">
precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution;
    vec4 colorInfo = texture2D(u_texture, uv);
    if (colorInfo.r < 0.89453125) {
        discard;
    }
    gl_FragColor = vec4(vec3(colorInfo.r), 1.0);
}
</script>

Answer №1

Is there a more efficient way to calculate pixel count?

  uniform sampler2D data;
  const int width = ${imageData.width};
  const int height = ${imageData.height};
  void main() {
    float count = 0.0;
    vec2 size = vec2(width, height);
    for (int y = 0; y < height; ++y) {
      for (int x = 0; x < width; ++x) {
        float data = texture2D(data, (vec2(x, y) + 0.5) / size).r;
        if (data < 229.0 / 255.0) {
          count += 1.0;
        }
      }
    }
    gl_FragColor = vec4(
        mod(count, 256.0),
        mod(floor(count / 256.0), 256.0),
        mod(floor(count / 256.0 / 256.0), 256.0),
        floor(count / 256.0 / 256.0 / 256.0)) / 255.0;
  }
}

Another approach is to draw just 1 pixel and read it

  const result = new Uint8Array(4);
  gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, result);
  const count = result[0] + 
                result[1] * 256 + 
                result[2] * 256 * 256 +
                result[3] * 256 * 256 * 256;

function main() {
  const gl = document.createElement('canvas').getContext('webgl');
  
  // generate random image
  const imageData = (function(width, height) {
    const size = width * height;
    const data = new Uint8Array(size);
    let count = 0;
    for (let i = 0; i < size; ++i) {
      const v = Math.random() * 256 | 0;
      data[i] = v;
      count += v < 229 ? 1 : 0;
    }
    return { width, height, data, count };
  }(400, 200));

  const dataTexture = gl.createTexture();
  gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
  gl.bindTexture(gl.TEXTURE_2D, dataTexture);
  gl.texImage2D(
      gl.TEXTURE_2D,
      0,  // mip level
      gl.LUMINANCE,  // internal format
      imageData.width,
      imageData.height,
      0,  // border
      gl.LUMINANCE,  // format
      gl.UNSIGNED_BYTE, // type
      imageData.data);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  
  const vs = `

  attribute vec4 position;
  void main() {
    gl_PointSize = 1.0;
    gl_Position = position;
  }
  `;
  
  const fs = `
  precision highp float;
  uniform sampler2D data;
  const int width = ${imageData.width};
  const int height = ${imageData.height};
  void main() {
    float count = 0.0;
    vec2 size = vec2(width, height);
    for (int y = 0; y < height; ++y) {
      for (int x = 0; x < width; ++x) {
        float data = texture2D(data, (vec2(x, y) + 0.5) / size).r;
        if (data < 229.0 / 255.0) {
          count += 1.0;
        }
      }
    }
    gl_FragColor = vec4(
        mod(count, 256.0),
        mod(floor(count / 256.0), 256.0),
        mod(floor(count / 256.0 / 256.0), 256.0),
        floor(count / 256.0 / 256.0 / 256.0)) / 255.0;
  }
  `;
  
  // compile shaders, link program, look up locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  
  gl.useProgram(programInfo.program);
  
  // calls gl.activeTeture, gl.bindTexture, gl.uniformXXX
  twgl.setUniforms(programInfo, {
    data: dataTexture,
  });
  

  gl.viewport(0, 0, 1, 1);
  
  // draw 1 point
  gl.drawArrays(gl.POINTS, 0, 1);
  
  const result = new Uint8Array(4);
  gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, result);
  const count = result[0] + 
                result[1] * 256 + 
                result[2] * 256 * 256 +
                result[3] * 256 * 256 * 256;
  const size = imageData.width * imageData.height;
  console.log('CPU count:', imageData.count, 'of', size);
  console.log('GPU count:', count, 'of', size);
}

main();
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

It would be beneficial to test whether this method is faster than JavaScript's iteration over all pixels. Remember that sending the image to the GPU incurs overhead.

If the image is large, considering dividing it into sections could provide better performance. For example, counting every 16x16 area in a separate texture and summing the results later using multiple draw calls on the GPU for parallel processing.

A broader coverage of channels R,G,B,A simultaneously may increase efficiency compared to single-channel counting.

PS: To make the code compatible with Safari, address Safari's bugs, provide data buffer, and set up the attribute for drawing 1 point in WebGL implementation.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Avoid page refreshing when retrieving data using PHP and AJAX

In my current program, I am able to add data and insert it into the database. However, I am looking for a way to automatically send this data to another page or browser without refreshing. I have two browsers open, so how can I submit the data to the other ...

Angularjs changes the "&" character to "&"

Here is the AngularJS code I am using: $scope.getEventSeconds = function(){ $http.get('myfile.php', { params: { 'action': 'get_datas' } }).success(function(data){ $scope.list = data; $scop ...

Get canvas picture through JS Jquery

I've been attempting to save a canvas image to my desktop using this code: <script type="text/javascript"> var canvas; $(document).ready(function() { if ($('#designs-wrapper').length) { $('.design').eac ...

Imitation of three-dimensional camera rotation

I've been exploring the realm of creating 3D games using JavaScript and HTML's 2D canvas. I recently came across a helpful tutorial that guided me in setting up a basic interactive scene. Now, I'm facing a challenge in implementing the func ...

Encountered an error while attempting to execute the command npx create react app: spawn UNKNOWN error occurred

After attempting to execute a basic npx create-react-app, an unexpected error occurred: D:\>npx create-react-app abc npx: installed 67 in 4.255s Creating a new React app in D:\abc. Installing packages. This might take a couple of minutes. In ...

Twice the calls are being made by jQuery Ajax

I am using an <input> tag with the following attributes: <input type="button" id="btnSave2" value="Save" onclick="Event(1)" class="btn btn-primary col-lg-12" /> In addition, I have a script as ...

The Angular Animation constantly resets with each new action taken

In my Angular project, I am working on a scaling animation for a list. I want the animation to only trigger when specific buttons (red and green) are pressed. Currently, the animation restarts regardless of what I click on. Can anyone help me troubleshoot ...

Enhancing Google Custom Search Engine with Bootstrap3 and CSS3 styling

I'm looking for guidance on how to customize the appearance of a Google Custom Searchbar on my website. Is it feasible to style it using CSS3 and Bootstrap 3 like the example in the image below? Any assistance is greatly appreciated. ...

I am facing an issue with the Angular signup page that is using ui-router, as it is unable

I have been working on an Angular sign-up page and here is the project directory structure: signUpPage-Angular bin bower_components model mongodbApp.js node_modules **partials fail.html main.html succe ...

Attempting to incorporate an audio file into a Discord.js module

My bot can join the voice channel successfully, but when I attempt to play audio, I encounter multiple errors indicating that I am missing certain modules [@discordjs/opus, node-opus, opusscript]. Although I have installed these modules, I am unsure of w ...

trouble with synchronizing multiple requests in node express mysql

An expressJS endpoint is connected to a mysql DB, where a query retrieves a set of data and assigns the first returned result to a user by updating a field with the user id. The rule is that a single row can only be allocated to one user until the user pr ...

Sending information from a rails controller to a react component

Wondering how to pass the example @post = Post.all from the controller to React component props while integrating Rails with React via Webpacker. Is it necessary to do this through an API or is there another way? ...

Unable to attach an event listener to an element fetched from an API

I'm currently in the process of developing a trivia web application using an API, and my goal is to incorporate an event listener onto the button which corresponds to the correct answer. This way, when users click on it, a message will appear confirmi ...

Tips and tricks for effectively utilizing Material UI's sx props in conjunction with a styles object

In my current project, I am working with a styles object that is structured as follows: styles = { color: "#333", fontSize: "16px", }; When applying these styles to a Material UI component like the example below, everything works a ...

What is the process for updating the package-lock.json file in Laravel?

GateLab's security feature has identified some known vulnerabilities in the package-lock.json file that need to be updated. The message states: Known security vulnerabilities detected Dependency object-path Version < 0.11.5 Upgrade to ~> 0.11. ...

Customizing the color of cells in an HTML table within a JSON based on their status

Would you like to learn how to customize the color of cells in an HTML table based on the status of a college semester's course map? The table represents all the necessary courses for your major, with green indicating completed courses, yellow for cou ...

The Tailwind style did not completely translate when applied to the content of dangerouslySetInnerHtml

I have an HTML file containing Tailwind styles stored in my database, which needs to be fetched first. This will result in an HTML string that will be inserted into dangerouslySetInnerHtml. Here is the code snippet (utilizing Qwik): export const useHTML = ...

Tips on dividing a div into two separate pages when converting to PDF

I am working on an asp.net MVC project and I need to convert a HTML div into a PDF with two separate pages. Here is an example of my code: HTML Code <div class="row" id="mycanvas"> <p>This is the first part of my content.</p> <p ...

Issue with Bootstrap v3.3.6 Dropdown Functionality

previewCan someone help me figure out why my Bootstrap dropdown menu is not working correctly? I recently downloaded Bootstrap to create a custom design, and while the carousel is functioning properly, when I click on the dropdown button, the dropdown-menu ...

Troubleshooting issues with Firebase integration in a Node.js environment

I am currently encountering difficulties implementing Firebase in my Node.js project. Below is the code snippet that I am attempting to execute on Node. var firebase = require("firebase"); var admin = require("firebase-admin"); var serviceAccount = requi ...