Nous avons depuis longtemps de nombreux outils de traitement d'images numériques: Photoshop, Lightroom, GIMP, PhotoScape et bien d'autres. Cependant, au cours des dernières années, l'un est devenu populaire parmi les utilisateurs non experts en raison de sa facilité d'utilisation et de ses fonctionnalités sociales: Instagram. Vous êtes-vous déjà demandé comment fonctionnent les filtres Instagram? Il s'agit en fait d'opérations matricielles assez simples! Si simple que nous pouvons construire notre propre bibliothèque sans aucune bibliothèque externe, juste HTML + JS pur et simple. Construisons-en un maintenant.
La première étape consiste à créer notre fichier HTML, il ne contient que 35 lignes. Appelons-le "OpenInsta", vous pouvez vérifier le code source final sur mon GitHub.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OpenInsta</title>
</head>
<body>
<h1>OpenInsta</h1>
<input type="file" accept="image/*" id="fileinput" />
<div>
<label for="red">Red</label>
<input type="range" min="-255" max="255" value="0" id="red">
<label for="green">Green</label>
<input type="range" min="-255" max="255" value="0" id="green">
<label for="blue">Blue</label>
<input type="range" min="-255" max="255" value="0" id="blue">
<label for="brightness">Brightness</label>
<input type="range" min="-255" max="255" value="0" id="brightness">
<label for="contrast">Constrast</label>
<input type="range" min="-255" max="255" value="0" id="contrast">
<label for="grayscale">Grayscale</label>
<input type="checkbox" id="grayscale">
<br/>
<canvas id="canvas" width="0" height="0"></canvas>
</div>
<script src="main.js"></script>
</body>
</html>
Nous avons une entrée de fichier, un contrôleur de plage pour chaque canal de couleur (rouge, bleu et vert) plus des contrôleurs de plage de luminosité et de contraste, et enfin une case à cocher en niveaux de gris. Nous avons également une toile pour dessiner notre image pendant que nous la traitons. En bas, nous incluons un
main.js
Fichier JavaScript qui contiendra notre moteur de traitement d'image que nous allons construire maintenant.
Si vous ouvrez le fichier HTML sur le navigateur, voici à quoi il devrait ressembler
Commençons à écrire notre
main.js
avec le chargeur de fichiers. Il connectera notre entrée de fichier avec notre toile, convertissant l'image dans n'importe quel format (JPEG, PNG, BMP, etc.) en un tableau unidimensionnel plat.const fileinput = document.getElementById('fileinput')
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const srcImage = new Image
let imgData = null
let originalPixels = null
fileinput.onchange = function (e) {
if (e.target.files && e.target.files.item(0)) {
srcImage.src = URL.createObjectURL(e.target.files[0])
}
}
srcImage.onload = function () {
canvas.width = srcImage.width
canvas.height = srcImage.height
ctx.drawImage(srcImage, 0, 0, srcImage.width, srcImage.height)
imgData = ctx.getImageData(0, 0, srcImage.width, srcImage.height)
originalPixels = imgData.data.slice()
}
Nous attribuons deux événements: lorsque notre entrée change (c'est-à-dire lorsque l'utilisateur sélectionne une image), nous attachons le fichier comme source d'un nœud d' image JavaScript . Ce nœud commencera automatiquement à charger l'image et, une fois terminé, nous dessinerons son contenu sur notre toile pendant que nous extrairons le tableau de pixels sur une variable distincte que nous utiliserons pour le traitement.
Le nœud Image que nous créons est essentiellement la représentation JS d'un HTML
<img>
étiquette.
le
imgData
variable que nous avons extraite en utilisant
getImageData()
est une représentation objet de l'image, ayant seulement trois attributs: largeur, hauteur et données, qui est le tableau de pixels unidimensionnels. Nous stockons le tableau de pixels sur une variable distincte pour l'utiliser comme base pour nos modifications d'image.
Supposons que notre image soit 2x2, le tableau serait quelque chose comme
[128, 255, 0, 255, 186, 182, 200, 255, 186, 255, 255, 255, 127, 60, 20, 128]
, où les huit premières valeurs représentent les 2 pixels de la première ligne et les huit valeurs suivantes représentent les 2 pixels de la deuxième ligne, chaque pixel ayant quatre valeurs variant de 0 à 255 représentant respectivement les canaux rouge, vert, bleu et alpha. Nous pouvons donc créer une méthode pour obtenir l'indice d'un pixel donné comme suit:function getIndex(x, y) {
return (x + y * srcImage.width) * 4
}
Nous pouvons maintenant commencer à construire nos filtres! Tout d'abord, définissons une méthode pour effectuer les transformations et affectons-la à chaque entrée de plage / vérification dans notre page HTML.
const red = document.getElementById('red')
const green = document.getElementById('green')
const blue = document.getElementById('blue')
const brightness = document.getElementById('brightness')
const grayscale = document.getElementById('grayscale')
const contrast = document.getElementById('contrast')
function runPipeline() {
// Get each input value
for (let i = 0; i < srcImage.height; i++) {
for (let j = 0; j < srcImage.width; j++) {
// Apply grayscale to pixel (j, i) if checked
// Apply brightness to pixel (j, i) according to selected value
// Apply contrast to pixel (j, i) according to selected value
// Add red to pixel (j, i) according to selected value
// Add green to pixel (j, i) according to selected value
// Add blue to pixel (j, i) according to selected value
}
}
// Draw updated image
}
red.onchange = runPipeline
green.onchange = runPipeline
blue.onchange = runPipeline
brightness.onchange = runPipeline
grayscale.onchange = runPipeline
contrast.onchange = runPipeline
Maintenant, chaque fois que le rouge, le vert, le bleu, la luminosité, les niveaux de gris et le contraste changent, notre pipeline fonctionnera en appliquant nos filtres et en affichant le résultat.
Rouge, vert et bleu
Nous commençons par les filtres les plus simples. De -255 à 255, l'utilisateur sélectionne une valeur à ajouter sur un canal donné à chaque pixel. Ainsi, par exemple, si le rouge du pixel est actuellement de 128 et que l'utilisateur sélectionne 50 sur l'entrée, nous aurons un pixel avec du rouge 178. Mais que se passe-t-il si le pixel est actuellement de 230? Nous ne pouvons pas avoir un rouge de valeur 280, nous devons donc le fixer pour le maintenir entre les limites.
function clamp(value) {
return Math.max(0, Math.min(Math.floor(value), 255))
}
Nous pouvons définir mathématiquement la fonction rouge, verte et bleue comme f (x) = x + ɑ , où x est la valeur actuelle des pixels sur ce canal et alpha est la valeur sélectionnée par l'utilisateur.
Voici comment notre fonction pour ajouter une valeur bleue à un pixel est définie. Les fonctions pour ajouter du rouge et du vert sont laissées comme exercice, mais sont à peu près les mêmes, n'ayant à changer que le décalage qu'il utilise.
const R_OFFSET = 0
const G_OFFSET = 1
const B_OFFSET = 2
function addBlue(x, y, value) {
const index = getIndex(x, y) + B_OFFSET
const currentValue = currentPixels[index]
currentPixels[index] = clamp(currentValue + value)
}
Une image avec un bleu amélioré ressemble à ceci:
Luminosité
Le concept de luminosité fait référence à la proximité du blanc avec nos pixels. Étant donné que le pixel blanc pur est représenté par R = 255, G = 255 et B = 255, nous pouvons facilement vérifier que les valeurs de canal les plus élevées conduisent à des pixels plus lumineux. Comme nous ne voulons pas améliorer spécifiquement un canal de couleur, nous devons tous les changer, en augmentant ou en diminuant la moyenne de chaque pixel.
function addBrightness(x, y, value) {
addRed(x, y, value)
addGreen(x, y, value)
addBlue(x, y, value)
}
Une image avec une luminosité réduite ressemble à ceci:
Niveaux de gris
Notre dernier filtre n'est pas effrayant comme le précédent. Un pixel peut être rendu en niveaux de gris, ou monochrome, en le convertissant simplement de trois canaux en un seul. Qu'est-ce qui vous vient à l'esprit? Signifier! Il vous suffit de prendre la moyenne des canaux R, G et B et de l'appliquer aux canaux R, G et B.
function setGrayscale(x, y) {
const redIndex = getIndex(x, y) + R_OFFSET
const greenIndex = getIndex(x, y) + G_OFFSET
const blueIndex = getIndex(x, y) + B_OFFSET
const redValue = currentPixels[redIndex]
const greenValue = currentPixels[greenIndex]
const blueValue = currentPixels[blueIndex]
const mean = (redValue + greenValue + blueValue) / 3
currentPixels[redIndex] = clamp(mean)
currentPixels[greenIndex] = clamp(mean)
currentPixels[blueIndex] = clamp(mean)
}
Une image colorée convertie en niveaux de gris ressemble à:
Tout rassembler
Maintenant que nous avons suffisamment de filtres, mettons à jour notre fonction "pipeline" pour les exécuter efficacement. L'ordre ici est important, car le résultat d'un filtre est utilisé pour le suivant. Tout d'abord, nous appliquons l'échelle de gris si elle est cochée, puis la luminosité et le contraste et enfin R, G et B (uniquement si ce n'est pas en échelle de gris).
function runPipeline() {
currentPixels = originalPixels.slice()
const grayscaleFilter = grayscale.checked
const brightnessFilter = Number(brightness.value)
const contrastFilter = Number(contrast.value)
const redFilter = Number(red.value)
const greenFilter = Number(green.value)
const blueFilter = Number(blue.value)
for (let i = 0; i < srcImage.height; i++) {
for (let j = 0; j < srcImage.width; j++) {
if (grayscaleFilter) {
setGrayscale(j, i)
}
addBrightness(j, i, brightnessFilter)
addContrast(j, i, contrastFilter)
if (!grayscaleFilter) {
addRed(j, i, redFilter)
addGreen(j, i, greenFilter)
addBlue(j, i, blueFilter)
}
}
}
commitChanges()
}
Notre fonction de validation obtient simplement les pixels modifiés, applique sur l'objet ImageData et dessine le résultat.
function commitChanges() {
for (let i = 0; i < imgData.data.length; i++) {
imgData.data[i] = currentPixels[i]
}
ctx.putImageData(imgData, 0, 0, 0, 0, srcImage.width, srcImage.height)
}
Nous ne pouvons pas simplement faire
imgData.data = currentPixels
car c'est une constante qui ne peut pas être réaffectée.
Et c'est tout! Jouez avec le résultat final et téléchargez les images en cliquant avec le bouton droit> Enregistrer l'image sous ...
Dernières pensées
À la fin, nous avons développé une application avec moins de 200 lignes de code qui peut appliquer puissamment les filtres les plus courants que nous souhaiterions sur un outil de traitement d'image! Mais il manque quelque chose: où sont ces filtres de nom de ville sympas? Ils sont à peu près une combinaison prédéfinie de ceux-ci présentés ici. Pourquoi ne pas vous salir les mains et mettre à jour cette application avec vos villes préférées comme préréglages?
Le code source final peut être vérifié ici et vous pouvez jouer avec le logiciel ici . Tous les exemples ont été réalisés à l'aide de ce code. Photos: plage de Joaquina (éclaboussure), centre-ville de Cascavel (bleu), jardin botanique de Curitiba (luminosité), Ilha do Mel (contraste) et pont Hercílio Luz (niveaux de gris).
Aucun commentaire:
Enregistrer un commentaire