Ejercicio 4: Kernel de imagenes #
1. Introducción
Un kernel, matriz de convolución o máscara es una matriz diminuta utilizada en el procesamiento de imágenes para la detección de bordes, el relieve, la nitidez y otras funciones. Para ello se utiliza la convolución entre el núcleo y una imagen. Dicho de otro modo, el núcleo o kernel es la función que determina cómo cada píxel de la imagen de salida se ve afectado por los píxeles vecinos (incluido él mismo) de la imagen de entrada.

2. Antecedentes y trabajo previo
Algunos de los trabajos previos relevantes en este campo incluyen el filtro de Sobel, que se utiliza para detectar bordes en una imagen, el filtro de Laplaciano, que se utiliza para la detección de características y el filtro de Gaussiano, que se utiliza para suavizar imágenes y eliminar ruido. Además, se han desarrollado muchos otros kernels y filtros específicos para tareas particulares en visión por computadora, como el kernel de Gabor para el análisis de texturas y el kernel de Haar para la detección de objetos en imágenes.
3. Solución
Un script que permite cargar una imagen para que sea procesada por el kernel de elección. Al mismo tiempo se elige los niveles de Hue saturation lighthness o hue saturation value.
De igual forma, se presentan los histogramas RGB de la imagen cargada.
Código
function bound(color) {
if (color > 255)
return 255
else if (color < 0)
return 0
return color
}
function applyLightness(image, width, height) {
let L = document.querySelector('#lightness-input').value
if (L == '')
return;
let canvas = document.querySelector("#canvas-for-rgba");
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
var data = ctx.getImageData(0, 0, width, height).data;
lightness_data = []
let R_array = []
let G_array = []
let B_array = []
let def = document.querySelector('#lightness-definition-select').value
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
let a = data[i + 3];
let c = constant(L, r, g, b, def)
let cr = c * r
let cg = c * g
let cb = c * b
let R = bound(Math.round(cr))
let G = bound(Math.round(cg))
let B = bound(Math.round(cb))
let A = a
R_array.push(R)
G_array.push(G)
B_array.push(B)
lightness_data.push(R)
lightness_data.push(G)
lightness_data.push(B)
lightness_data.push(A)
}
canvas = document.querySelector("#transformed-image-canvas");
canvas.width = width;
canvas.height = height;
ctx = canvas.getContext("2d");
var imageData = canvas.getContext('2d').createImageData(width, height);
imageData.data.set(lightness_data);
ctx.putImageData(imageData, 0, 0)
drawHistogram(R_array, 'red');
drawHistogram(G_array, 'green');
drawHistogram(B_array, 'blue');
}
// función de procesamiento de la imagen
function processImage(image, width, height) {
let canvas = document.querySelector("#canvas-for-rgba");
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
var data = ctx.getImageData(0, 0, width, height).data; // data es un arreglo con los valores RGBA de la imagen (arreglo unidimensional)
transformed_data = [] // es el arreglo transformado o procesado
let ker = kernel(document.querySelector('#kernel-select').value) // kernel a usar
let R_array = []
let G_array = []
let B_array = []
for (let i = 0; i < data.length; i += 4) { // se itera de 4, i corresponde al valor R del pixel i-ésimo de la imagen
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
let a = data[i + 3];
let pos = position(i, width, height)
let nbs = neighbours(i, pos, width)
let ws = weights(ker, pos)
let sum = ws.reduce((partialSum, a) => partialSum + a, 0); // en la matriz se debe garantizar que la suma de los pesos no sea cero para que funcione
let rtotal = 0
let gtotal = 0
let btotal = 0
let atotal = 0
// suma ponderada para cada valor R G B A
for (let j = 0; j < nbs.length; j++) {
rtotal += data[nbs[j]] * ws[j]
gtotal += data[nbs[j] + 1] * ws[j]
btotal += data[nbs[j] + 2] * ws[j]
atotal += data[nbs[j] + 3] * ws[j]
}
// se obtiene la suma ponderada: error cuando sum es cero ...
let R = Math.round(rtotal / sum)
let G = Math.round(gtotal / sum)
let B = Math.round(btotal / sum)
let A = Math.round(atotal / sum)
R_array.push(R)
G_array.push(G)
B_array.push(B)
// se agregan los nuevos valores al arreglo transformado
transformed_data.push(R)
transformed_data.push(G)
transformed_data.push(B)
transformed_data.push(A)
}
// se crea canvas de la imagen transformada para mostrarla en pantalla
// nota: se necesita usar canvas para poder visualizar la imagen apartir del arreglo de R G B A
canvas = document.querySelector("#transformed-image-canvas");
canvas.width = width;
canvas.height = height;
ctx = canvas.getContext("2d");
var imageData = canvas.getContext('2d').createImageData(width, height);
imageData.data.set(transformed_data);
ctx.putImageData(imageData, 0, 0)
drawHistogram(R_array, 'red');
drawHistogram(G_array, 'green');
drawHistogram(B_array, 'blue');
}
// se procesa imagen cuando se sube archivo
const image_input = document.querySelector("#image-input");
image_input.addEventListener("change", function() {
const reader = new FileReader();
reader.readAsDataURL(this.files[0]);
reader.onload = (e) => {
const image = new Image();
image.src = e.target.result;
image.onload = (e) => {
const width = e.target.width;
const height = e.target.height;
const uploaded_image = reader.result
document.querySelector("#uploaded-image").src = uploaded_image;
processImage(image, width, height)
document.querySelector('#lightness-input').removeAttribute("hidden");
document.querySelector('#lightness-definition-select').removeAttribute('hidden')
};
};
});
// se procesa imagen cuando se cambia el valor del select kernel
const kernel_select = document.querySelector("#kernel-select");
kernel_select.addEventListener("change", function() {
const image = new Image();
let img = document.querySelector("#uploaded-image")
image.src = img.src;
let width = img.width
let height = img.height
processImage(image, width, height)
});
// se procesa imagen cuando se cambia el valor del select kernel
const lightness_input = document.querySelector("#lightness-input");
lightness_input.addEventListener("change", function() {
const image = new Image();
let img = document.querySelector("#uploaded-image")
image.src = img.src;
let width = img.width
let height = img.height
applyLightness(image, width, height)
});
// se procesa imagen cuando se cambia el valor del select kernel
const lightness_definition_select = document.querySelector("#lightness-definition-select");
lightness_definition_select.addEventListener("change", function() {
const image = new Image();
let img = document.querySelector("#uploaded-image")
image.src = img.src;
let width = img.width
let height = img.height
applyLightness(image, width, height)
});
// obtener posición del pixel según su índice y los valores weight y height
let position = (i, w, h) => {
if (i == 0)
return 'top-left-corner';
else if (i == (w * 4) - 1)
return 'top-right-corner';
else if (i == h * ((w * 4) - 1))
return 'bottom-left-corner';
else if (i == (h * w * 4) - 1)
return 'bottom-right-corner';
else if (i < (w * 4) - 1)
return 'top-row';
else if (i % (w * 4) == (w * 4) - 1)
return 'right-row';
else if (i > h * ((w * 4) - 1) && i < (h * w * 4) - 1)
return 'bottom-row';
else if (i % (w * 4) == 0)
return 'left-row';
else
return 'inner-cell'
}
// arreglo de índices según posición, que será usado para obtener las posiciones de los pixeles vecinos (neighbours) y
// las posiciones de los pesos de la matriz del kernel
let indexes = (position) => {
if (position == 'inner-cell')
return [0, 1, 2,
3, 4, 5,
6, 7, 8]
else if (position == 'left-row')
return [ 1, 2,
4, 5,
7, 8]
else if (position == 'right-row')
return [0, 1,
3, 4,
6, 7 ]
else if (position == 'top-row')
return [
3, 4, 5,
6, 7, 8]
else if (position == 'bottom-row')
return [0, 1, 2,
3, 4, 5,
]
else if (position == 'top-right-corner')
return [
3, 4,
6, 7 ]
else if (position == 'top-left-corner')
return [
4, 5,
7, 8]
else if (position == 'bottom-left-corner')
return [ 1, 2,
4, 5
]
else if (position == 'bottom-right-corner')
return [0, 1,
3, 4
]
else
return []
}
// arreglo con los índices de los pixeles vecinos
let neighbours = (i, position, w) => {
let matrix = [i - (w * 4) - 4, i - (w * 4), i - (w * 4) + 4,
i - 4, i , i + 4,
i + (w * 4) - 4, i + (w * 4), i + (w * 4) + 4]
let idx = indexes(position)
let nbs = []
idx.forEach((i) => {
nbs.push(matrix[i])
})
return nbs
}
// kernels disponibles: cada matriz es una matriz de pesos
let kernel = (kernel) => {
if (kernel == 'identity')
return [0, 0, 0,
0, 1, 0,
0, 0, 0]
else if (kernel == 'gaussian-blur')
return [1, 2, 1,
2, 4, 1,
1, 2, 1]
else if (kernel == 'emboss')
return [-2, -1, 0,
-1, 2, 1,
0, 1, 2]
else if (kernel == 'left-sobel')
return [1, 0, -1,
2, 1, -2,
1, 0, -1]
else if (kernel == 'right-sobel')
return [-1, 0, 1,
-2, 1, 2, // se agregó 1 en la posición central para garantizar que suma de pesos no sea cero
-1, 0, 1]
else if (kernel == 'top-sobel')
return [1, 2, 1,
0, 1, 0,
-1, -2, -1]
else if (kernel == 'bottom-sobel')
return [-1, -2, -1,
0, 1, 0,
1, 2, 1]
else if (kernel == 'sharpen')
return [0, -1, 0,
-1, 5, -1,
0, -1, 0]
else if (kernel == 'outline')
return [-1, -1, -1,
-1, 9, -1,
-1, 1, -1]
else
return []
}
// arreglo de pesos que se usarán en la suma ponderada del pixel actual: depende del kernel y de la posición del pixel
let weights = (ker, position) => {
let idx = indexes(position)
let ws = []
idx.forEach((i) => {
ws.push(ker[i])
})
return ws
}
let constant = (L, R, G, B, definition) => {
if (definition == 'HSI')
return 3 * (L / (R + G + B))
else if (definition == 'HSV')
return L / Math.max(R, G, B)
else if (definition == 'HSL')
return 2 * (L / (Math.min(R, G, B) + Math.max(R, G, B)))
else if (definition == 'Luma 601')
return L / (0.2989 * R + 0.5870 * G + 0.1140 * B)
else if (definition == 'Luma 240')
return L / (0.212 * R + 0.701 * G + 0.087 * B)
else if (definition == 'Luma 709')
return L / (0.2126 * R + 0.7152 * G + 0.0722 * B)
else if (definition == 'Luma 2020')
return L / (0.2627 * R + 0.6780 * G + 0.0593 * B)
else
return 1
}
function drawHistogram(data, color) {
let colors = {
'red': '#FF0000',
'green': '#00FF00',
'blue': '#0000FF'
}
var domain = [0, 255]
var margin = { top: 30, right: 30, bottom: 30, left: 50 },
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var x = d3
.scaleLinear()
.domain(domain)
.range([0, width]);
var histogram = d3
.histogram()
.domain(x.domain())
.thresholds(x.ticks(256));
var bins = histogram(data);
d3
.select(`#${color}-histogram svg`).remove()
var svg = d3
.select(`#${color}-histogram`)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var y = d3
.scaleLinear()
.range([height, 0])
.domain([
0,
d3.max(bins, function(d) {
return d.length;
})
]);
svg.append("g").call(d3.axisLeft(y));
svg
.selectAll("rect")
.data(bins)
.enter()
.append("rect")
.attr("x", 1)
.attr("transform", function(d) {
return "translate(" + x(d.x0) + "," + y(d.length) + ")";
})
.attr("width", function(d) {
return x(d.x1) - x(d.x0) - 1;
})
.attr("height", function(d) {
return height - y(d.length);
})
.style("fill", colors[color]);
}
Explicación breve #
La función processImage procesa la imagen utilizando una matriz de convolución (kernel) y devuelve una imagen transformada. La función applyLightness ajusta la luminosidad de la imagen según el valor de entrada del usuario. También dibuja un histograma para cada componente RGB de la imagen transformada. Ambas funciones utilizan la función bound para garantizar que los valores RGB estén dentro del rango [0, 255]. La matriz de convolución y la definición de luminosidad se obtienen a través de selecciones de usuario en la página web.
4. Conclusión y trabajo a futuro/
El trabajo realizado en este campo ha sido fundamental para el desarrollo de la visión por computadora y el aprendizaje automático, y el trabajo futuro promete seguir mejorando nuestras capacidades para procesar y comprender imágenes.
A medida que las aplicaciones que involucran conceptos de computación visual o procesamiento de imágenes se vuelven más complejas, se espera que la investigación sobre los kernels y el procesamiento de imágenes continúe avanzando. Por ejemplo, se están investigando nuevos kernels y filtros para mejorar la detección de características y la clasificación de imágenes. Además, se están desarrollando nuevas técnicas para interpretar y visualizar las características aprendidas por las redes neuronales convolucionales, que son ampliamente utilizadas para estas tareas.