基于 Vue3和Canvas的图片随机色处理工具实现

开篇

本文基于工具小站中的图片随机色功能整理总结,因该功能比较繁琐,所以文章大概率会比较长。。
本文的网格边缘平滑函数用到了高斯模糊算法,关于高斯模糊的应用,以及模糊的效果,可以参考之前的一篇文章:前端图像处理:基于 Canvas 的图片模糊化实现

效果展示

在这里插入图片描述

技术栈

  • 前端框架:Vue 3 + Element Plus
  • 状态管理:组件内部状态
  • 图片处理:Web Worker + Canvas API
  • 颜色空间转换:RGB、LAB、HSL
  • 文件处理:File API、Blob API
  • 打包下载:JSZip

主要逻辑代码实现

处理图片通用函数

// 处理单张图片的通用函数
const processImage = (image) => {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const img = new Image();

    // 创建一个 Promise 来处理 worker 的响应
    const workerPromise = new Promise((workerResolve) => {
      const messageHandler = (e) => {
        if (e.data.id === image.id) {
          const { imageData } = e.data;
          const canvas = document.createElement("canvas");
          const ctx = canvas.getContext("2d");

          canvas.width = imageData.width;
          canvas.height = imageData.height;
          ctx.putImageData(imageData, 0, 0);

          if (image.id === "single") {
            singleImage.value.processedPreview = canvas.toDataURL("image/png");
            processing.value = false;
          } else {
            const targetImage = images.value.find((img) => img.id === image.id);
            if (targetImage) {
              targetImage.processedPreview = canvas.toDataURL("image/png");
            }
          }

          // 移除消息监听器
          worker.removeEventListener("message", messageHandler);
          workerResolve();
        }
      };

      // 添加消息监听器
      worker.addEventListener("message", messageHandler);
    });

    img.onload = () => {
      canvas.width = img.width;
      canvas.height = img.height;
      ctx.drawImage(img, 0, 0);

      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

      // 发送数据到 worker 处理,添加 algorithm 参数
      worker.postMessage({
        imageData,
        id: image.id,
        algorithm: settings.value.algorithm,
      });

      // 等待 worker 处理完成
      workerPromise.then(resolve).catch(reject);
    };

    img.onerror = reject;
    img.src = image.originalPreview;
  });
};

无论是处理单张图片,还是处理批量图片,都调用了这个方法。
这段方法的主要作用获取到图片数据,并将图片获取发送给web worker进行处理,待webworker处理完成并返回数据之后,匹配到图片并更新预览图。
这一块主要是通用性的方法,并不涉及过多的逻辑操作。当web worker接收到图片数据后,根据当前选择的模式,进行了不同的处理。
接下来我们讲讲四种模式的逻辑。

区域随机

  • 主函数:processRegionalRandomColor()
function processRegionalRandomColor(imageData, gridSize = 4) {
  const width = imageData.width;
  const height = imageData.height;
  const data = imageData.data;
  
  // 计算每个网格的大小
  const cellWidth = Math.floor(width / gridSize);
  const cellHeight = Math.floor(height / gridSize);
  
  // 处理每个网格
  for (let gridY = 0; gridY < gridSize; gridY++) {
    for (let gridX = 0; gridX < gridSize; gridX++) {
      // 计算当前网格的边界
      const startX = gridX * cellWidth;
      const startY = gridY * cellHeight;
      const endX = Math.min((gridX + 1) * cellWidth, width);
      const endY = Math.min((gridY + 1) * cellHeight, height);
      
      // 收集网格内的像素
      const gridPixels = [];
      const originalColors = new Map(); // 存储原始颜色
      
      for (let y = startY; y < endY; y++) {
        for (let x = startX; x < endX; x++) {
          const i = (y * width + x) * 4;
          const r = data[i];
          const g = data[i + 1];
          const b = data[i + 2];
          const lab = rgbToLab(r, g, b);
          
          gridPixels.push({
            index: i,
            rgb: [r, g, b],
            lab: lab
          });
          
          // 记录原始颜色
          originalColors.set(i, {
            rgb: [r, g, b],
            lab: lab
          });
        }
      }
      
      // 对网格内的像素进行聚类
      const { centroids, colorMappings } = clusterColors({ 
        data: new Uint8ClampedArray(gridPixels.length * 4),
        width: endX - startX,
        height: endY - startY
      }, 8);
      
      // 为每个聚类生成随机色相偏移
      const hueShifts = new Map();
      centroids.forEach((_, index) => {
        hueShifts.set(index, Math.random() * 360);
      });
      
      // 应用颜色映射,保持原始明度
      for (const pixel of gridPixels) {
        let minDistance = Infinity;
        let closestCentroid = 0;
        
        centroids.forEach((centroid, index) => {
          const distance = deltaE(pixel.lab, centroid);
          if (distance < minDistance) {
            minDistance = distance;
            closestCentroid = index;
          }
        });
        
        // 获取原始颜色
        const originalColor = originalColors.get(pixel.index);
        
        // 获取色相偏移
        const hueShift = hueShifts.get(closestCentroid);
        
        // 转换到 HSL 空间
        const hsl = rgbToHsl(originalColor.rgb[0], originalColor.rgb[1], originalColor.rgb[2]);
        
        // 应用色相偏移,保持饱和度和明度
        const newHsl = [
          (hsl[0] + hueShift) % 360,
          hsl[1],
          hsl[2]
        ];
        
        // 转回 RGB
        const newRgb = hslToRgb(newHsl[0], newHsl[1], newHsl[2]);
        
        data[pixel.index] = newRgb[0];
        data[pixel.index + 1] = newRgb[1];
        data[pixel.index + 2] = newRgb[2];
      }
    }
  }
  
  // 平滑网格边界
  smoothGridBoundaries(imageData, gridSize);
  
  return imageData;
}

上面这段代码的逻辑,主要是将图像分割成网格,并对每个网格内的颜色进行了处理。
1.计算每个网格的大小
2.使用嵌套循环遍历每个网格,并收集网格内的所有像素,并将它们的RGB、Lab颜色值存储起来,同时保存原始颜色信息
3.对网格内的像素进行颜色聚类
4.为每个聚类生成随机的色相偏移值
5.对网格内的每个像素,都分别进行找到最接近的聚类中心、获取原始颜色和对应的色相偏移、将颜色从RGB转换到HSL空间、应用色相偏移并保持饱和度和明亮度不变、将新的HSL颜色转回RGB,并更新颜色数据
6.调用smoothGridBoundaries方法平滑网格边界
7.将处理好的数据返回给imageData

  • 辅助函数:rgbToHsl()
// 添加 RGB 到 HSL 的转换函数
function rgbToHsl(r, g, b) {
  r /= 255;
  g /= 255;
  b /= 255;
  
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;
  
  if (max === min) {
    h = s = 0;
  } else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    
    switch (max) {
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      case b: h = (r - g) / d + 4; break;
    }
    
    h /= 6;
  }
  
  return [h * 360, s * 100, l * 100];
}

这个函数的目的,是将RGB(红、绿、蓝)颜色空间转换为HSL(色相、饱和度、亮度)颜色空间

  • 辅助函数:hslToRgb()
// 添加 HSL 到 RGB 的转换函数
function hslToRgb(h, s, l) {
  h /= 360;
  s /= 100;
  l /= 100;
  
  let r, g, b;
  
  if (s === 0) {
    r = g = b = l;
  } else {
    const hue2rgb = (p, q, t) => {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1/6) return p + (q - p) * 6 * t;
      if (t < 1/2) return q;
      if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
      return p;
    };
    
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    
    r = hue2rgb(p, q, h + 1/3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1/3);
  }

这个函数的目的是将HSL(色相、饱和度、亮度)颜色空间转换回RGB(红、绿、蓝)颜色空间。其实就是上个函数的反转版。

  • 辅助函数:clusterColors()
// 对图片颜色进行聚类
function clusterColors(imageData, maxColors = 16) {
  const pixels = [];
  const data = imageData.data;
  
  // 收集所有像素的颜色
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    const lab = rgbToLab(r, g, b);
    pixels.push({
      rgb: [r, g, b],
      lab: lab
    });
  }

  // 使用 K-means 聚类
  const centroids = [];
  const colorMap = new Map();
  
  // 随机选择初始中心点
  for (let i = 0; i < maxColors; i++) {
    const randomIndex = Math.floor(Math.random() * pixels.length);
    centroids.push(pixels[randomIndex].lab);
  }

  // 迭代优化聚类
  const maxIterations = 10;
  for (let iteration = 0; iteration < maxIterations; iteration++) {
    const clusters = Array(maxColors).fill().map(() => []);
    
    // 将每个像素分配到最近的中心点
    for (const pixel of pixels) {
      let minDistance = Infinity;
      let closestCentroid = 0;
      
      for (let i = 0; i < centroids.length; i++) {
        const distance = deltaE(pixel.lab, centroids[i]);
        if (distance < minDistance) {
          minDistance = distance;
          closestCentroid = i;
        }
      }
      
      clusters[closestCentroid].push(pixel);
    }
    
    // 更新中心点
    for (let i = 0; i < maxColors; i++) {
      if (clusters[i].length === 0) continue;
      
      const newCentroid = [0, 0, 0];
      for (const pixel of clusters[i]) {
        newCentroid[0] += pixel.lab[0];
        newCentroid[1] += pixel.lab[1];
        newCentroid[2] += pixel.lab[2];
      }
      
      centroids[i] = [
        newCentroid[0] / clusters[i].length,
        newCentroid[1] / clusters[i].length,
        newCentroid[2] / clusters[i].length
      ];
    }
  }

  // 为每个聚类生成随机颜色
  const colorMappings = new Map();
  centroids.forEach((centroid, i) => {
    colorMappings.set(i, getRandomColor());
  });

  return { centroids, colorMappings };
}

这段代码实现了一个基于K-means聚类算法的颜色聚类功能。将图像中的颜色分成若干个主要颜色(最多为maxColors个),并返回这些颜色的Lab颜色空间的中心和与每个中心对应的随机颜色。
代码的逻辑稍微梳理一下就可以明白,这里需要讲一下k-means算法,这是一种聚类方法,目的是将数据分成若干个“簇”。这些簇中的数据点要尽可能的相似,而簇与簇之间要尽可能的不同。而我在这里使用这个算法的目的是什么呢?是为了将图片中同一种颜色转成另一种颜色。比如说图片中的红色,都随机转换成蓝色,而不是有的地方转成蓝色,有的地方转成红色。

  • 平滑网格边界函数:smoothGridBoundaries()
// 辅助函数: 平滑网格边界
function smoothGridBoundaries(imageData, gridSize) {
  const width = imageData.width;
  const height = imageData.height;
  const data = imageData.data;
  const cellWidth = Math.floor(width / gridSize);
  const cellHeight = Math.floor(height / gridSize);
  
  // 创建临时缓冲区
  const tempData = new Uint8ClampedArray(data);
  
  // 在网格边界处应用高斯模糊(这里处理的是垂直边界)
  const blurRadius = 3;
  for (let gridY = 1; gridY < gridSize; gridY++) {
  	// 确定垂直网格的边界
    const y = gridY * cellHeight;
    // 遍历每一列像素
    for (let x = 0; x < width; x++) {
    	// 对周围的像素进行高斯加权,这里对dy范围内垂直方向上blurRadius个像素都进行了模糊
      for (let dy = -blurRadius; dy <= blurRadius; dy++) {
        const cy = y + dy;
        if (cy < 0 || cy >= height) continue;
        // 对dx范围内水平方向上blurRadius个像素进行模糊
        for (let dx = -blurRadius; dx <= blurRadius; dx++) {
          const cx = x + dx;
          if (cx < 0 || cx >= width) continue;
          // 计算高斯权重
          const weight = Math.exp(-(dx * dx + dy * dy) / (2 * blurRadius * blurRadius));
          // 加权累加像素值
          const srcIdx = (cy * width + cx) * 4;
          const dstIdx = (y * width + x) * 4;
          
          for (let c = 0; c < 3; c++) {
            tempData[dstIdx + c] += data[srcIdx + c] * weight;
          }
        }
      }
    }
  }
  
  // 在网格边界处应用高斯模糊(这里处理的是水平边界)
  for (let gridX = 1; gridX < gridSize; gridX++) {
    const x = gridX * cellWidth;
    for (let y = 0; y < height; y++) {
      for (let dx = -blurRadius; dx <= blurRadius; dx++) {
        const cx = x + dx;
        if (cx < 0 || cx >= width) continue;
        
        for (let dy = -blurRadius; dy <= blurRadius; dy++) {
          const cy = y + dy;
          if (cy < 0 || cy >= height) continue;
          
          const weight = Math.exp(-(dx * dx + dy * dy) / (2 * blurRadius * blurRadius));
          const srcIdx = (cy * width + cx) * 4;
          const dstIdx = (y * width + x) * 4;
          
          for (let c = 0; c < 3; c++) {
            tempData[dstIdx + c] += data[srcIdx + c] * weight;
          }
        }
      }
    }
  }
  
  // 将平滑后的结果复制回原数组
  for (let i = 0; i < data.length; i++) {
    data[i] = tempData[i];
  }
}

这段代码的目的是用来平滑网格边界,目标是在给定的网格的边界处应用高斯模糊,从而使得网格边界更快平滑。也是对于我们是网格化处理图片的一种后续的维护服务。
这里我再讲一下高斯模糊。高斯模糊是一种图像模糊技术,通过对每个像素及其邻近像素赋予不同的权重来实现模糊效果,这种模糊效果可以参考我们之前实现的图像模糊化功能。而这里的权重是什么呢?权重是根据高斯函数(也就是正态分布函数)计算出来的,距离模糊中心越远,像素权重越小。
这个公式就是下面这种
在这里插入图片描述
而在我们的方法里,以垂直边界平滑逻辑为例,我会给出极其详尽的注释。

全局随机

// 全局随机
function processRandomColor(imageData) {
	// 获取聚类中心和颜色映射
  const { centroids, colorMappings } = clusterColors(imageData);
  const data = imageData.data;
  
  // 处理每个像素
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    const pixelLab = rgbToLab(r, g, b);
    
    // 找到最近的聚类中心
    let minDistance = Infinity;
    let closestCentroid = 0;
    
    centroids.forEach((centroid, index) => {
      const distance = deltaE(pixelLab, centroid);
      if (distance < minDistance) {
        minDistance = distance;
        closestCentroid = index;
      }
    });
    
    // 应用对应的随机颜色
    const newColor = colorMappings.get(closestCentroid);
    data[i] = newColor[0];
    data[i + 1] = newColor[1];
    data[i + 2] = newColor[2];
  }
  
  return imageData;
}

这段代码的主要逻辑为:
1.对输入的图像进行颜色聚类,找出颜色簇的中心;
2.对图像中的每个像素,根据其颜色与各个聚类中心的距离,找出最接近的聚类中心;
3.用聚类中心对应的随机颜色替换该像素的颜色;
4.返回修改后的图像数据;

所有代码

限于篇幅原因,其他两个模式我就不讲解了,感兴趣的可以自己研究梳理一下。下面给出这个功能涉及到的两个文件的完整代码。

  • RandomColor.vue
<template>
  <div class="app-container">
    <header class="app-header">
      <h1>图片随机色</h1>
      <p class="subtitle">专业的图片随机色处理工具,支持多种算法</p>
    </header>

    <main class="main-content">
      <!-- 添加标签页 -->
      <el-tabs v-model="activeTab" class="image-tabs">
        <el-tab-pane label="单张处理" name="single">
          <!-- 单张图片处理区域 -->
          <div class="upload-section" v-if="!singleImage">
            <el-upload
              class="upload-drop-zone"
              drag
              :auto-upload="false"
              accept="image/*"
              :show-file-list="false"
              :multiple="false"
              @change="handleSingleFileChange"
            >
              <el-icon class="upload-icon"><upload-filled /></el-icon>
              <div class="upload-text">
                <h3>将图片拖到此处,或点击上传</h3>
                <p>支持 PNG、JPG、WebP 等常见格式</p>
              </div>
            </el-upload>
          </div>

          <div v-else class="process-section single-mode">
            <div class="image-comparison">
              <!-- 原图预览 -->
              <div class="image-preview original">
                <h3>原图</h3>
                <div class="image-container">
                  <img
                    :src="singleImage.originalPreview"
                    :alt="singleImage.file.name"
                  />
                </div>
              </div>

              <!-- 随机色效果预览 -->
              <div class="image-preview processed">
                <h3>随机色效果</h3>
                <div class="image-container">
                  <img
                    v-if="singleImage.processedPreview"
                    :src="singleImage.processedPreview"
                    :alt="singleImage.file.name + '(随机色)'"
                  />
                  <div v-else class="placeholder">
                    <el-icon><picture-rounded /></el-icon>
                    <span>待处理</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="批量处理" name="batch">
          <!-- 批量处理区域 -->
          <div class="upload-section" v-if="!images.length">
            <el-upload
              class="upload-drop-zone"
              drag
              :auto-upload="false"
              accept="image/*"
              :show-file-list="false"
              :multiple="true"
              @change="handleBatchFileChange"
            >
              <el-icon class="upload-icon">
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值