12 - Problema resuelto: Procesamiento paralelo de imágenes

Objetivo: aplicar filtros (blanco y negro, blur, contraste) a una carpeta de imágenes en paralelo y comparar tiempos entre ejecución secuencial, pool fijo e hilos virtuales.

  • Entrada: carpeta con imágenes (PNG/JPG).
  • Salida: carpeta out/ con copias filtradas.
  • Refuerza: trabajo CPU-bound pesado, dimensionar pools y medir vs Loom.
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class ProcesarImagenes {
  public static void main(String[] args) throws Exception {
    Path in = Paths.get("imagenes-in");
    Path out = Paths.get("out");
    Files.createDirectories(out);

    List<Path> archivos = listarImagenes(in);
    if (archivos.isEmpty()) {
      System.out.println("No hay archivos en " + in.toAbsolutePath());
      return;
    }

    System.out.println("Archivos a procesar: " + archivos.size());
    medir("Secuencial", () -> procesarSecuencial(archivos, out));
    medir("Pool fijo", () -> procesarConPoolFijo(archivos, out, Runtime.getRuntime().availableProcessors()));
    medir("Hilos virtuales", () -> procesarConVirtuales(archivos, out));
  }

  @FunctionalInterface
  interface Task { void run() throws Exception; }

  private static void medir(String etiqueta, Task tarea) throws Exception {
    long ini = System.nanoTime();
    tarea.run();
    long fin = System.nanoTime();
    System.out.println(etiqueta + " tardo: " + TimeUnit.NANOSECONDS.toMillis(fin - ini) + " ms");
  }

  private static List<Path> listarImagenes(Path in) throws IOException {
    List<Path> res = new ArrayList<>();
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(in, "*.{png,jpg,jpeg}")) {
      for (Path p : stream) res.add(p);
    }
    return res;
  }

  private static void procesarSecuencial(List<Path> archivos, Path out) {
    for (Path p : archivos) {
      try {
        procesarArchivo(p, out, "seq");
      } catch (IOException e) {
        System.err.println("Error en " + p + ": " + e.getMessage());
      }
    }
  }

  private static void procesarConPoolFijo(List<Path> archivos, Path out, int hilos) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(hilos);
    try {
      List<Callable<Path>> tareas = new ArrayList<>();
      for (Path p : archivos) {
        tareas.add(() -> procesarArchivo(p, out, "fixed"));
      }
      List<Future<Path>> resultados = pool.invokeAll(tareas);
      for (Future<Path> f : resultados) {
        try { f.get(); } catch (Exception e) { System.err.println("Fallo: " + e.getMessage()); }
      }
    } finally {
      pool.shutdown();
      pool.awaitTermination(1, TimeUnit.MINUTES);
    }
  }

  private static void procesarConVirtuales(List<Path> archivos, Path out) throws Exception {
    try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
      List<Callable<Path>> tareas = new ArrayList<>();
      for (Path p : archivos) {
        tareas.add(() -> procesarArchivo(p, out, "vt"));
      }
      List<Future<Path>> resultados = pool.invokeAll(tareas);
      for (Future<Path> f : resultados) {
        try { f.get(); } catch (Exception e) { System.err.println("Fallo: " + e.getMessage()); }
      }
    }
  }

  private static Path procesarArchivo(Path origen, Path out, String prefijo) throws IOException {
    BufferedImage img = ImageIO.read(origen.toFile());
    BufferedImage bn = filtroBlancoNegro(img);
    BufferedImage blur = filtroBlur(bn);
    BufferedImage contraste = filtroContraste(blur, 1.2);

    Path destino = out.resolve(prefijo + "-" + origen.getFileName().toString());
    ImageIO.write(contraste, "png", destino.toFile());
    return destino;
  }

  private static BufferedImage filtroBlancoNegro(BufferedImage src) {
    BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB);
    for (int y = 0; y < src.getHeight(); y++) {
      for (int x = 0; x < src.getWidth(); x++) {
        Color c = new Color(src.getRGB(x, y));
        int g = (int)(0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue());
        dest.setRGB(x, y, new Color(g, g, g).getRGB());
      }
    }
    return dest;
  }

  private static BufferedImage filtroBlur(BufferedImage src) {
    int w = src.getWidth(), h = src.getHeight();
    BufferedImage dest = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
    int[] kernel = {1,1,1,1,1,1,1,1,1};
    int kSum = 9;
    for (int y = 1; y < h - 1; y++) {
      for (int x = 1; x < w - 1; x++) {
        int r = 0, g = 0, b = 0, idx = 0;
        for (int ky = -1; ky <= 1; ky++) {
          for (int kx = -1; kx <= 1; kx++) {
            Color c = new Color(src.getRGB(x + kx, y + ky));
            int k = kernel[idx++];
            r += c.getRed() * k;
            g += c.getGreen() * k;
            b += c.getBlue() * k;
          }
        }
        dest.setRGB(x, y, new Color(r / kSum, g / kSum, b / kSum).getRGB());
      }
    }
    return dest;
  }

  private static BufferedImage filtroContraste(BufferedImage src, double factor) {
    BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB);
    for (int y = 0; y < src.getHeight(); y++) {
      for (int x = 0; x < src.getWidth(); x++) {
        Color c = new Color(src.getRGB(x, y));
        int r = ajustar(c.getRed(), factor);
        int g = ajustar(c.getGreen(), factor);
        int b = ajustar(c.getBlue(), factor);
        dest.setRGB(x, y, new Color(r, g, b).getRGB());
      }
    }
    return dest;
  }

  private static int ajustar(int valor, double factor) {
    int v = (int)((valor - 128) * factor + 128);
    return Math.max(0, Math.min(255, v));
  }
}

12.1 Ejecución y comparación

  • Coloca archivos en imagenes-in/ (PNG/JPG).
  • Ejecuta con Java 21+ para soportar hilos virtuales.
  • El programa genera out/ con tres prefijos: seq-, fixed-, vt-, midiendo tiempo en consola.
  • Compara los ms de cada modo para ver el beneficio de un pool fijo vs hilos virtuales en una carga CPU-bound.
Comparación de tiempos secuencial vs pool fijo vs hilos virtuales