ছবি এবং ভিডিওর জন্য রিয়েল-টাইম প্রভাব

Mat Scales

আজকের অনেক জনপ্রিয় অ্যাপ আপনাকে ছবি বা ভিডিওতে ফিল্টার এবং প্রভাব প্রয়োগ করতে দেয়। এই নিবন্ধটি ওপেন ওয়েবে এই বৈশিষ্ট্যগুলি কীভাবে প্রয়োগ করতে হয় তা দেখায়৷

প্রক্রিয়াটি মূলত ভিডিও এবং চিত্রগুলির জন্য একই, তবে আমি শেষে কিছু গুরুত্বপূর্ণ ভিডিও বিবেচনা কভার করব। পুরো নিবন্ধ জুড়ে আপনি ধরে নিতে পারেন যে 'ইমেজ' মানে 'ছবি বা একটি ভিডিওর একক ফ্রেম'

কিভাবে একটি ছবির জন্য পিক্সেল ডেটা পেতে হয়

ইমেজ ম্যানিপুলেশনের 3টি মৌলিক বিভাগ রয়েছে যা সাধারণ:

  • কনট্রাস্ট, উজ্জ্বলতা, উষ্ণতা, সেপিয়া টোন, স্যাচুরেশনের মতো পিক্সেল প্রভাব।
  • মাল্টি-পিক্সেল প্রভাব, যাকে কনভোলিউশন ফিল্টার বলা হয়, যেমন শার্পনিং, এজ ডিটেকশন, ব্লার।
  • পুরো ইমেজ বিকৃতি, যেমন ক্রপিং, স্কুইং, স্ট্রেচিং, লেন্স ইফেক্ট, রিপলস।

এই সমস্তগুলির মধ্যে উৎস ইমেজের প্রকৃত পিক্সেল ডেটা পাওয়া এবং তারপর এটি থেকে একটি নতুন চিত্র তৈরি করা জড়িত এবং এটি করার একমাত্র ইন্টারফেস হল একটি ক্যানভাস।

তাহলে, সত্যিই গুরুত্বপূর্ণ পছন্দ হল CPU-তে, 2D ক্যানভাস দিয়ে, নাকি GPU-তে WebGL-এর মাধ্যমে প্রসেসিং করা উচিত।

আসুন দুটি পদ্ধতির মধ্যে পার্থক্যগুলি দ্রুত দেখে নেওয়া যাক।

2D ক্যানভাস

এটি অবশ্যই, একটি দীর্ঘ পথ দ্বারা, দুটি বিকল্পের মধ্যে সবচেয়ে সহজ। প্রথমে আপনি ক্যানভাসে ছবিটি আঁকুন।

const source = document.getElementById('source-image');

// Create the canvas and get a context
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// Set the canvas to be the same size as the original image
canvas.width = source.naturalWidth;
canvas.height = source.naturalHeight;

// Draw the image onto the top-left corner of the canvas
context.drawImage(theOriginalImage, 0, 0);

তারপর আপনি পুরো ক্যানভাসের জন্য পিক্সেল মানগুলির একটি অ্যারে পাবেন।

const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;

এই মুহুর্তে pixels ভেরিয়েবল হল একটি Uint8ClampedArray যার দৈর্ঘ্য width * height * 4 । প্রতিটি অ্যারের উপাদান একটি বাইট এবং অ্যারের প্রতিটি চারটি উপাদান একটি পিক্সেলের রঙকে উপস্থাপন করে। চারটি উপাদানের প্রতিটি সেই ক্রমে লাল, সবুজ, নীল এবং আলফা (স্বচ্ছতা) এর পরিমাণকে প্রতিনিধিত্ব করে। পিক্সেলগুলি উপরের-বাম কোণ থেকে শুরু করে এবং বাম থেকে ডানে এবং উপরে থেকে নীচে কাজ করে।

pixels[0] = red value for pixel 0
pixels[1] = green value for pixel 0
pixels[2] = blue value for pixel 0
pixels[3] = alpha value for pixel 0
pixels[4] = red value for pixel 1
pixels[5] = green value for pixel 1
pixels[6] = blue value for pixel 1
pixels[7] = alpha value for pixel 1
pixels[8] = red value for pixel 2
pixels[9] = green value for pixel 2
pixels[10] = blue value for pixel 2
pixels[11] = alpha value for pixel 2
pixels[12] = red value for pixel 3
...

কোন প্রদত্ত পিক্সেল এর স্থানাঙ্ক থেকে সূচক খুঁজে পেতে, একটি সহজ সূত্র আছে।

const index = (x + y * imageWidth) * 4;
const red = pixels[index];
const green = pixels[index + 1];
const blue = pixels[index + 2];
const alpha = pixels[index + 3];

আপনি এখন এই ডেটা পড়তে এবং লিখতে পারেন যা আপনি চান, আপনাকে যে কোনো প্রভাব প্রয়োগ করার অনুমতি দেয় যা আপনি ভাবতে পারেন। যাইহোক, এই অ্যারেটি ক্যানভাসের জন্য প্রকৃত পিক্সেল ডেটার একটি অনুলিপি । সম্পাদিত সংস্করণটি আবার লিখতে আপনাকে ক্যানভাসের উপরের-বাম কোণে এটিকে আবার লিখতে putImageData পদ্ধতি ব্যবহার করতে হবে

context.putImageData(imageData, 0, 0);

ওয়েবজিএল

WebGL একটি বড় বিষয়, একটি নিবন্ধে এটি ন্যায়বিচার করতে অবশ্যই খুব বড়। আপনি যদি WebGL সম্পর্কে আরও জানতে চান, তাহলে এই নিবন্ধের শেষে প্রস্তাবিত পড়া দেখুন।

যাইহোক, এখানে একটি একক চিত্র হেরফের করার ক্ষেত্রে কী করা দরকার তার একটি খুব সংক্ষিপ্ত ভূমিকা রয়েছে।

WebGL সম্পর্কে মনে রাখার সবচেয়ে গুরুত্বপূর্ণ বিষয়গুলির মধ্যে একটি হল এটি একটি 3D গ্রাফিক্স API নয় । আসলে, ওয়েবজিএল (এবং ওপেনজিএল) ঠিক একটি বিষয়ে ভাল - ত্রিভুজ আঁকা। আপনার অ্যাপ্লিকেশনে আপনাকে অবশ্যই ত্রিভুজের পরিপ্রেক্ষিতে আপনি আসলে কী আঁকতে চান তা বর্ণনা করতে হবে। একটি 2D চিত্রের ক্ষেত্রে, এটি খুবই সহজ, কারণ একটি আয়তক্ষেত্র হল দুটি অনুরূপ সমকোণী ত্রিভুজ, যাতে তাদের কর্ণগুলি একই জায়গায় থাকে।

মৌলিক প্রক্রিয়া হল:

  • GPU-তে ডেটা পাঠান যা ত্রিভুজের শীর্ষবিন্দু (বিন্দু) বর্ণনা করে।
  • টেক্সচার (ছবি) হিসেবে আপনার উৎসের ছবি GPU-তে পাঠান।
  • একটি 'ভার্টেক্স শেডার' তৈরি করুন।
  • একটি 'ফ্র্যাগমেন্ট শেডার' তৈরি করুন।
  • 'ইউনিফর্ম' নামে কিছু শেডার ভেরিয়েবল সেট করুন।
  • শেডার্স চালান।

চলুন বিস্তারিত যান. গ্রাফিক্স কার্ডে কিছু মেমরি বরাদ্দ করে শুরু করুন যাকে ভার্টেক্স বাফার বলা হয়। আপনি এতে ডেটা সঞ্চয় করেন যা প্রতিটি ত্রিভুজের প্রতিটি বিন্দুকে বর্ণনা করে। আপনি কিছু ভেরিয়েবলও সেট করতে পারেন, যাকে ইউনিফর্ম বলা হয়, যেগুলি উভয় শেডারের মাধ্যমে বিশ্বব্যাপী মান।

একটি ভার্টেক্স শেডার প্রতিটি ত্রিভুজের তিনটি বিন্দু আঁকতে স্ক্রিনে কোথায় আছে তা গণনা করতে ভার্টেক্স বাফার থেকে ডেটা ব্যবহার করে।

এখন GPU জানে ক্যানভাসের মধ্যে কোন পিক্সেলগুলি আঁকতে হবে। ফ্র্যাগমেন্ট শেডারকে পিক্সেল প্রতি একবার বলা হয় এবং স্ক্রিনে যে রঙটি আঁকা উচিত সেটি ফেরত দিতে হবে। ফ্র্যাগমেন্ট শেডার রঙ নির্ধারণ করতে এক বা একাধিক টেক্সচার থেকে তথ্য পড়তে পারে।

একটি ফ্র্যাগমেন্ট শেডারে টেক্সচার পড়ার সময় আপনি 0 (বাম বা নীচে) এবং 1 (ডান বা উপরে) এর মধ্যে দুটি ফ্লোটিং-পয়েন্ট স্থানাঙ্ক ব্যবহার করে চিত্রের কোন অংশটি পড়তে চান তা নির্দিষ্ট করুন।

আপনি যদি পিক্সেল স্থানাঙ্কের উপর ভিত্তি করে টেক্সচারটি পড়তে চান তবে আপনাকে একটি অভিন্ন ভেক্টর হিসাবে পিক্সেলগুলিতে টেক্সচারের আকারটি পাস করতে হবে যাতে আপনি প্রতিটি পিক্সেলের জন্য রূপান্তর করতে পারেন।

varying vec2 pixelCoords;

uniform vec2 textureSize;
uniform sampler2D textureSampler;

main() {
  vec2 textureCoords = pixelCoords / textureSize;
  vec4 textureColor = texture2D(textureSampler, textureCoords);
  gl_FragColor = textureColor;
 }
Pretty much every kind of 2D image manipulation that you might want to do can be done in the
fragment shader, and all of the other WebGL parts can be abstracted away. You can see [the
abstraction layer](https://github.com/GoogleChromeLabs/snapshot/blob/master/src/filters/image-shader.ts) (in
TypeScript) that is being in used in one of our sample applications if you'd like to see an example.

### Which should I use?

For pretty much any professional quality image manipulation, you should use WebGL. There is no
getting away from the fact that this kind of work is the whole reason GPUs were invented. You can
process images an order of magnitude faster on the GPU, which is essential for any real-time
effects.

The way that graphics cards work means that every pixel can be calculated in it's own thread. Even
if you parallelize your code CPU-based code with `Worker`s, your GPU may have 100s of times as many
specialized cores as your CPU has general cores.

2D canvas is much simpler, so is great for prototyping and may be fine for one-off transformations.
However, there are plenty of abstractions around for WebGL that mean you can get the performance
boost without needing to learn the details.

Examples in this article are mostly for 2D canvas to make explanations easier, but the principles
should translate pretty easily to fragment shader code.

## Effect types

### Pixel effects

This is the simplest category to both understand and implement. All of these transformations take
the color value of a single pixel and pass it into a function that returns another color value.

There are many variations on these operations that are more or less complicated. Some will take into
account how the human brain processes visual information based on decades of research, and some will
be dead simple ideas that give an effect that's mostly reasonable.

For example, a brightness control can be implemented by simply taking the red, green and blue values
of the pixel and multiplying them by a brightness value. A brightness of 0 will make the image
entirely black. A value of 1 will leave the image unchanged. A value greater than 1 will make it
brighter.

For 2D canvas:

```js
const brightness = 1.1; // Make the image 10% brighter
for (let i = 0; i < imageData.data.length; i += 4) {
  imageData.data[i] = imageData.data[i] * brightness;
  imageData.data[i + 1] = imageData.data[i + 1] * brightness;
  imageData.data[i + 2] = imageData.data[i + 2] * brightness;
}

নোট করুন যে লুপটি একবারে 4 বাইট নিয়ে যায়, কিন্তু শুধুমাত্র তিনটি মান পরিবর্তন করে - কারণ এই বিশেষ রূপান্তরটি আলফা মান পরিবর্তন করে না। এছাড়াও মনে রাখবেন যে একটি Uint8ClampedArray সমস্ত মানকে পূর্ণসংখ্যাতে পরিণত করবে এবং ক্ল্যাম্পের মান 0 থেকে 255 এর মধ্যে হবে।

WebGL ফ্র্যাগমেন্ট শেডার:

    float brightness = 1.1;
    gl_FragColor = textureColor;
    gl_FragColor.rgb *= brightness;

একইভাবে, এই নির্দিষ্ট রূপান্তরের জন্য আউটপুট রঙের শুধুমাত্র RGB অংশকে গুণ করা হয়।

এই ফিল্টারগুলির মধ্যে কিছু অতিরিক্ত তথ্য নেয়, যেমন পুরো চিত্রের গড় আলোকসজ্জা, তবে এইগুলি এমন জিনিস যা পুরো চিত্রের জন্য একবার গণনা করা যেতে পারে।

বৈসাদৃশ্য পরিবর্তনের একটি উপায়, উদাহরণস্বরূপ, প্রতিটি পিক্সেলকে কিছু 'ধূসর' মানের দিকে বা দূরে সরানো হতে পারে, যথাক্রমে নিম্ন বা উচ্চতর বৈসাদৃশ্যের জন্য। ধূসর মানটিকে সাধারণত একটি ধূসর রঙ হিসাবে বেছে নেওয়া হয় যার উজ্জ্বলতা চিত্রের সমস্ত পিক্সেলের মধ্যম আলো।

আপনি একবার এই মানটি গণনা করতে পারেন যখন ইমেজ লোড হয় এবং তারপর প্রতিবার ইমেজ প্রভাব সামঞ্জস্য করার জন্য এটি ব্যবহার করুন।

মাল্টি-পিক্সেল

বর্তমান পিক্সেলের রঙ নির্ধারণ করার সময় কিছু প্রভাব প্রতিবেশী পিক্সেলের রঙ ব্যবহার করে।

এটি 2D ক্যানভাস ক্ষেত্রে আপনি কীভাবে জিনিসগুলি করেন তা কিছুটা পরিবর্তন করে কারণ আপনি চিত্রের আসল রঙগুলি পড়তে সক্ষম হতে চান এবং আগের উদাহরণটি ছিল জায়গায় পিক্সেল আপডেট করা।

এই যথেষ্ট সহজ, যদিও. আপনি যখন প্রাথমিকভাবে আপনার ইমেজ ডেটা অবজেক্ট তৈরি করেন তখন আপনি ডেটার একটি কপি তৈরি করতে পারেন।

const originalPixels = new Uint8Array(imageData.data);

WebGL ক্ষেত্রে আপনাকে কোনো পরিবর্তন করতে হবে না, যেহেতু শেডার ইনপুট টেক্সচারে লেখে না।

মাল্টি-পিক্সেল প্রভাবের সবচেয়ে সাধারণ বিভাগকে কনভোলিউশন ফিল্টার বলা হয়। একটি কনভোলিউশন ফিল্টার ইনপুট ইমেজের প্রতিটি পিক্সেলের রঙ গণনা করতে ইনপুট ইমেজ থেকে বেশ কয়েকটি পিক্সেল ব্যবহার করে। আউটপুটে প্রতিটি ইনপুট পিক্সেলের প্রভাবের মাত্রাকে ওজন বলা হয়।

ওজনগুলিকে একটি ম্যাট্রিক্স দ্বারা উপস্থাপন করা যেতে পারে, যাকে একটি কার্নেল বলা হয়, যার কেন্দ্রীয় মান বর্তমান পিক্সেলের সাথে সম্পর্কিত। উদাহরণস্বরূপ, এটি একটি 3x3 গাউসিয়ান ব্লারের জন্য কার্নেল।

    | 0  1  0 |
    | 1  4  1 |
    | 0  1  0 |

তাহলে ধরা যাক যে আপনি (23, 19) এ পিক্সেলের আউটপুট রঙ গণনা করতে চান। আশেপাশের 8 পিক্সেল (23, 19) সেইসাথে পিক্সেল নিজেই নিন, এবং সংশ্লিষ্ট ওজন দ্বারা তাদের প্রতিটির জন্য রঙের মান গুণ করুন।

    (22, 18) x 0    (23, 18) x 1    (24, 18) x 0
    (22, 19) x 1    (23, 19) x 4    (24, 19) x 1
    (22, 20) x 0    (23, 20) x 1    (24, 20) x 0

তাদের সকলকে একসাথে যোগ করুন তারপর ফলাফলটিকে 8 দ্বারা ভাগ করুন, যা ওজনের যোগফল। আপনি দেখতে পাচ্ছেন কীভাবে ফলাফলটি এমন একটি পিক্সেল হবে যা বেশিরভাগই আসল, কিন্তু কাছাকাছি পিক্সেলগুলি রক্তপাতের সাথে।

const kernel = [
  [0, 1, 0],
  [1, 4, 1],
  [0, 1, 0],
];

for (let y = 0; y < imageHeight; y++) {
  for (let x = 0; x < imageWidth; x++) {
    let redTotal = 0;
    let greenTotal = 0;
    let blueTotal = 0;
    let weightTotal = 0;
    for (let i = -1; i <= 1; i++) {
      for (let j = -1; j <= 1; j++) {
        // Filter out pixels that are off the edge of the image
        if (
          x + i > 0 &&
          x + i < imageWidth &&
          y + j > 0 &&
          y + j < imageHeight
        ) {
          const index = (x + i + (y + j) * imageWidth) * 4;
          const weight = kernel[i + 1][j + 1];
          redTotal += weight * originalPixels[index];
          greenTotal += weight * originalPixels[index + 1];
          blueTotal += weight * originalPixels[index + 2];
          weightTotal += weight;
        }
      }
    }

    const outputIndex = (x + y * imageWidth) * 4;
    imageData.data[outputIndex] = redTotal / weightTotal;
    imageData.data[outputIndex + 1] = greenTotal / weightTotal;
    imageData.data[outputIndex + 2] = blueTotal / weightTotal;
  }
}

এটি মৌলিক ধারণা দেয়, তবে সেখানে গাইড রয়েছে যা আরও বিশদে যাবে এবং অন্যান্য অনেক দরকারী কার্নেলের তালিকা করবে।

পুরো ইমেজ

কিছু সম্পূর্ণ ইমেজ রূপান্তর সহজ. একটি 2D ক্যানভাসে, ক্রপিং এবং স্কেলিং হল ক্যানভাসে শুধুমাত্র উৎস ইমেজের কিছু অংশ আঁকার একটি সাধারণ ঘটনা।

// Set the canvas to be a little smaller than the original image
canvas.width = source.naturalWidth - 100;
canvas.height = source.naturalHeight - 100;

// Draw only part of the image onto the canvas
const sx = 50; // The left-most part of the source image to copy
const sy = 50; // The top-most part of the source image to copy
const sw = source.naturalWidth - 100; // How wide the rectangle to copy is
const sh = source.naturalHeight - 100; // How tall the rectangle to copy is

const dx = 0; // The left-most part of the canvas to draw over
const dy = 0; // The top-most part of the canvas to draw over
const dw = canvas.width; // How wide the rectangle to draw over is
const dh = canvas.height; // How tall the rectangle to draw over is

context.drawImage(theOriginalImage, sx, sy, sw, sh, dx, dy, dw, dh);

ঘূর্ণন এবং প্রতিফলন সরাসরি 2D প্রসঙ্গে উপলব্ধ। আপনি ক্যানভাসে ছবিটি আঁকার আগে, বিভিন্ন রূপান্তর পরিবর্তন করুন।

// Move the canvas so that the center of the canvas is on the Y-axis
context.translate(-canvas.width / 2, 0);

// An X scale of -1 reflects the canvas in the Y-axis
context.scale(-1, 1);

// Rotate the canvas by 90°
context.rotate(Math.PI / 2);

কিন্তু আরও শক্তিশালীভাবে, অনেক 2D রূপান্তর 2x3 ম্যাট্রিক্স হিসাবে লেখা যেতে পারে এবং setTransform() দিয়ে ক্যানভাসে প্রয়োগ করা যেতে পারে। এই উদাহরণটি একটি ম্যাট্রিক্স ব্যবহার করে যা একটি ঘূর্ণন এবং একটি অনুবাদকে একত্রিত করে।

const matrix = [
  Math.cos(rot) * x1,
  -Math.sin(rot) * x2,
  tx,
  Math.sin(rot) * y1,
  Math.cos(rot) * y2,
  ty,
];

context.setTransform(
  matrix[0],
  matrix[1],
  matrix[2],
  matrix[3],
  matrix[4],
  matrix[5],
);

লেন্সের বিকৃতি বা লহরের মতো আরও জটিল প্রভাবগুলির মধ্যে উৎস পিক্সেল স্থানাঙ্ক গণনা করার জন্য প্রতিটি গন্তব্য স্থানাঙ্কে কিছু অফসেট প্রয়োগ করা জড়িত। উদাহরণস্বরূপ, একটি অনুভূমিক তরঙ্গ প্রভাবের জন্য আপনি y স্থানাঙ্কের উপর ভিত্তি করে কিছু মান দ্বারা উত্স পিক্সেল x স্থানাঙ্ক অফসেট করতে পারেন।

for (let y = 0; y < canvas.height; y++) {
  const xOffset = 20 * Math.sin((y * Math.PI) / 20);
  for (let x = 0; x < canvas.width; x++) {
    // Clamp the source x between 0 and width
    const sx = Math.min(Math.max(0, x + xOffset), canvas.width);

    const destIndex = (y * canvas.width + x) * 4;
    const sourceIndex = (y * canvas.width + sx) * 4;

    imageData.data[destIndex] = originalPixels.data[sourceIndex];
    imageData.data[destIndex + 1] = originalPixels.data[sourceIndex + 1];
    imageData.data[destIndex + 2] = originalPixels.data[sourceIndex + 2];
  }
}

ভিডিও

আপনি যদি উৎস ইমেজ হিসাবে একটি video উপাদান ব্যবহার করেন তবে নিবন্ধের বাকি সবকিছু ইতিমধ্যেই ভিডিওর জন্য কাজ করে।

ক্যানভাস 2D:

context.drawImage(<strong>video</strong>, 0, 0);

ওয়েবজিএল:

gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  <strong>video</strong>,
);

যাইহোক, এটি শুধুমাত্র বর্তমান ভিডিও ফ্রেম ব্যবহার করবে। সুতরাং আপনি যদি একটি প্লে ভিডিওতে একটি প্রভাব প্রয়োগ করতে চান তবে আপনাকে একটি নতুন ভিডিও ফ্রেম দখল করতে এবং প্রতিটি ব্রাউজার অ্যানিমেশন ফ্রেমে এটি প্রক্রিয়া করতে প্রতিটি ফ্রেমে drawImage / texImage2D ব্যবহার করতে হবে৷

const draw = () => {
  requestAnimationFrame(draw);

  context.drawImage(video, 0, 0);

  // ...image processing goes here
};

ভিডিওর সাথে কাজ করার সময় এটি বিশেষভাবে গুরুত্বপূর্ণ যে আপনার প্রক্রিয়াকরণ দ্রুত হয়। একটি স্থির চিত্রের সাথে একজন ব্যবহারকারী একটি বোতামে ক্লিক করার এবং একটি প্রভাব প্রয়োগ করার মধ্যে 100ms বিলম্ব লক্ষ্য করতে পারে না। যখন অ্যানিমেটেড করা হয়, তবে মাত্র 16ms বিলম্ব দৃশ্যমান ঝাঁকুনির কারণ হতে পারে।

প্রতিক্রিয়া