前端图像处理惩罚实战: 基于Web Worker和SIMD优化实现图像转灰度功能 ...

打印 上一主题 下一主题

主题 866|帖子 866|积分 2598

开篇

   本篇博客是对于工具小站-图片工具模块-图片转灰度功能的总结。
  原理与作用

   图像灰度化处理惩罚的核心思想是将彩色图像的每个像素转换为灰度值。灰度值通常是根据图像的每个像素的红、绿、蓝(RGB)通道的值按某种加权方式计算出来的。如许可以将彩色图像转换为灰度图像,减少颜色信息,使图像只保留亮度信息。
灰度值的计算方法可以有差别的算法,例如加权平均法、简单平均法等。常见的灰度化算法有:
加权法:根据差别颜色的感知权重来计算灰度值。常见权重为:红色0.2156,绿色0.7152,蓝色0.0722.
平均法:将RGB值平均后作为灰度值。
亮度法:与加权法类似,但利用差别的权重(红色0.299,绿色0.587,蓝色0.114)。
  功能实现逻辑

思量到图片处理惩罚时需要计算的数据量较大,为了避免壅闭主线程的渲染,我接纳了WebWorker的方式在后台线程中实行图像处理惩罚操纵。
下面将简单担当一下webworker文件中,重要的功能的实现逻辑代码。
灰度值查找表预计算

为了优化灰度化处理惩罚性能,这里首先创建了一个grayLookupTable查找表。这张表预计算了每个RGB值对应的灰度值(按照差别的算法),如许可以在现实处理惩罚时直接查找对应的值,避免重复计算。
  1. // 预计算灰度值查找表
  2. const grayLookupTable = new Uint8Array(256 * 3);
  3. const weights = {
  4.   weighted: [0.2126, 0.7152, 0.0722],
  5.   average: [0.3333, 0.3333, 0.3334],
  6.   luminosity: [0.299, 0.587, 0.114]
  7. };
  8. // 初始化查找表
  9. function initLookupTables() {
  10.   Object.keys(weights).forEach(method => {
  11.     const [r, g, b] = weights[method];
  12.     for (let i = 0; i < 256; i++) {
  13.       grayLookupTable[i * 3] = i * r;
  14.       grayLookupTable[i * 3 + 1] = i * g;
  15.       grayLookupTable[i * 3 + 2] = i * b;
  16.     }
  17.   });
  18. }
  19. initLookupTables();
复制代码
SIMD优化的灰度处理惩罚

在欣赏器支持SIMD(单指令多数据)时,利用SMD加速了灰度计算。SIMD允许一次操纵多个数据元素,因此可以明显提高性能。
processGrayscaleSIMD方法利用SIMD操纵来并行处理惩罚图像中的多个像素。具体步骤为:
1.加载权重:将算法选择的RGB权重通过SIMD.Float32x4.splat()方法加载到向量中。
2.处理惩罚图像数据:利用SIMD.Float32x4来加载多个像素的数据(每4个像素),举行加权计算,终极得到灰度值。
3.生存结果:将计算后的灰度值存回到图像数据中。
假如欣赏器不支持SIMD,则回退到标准的灰度处理惩罚方法(见下文)。
  1. // 使用 SIMD 优化的灰度处理(如果浏览器支持)
  2. function processGrayscaleSIMD(imageData, algorithm = 'weighted') {
  3.   const data = imageData.data;
  4.   const len = data.length;
  5.   const [r, g, b] = weights[algorithm];
  6.   
  7.   // 使用 SIMD 优化
  8.   if (typeof SIMD !== 'undefined' && SIMD.Float32x4) {
  9.     const rWeight = SIMD.Float32x4.splat(r);
  10.     const gWeight = SIMD.Float32x4.splat(g);
  11.     const bWeight = SIMD.Float32x4.splat(b);
  12.    
  13.     for (let i = 0; i < len; i += 16) {
  14.       const rgba0 = SIMD.Float32x4.load(data, i);
  15.       const rgba1 = SIMD.Float32x4.load(data, i + 4);
  16.       const rgba2 = SIMD.Float32x4.load(data, i + 8);
  17.       const rgba3 = SIMD.Float32x4.load(data, i + 12);
  18.       
  19.       const gray0 = SIMD.Float32x4.add(
  20.         SIMD.Float32x4.mul(rgba0, rWeight),
  21.         SIMD.Float32x4.add(
  22.           SIMD.Float32x4.mul(rgba1, gWeight),
  23.           SIMD.Float32x4.mul(rgba2, bWeight)
  24.         )
  25.       );
  26.       
  27.       SIMD.Float32x4.store(data, i, gray0);
  28.       SIMD.Float32x4.store(data, i + 4, gray0);
  29.       SIMD.Float32x4.store(data, i + 8, gray0);
  30.     }
  31.     return imageData;
  32.   }
  33.   
  34.   return processGrayscaleStandard(imageData, algorithm);
  35. }
复制代码
标准灰度处理惩罚(查找表优化)

本方法为标准灰度处理惩罚方法,通过查找表加速了每个像素的灰度值计算。具体步骤如下:
1.查找表选择:根据所选的算法(如加权法、平均值法等),确定利用的查找表的偏移量。
2.遍历图像数据中的每个像素,提取RGB值。
3.查找灰度值:利用查找表中的预计算结果来得到每个像素的灰度值。
4.更新像素值:将计算得到的灰度值赋给RGB通道,保持好Alpha通道不变。
  1. // 标准灰度处理(使用查找表优化)
  2. function processGrayscaleStandard(imageData, algorithm = 'weighted') {
  3.   const data = imageData.data;
  4.   const len = data.length;
  5.   const tableOffset = algorithm === 'weighted' ? 0 : (algorithm === 'average' ? 256 : 512);
  6.   
  7.   // 使用 Uint32Array 视图加速访问
  8.   const pixels = new Uint32Array(data.buffer);
  9.   const pixelCount = len >> 2;
  10.   
  11.   for (let i = 0; i < pixelCount; i++) {
  12.     const offset = i << 2;
  13.     const r = data[offset];
  14.     const g = data[offset + 1];
  15.     const b = data[offset + 2];
  16.    
  17.     // 使用查找表计算灰度值
  18.     const gray = (
  19.       grayLookupTable[tableOffset + r] +
  20.       grayLookupTable[tableOffset + g + 1] +
  21.       grayLookupTable[tableOffset + b + 2]
  22.     ) | 0;
  23.    
  24.     // 一次性设置 RGB 值(保持 Alpha 不变)
  25.     pixels[i] = (data[offset + 3] << 24) | // Alpha
  26.                 (gray << 16) |             // Red
  27.                 (gray << 8) |              // Green
  28.                 gray;                      // Blue
  29.   }
  30.   
  31.   return imageData;
  32. }
复制代码
亮度和对比度调整

最后便是对于亮度和对比度的调整。adjustBrightnessContrast函数应用了预计算的亮度和对比度查找表。具体步骤如下:
1.计算调整因子:利用对比度因子公式来计算处理惩罚图像的对比度。
2.亮度调整:根据传入的亮度值调整每个像素的亮度。
3.对比度调整:根据对比度公式对每个像素举行调整。
4.限制范围:确保调整后的像素值在0到255的有效范围内。
5.批量更新像素值:利用unit32Array加速图像数据更新。
  1. // 优化的亮度和对比度处理
  2. function adjustBrightnessContrast(imageData, brightness, contrast) {
  3.   const data = imageData.data;
  4.   const len = data.length;
  5.   const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
  6.   
  7.   // 预计算亮度和对比度查找表
  8.   const lookupTable = new Uint8Array(256);
  9.   for (let i = 0; i < 256; i++) {
  10.     let value = i;
  11.     // 应用亮度
  12.     value += brightness;
  13.     // 应用对比度
  14.     value = factor * (value - 128) + 128;
  15.     // 限制在有效范围内
  16.     lookupTable[i] = Math.max(0, Math.min(255, value)) | 0;
  17.   }
  18.   
  19.   // 使用 Uint32Array 视图加速访问
  20.   const pixels = new Uint32Array(data.buffer);
  21.   const pixelCount = len >> 2;
  22.   
  23.   for (let i = 0; i < pixelCount; i++) {
  24.     const offset = i << 2;
  25.     const r = lookupTable[data[offset]];
  26.     const g = lookupTable[data[offset + 1]];
  27.     const b = lookupTable[data[offset + 2]];
  28.    
  29.     pixels[i] = (data[offset + 3] << 24) | // Alpha
  30.                 (r << 16) |                // Red
  31.                 (g << 8) |                 // Green
  32.                 b;                         // Blue
  33.   }
  34.   
  35.   return imageData;
  36. }
复制代码
Worker与主线程通讯

  1. // 接收主线程消息
  2. self.onmessage = function(e) {
  3.   const { imageData, algorithm, brightness, contrast } = e.data;
  4.   
  5.   // 使用优化后的灰度处理
  6.   let processedData = processGrayscaleSIMD(imageData, algorithm);
  7.   
  8.   // 使用优化后的亮度和对比度处理
  9.   if (brightness !== 0 || contrast !== 0) {
  10.     processedData = adjustBrightnessContrast(processedData, brightness, contrast);
  11.   }
  12.   
  13.   // 返回处理后的数据
  14.   self.postMessage(processedData);
  15. }
复制代码
完备代码



  • 在项目根目次下新建workers文件夹,并增长grayscale.worker.js文件
  1. // 处理惩罚图片转灰度的 Worker// 预计算灰度值查找表
  2. const grayLookupTable = new Uint8Array(256 * 3);
  3. const weights = {
  4.   weighted: [0.2126, 0.7152, 0.0722],
  5.   average: [0.3333, 0.3333, 0.3334],
  6.   luminosity: [0.299, 0.587, 0.114]
  7. };
  8. // 初始化查找表
  9. function initLookupTables() {
  10.   Object.keys(weights).forEach(method => {
  11.     const [r, g, b] = weights[method];
  12.     for (let i = 0; i < 256; i++) {
  13.       grayLookupTable[i * 3] = i * r;
  14.       grayLookupTable[i * 3 + 1] = i * g;
  15.       grayLookupTable[i * 3 + 2] = i * b;
  16.     }
  17.   });
  18. }
  19. initLookupTables();
  20. // 使用 SIMD 优化的灰度处理(如果浏览器支持)
  21. function processGrayscaleSIMD(imageData, algorithm = 'weighted') {
  22.   const data = imageData.data;
  23.   const len = data.length;
  24.   const [r, g, b] = weights[algorithm];
  25.   
  26.   // 使用 SIMD 优化
  27.   if (typeof SIMD !== 'undefined' && SIMD.Float32x4) {
  28.     const rWeight = SIMD.Float32x4.splat(r);
  29.     const gWeight = SIMD.Float32x4.splat(g);
  30.     const bWeight = SIMD.Float32x4.splat(b);
  31.    
  32.     for (let i = 0; i < len; i += 16) {
  33.       const rgba0 = SIMD.Float32x4.load(data, i);
  34.       const rgba1 = SIMD.Float32x4.load(data, i + 4);
  35.       const rgba2 = SIMD.Float32x4.load(data, i + 8);
  36.       const rgba3 = SIMD.Float32x4.load(data, i + 12);
  37.       
  38.       const gray0 = SIMD.Float32x4.add(
  39.         SIMD.Float32x4.mul(rgba0, rWeight),
  40.         SIMD.Float32x4.add(
  41.           SIMD.Float32x4.mul(rgba1, gWeight),
  42.           SIMD.Float32x4.mul(rgba2, bWeight)
  43.         )
  44.       );
  45.       
  46.       SIMD.Float32x4.store(data, i, gray0);
  47.       SIMD.Float32x4.store(data, i + 4, gray0);
  48.       SIMD.Float32x4.store(data, i + 8, gray0);
  49.     }
  50.     return imageData;
  51.   }
  52.   
  53.   return processGrayscaleStandard(imageData, algorithm);
  54. }
  55. // 标准灰度处理(使用查找表优化)
  56. function processGrayscaleStandard(imageData, algorithm = 'weighted') {
  57.   const data = imageData.data;
  58.   const len = data.length;
  59.   const tableOffset = algorithm === 'weighted' ? 0 : (algorithm === 'average' ? 256 : 512);
  60.   
  61.   // 使用 Uint32Array 视图加速访问
  62.   const pixels = new Uint32Array(data.buffer);
  63.   const pixelCount = len >> 2;
  64.   
  65.   for (let i = 0; i < pixelCount; i++) {
  66.     const offset = i << 2;
  67.     const r = data[offset];
  68.     const g = data[offset + 1];
  69.     const b = data[offset + 2];
  70.    
  71.     // 使用查找表计算灰度值
  72.     const gray = (
  73.       grayLookupTable[tableOffset + r] +
  74.       grayLookupTable[tableOffset + g + 1] +
  75.       grayLookupTable[tableOffset + b + 2]
  76.     ) | 0;
  77.    
  78.     // 一次性设置 RGB 值(保持 Alpha 不变)
  79.     pixels[i] = (data[offset + 3] << 24) | // Alpha
  80.                 (gray << 16) |             // Red
  81.                 (gray << 8) |              // Green
  82.                 gray;                      // Blue
  83.   }
  84.   
  85.   return imageData;
  86. }
  87. // 优化的亮度和对比度处理
  88. function adjustBrightnessContrast(imageData, brightness, contrast) {
  89.   const data = imageData.data;
  90.   const len = data.length;
  91.   const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
  92.   
  93.   // 预计算亮度和对比度查找表
  94.   const lookupTable = new Uint8Array(256);
  95.   for (let i = 0; i < 256; i++) {
  96.     let value = i;
  97.     // 应用亮度
  98.     value += brightness;
  99.     // 应用对比度
  100.     value = factor * (value - 128) + 128;
  101.     // 限制在有效范围内
  102.     lookupTable[i] = Math.max(0, Math.min(255, value)) | 0;
  103.   }
  104.   
  105.   // 使用 Uint32Array 视图加速访问
  106.   const pixels = new Uint32Array(data.buffer);
  107.   const pixelCount = len >> 2;
  108.   
  109.   for (let i = 0; i < pixelCount; i++) {
  110.     const offset = i << 2;
  111.     const r = lookupTable[data[offset]];
  112.     const g = lookupTable[data[offset + 1]];
  113.     const b = lookupTable[data[offset + 2]];
  114.    
  115.     pixels[i] = (data[offset + 3] << 24) | // Alpha
  116.                 (r << 16) |                // Red
  117.                 (g << 8) |                 // Green
  118.                 b;                         // Blue
  119.   }
  120.   
  121.   return imageData;
  122. }
  123. // 接收主线程消息
  124. self.onmessage = function(e) {
  125.   const { imageData, algorithm, brightness, contrast } = e.data;
  126.   
  127.   // 使用优化后的灰度处理
  128.   let processedData = processGrayscaleSIMD(imageData, algorithm);
  129.   
  130.   // 使用优化后的亮度和对比度处理
  131.   if (brightness !== 0 || contrast !== 0) {
  132.     processedData = adjustBrightnessContrast(processedData, brightness, contrast);
  133.   }
  134.   
  135.   // 返回处理后的数据
  136.   self.postMessage(processedData);
  137. }
复制代码


  • 新建vue组件,并引用worker文件,绘制转灰度组件的UI
  1. <template>
  2.   <div class="app-container">
  3.     <header class="app-header">
  4.       <h1>图片转灰度</h1>
  5.       <p class="subtitle">专业的图片灰度处理工具,支持多种算法</p>
  6.     </header>
  7.     <main class="main-content">
  8.       <!-- 添加标签页 -->
  9.       <el-tabs v-model="activeTab" class="image-tabs">
  10.         <el-tab-pane label="单张处理" name="single">
  11.           <!-- 单张图片处理区域 -->
  12.           <div class="upload-section" v-if="!singleImage">
  13.             <el-upload
  14.               class="upload-drop-zone"
  15.               drag
  16.               :auto-upload="false"
  17.               accept="image/*"
  18.               :show-file-list="false"
  19.               :multiple="false"
  20.               @change="handleSingleFileChange"
  21.             >
  22.               <el-icon class="upload-icon"><upload-filled /></el-icon>
  23.               <div class="upload-text">
  24.                 <h3>将图片拖到此处,或点击上传</h3>
  25.                 <p>支持 PNG、JPG、WebP 等常见格式</p>
  26.               </div>
  27.             </el-upload>
  28.           </div>
  29.           <div v-else class="process-section single-mode">
  30.             <div class="image-comparison">
  31.               <!-- 原图预览 -->
  32.               <div class="image-preview original">
  33.                 <h3>原图</h3>
  34.                 <div class="image-container">
  35.                   <img :src="singleImage.originalPreview" :alt="singleImage.file.name" />
  36.                 </div>
  37.               </div>
  38.               <!-- 灰度图预览 -->
  39.               <div class="image-preview processed">
  40.                 <h3>灰度效果</h3>
  41.                 <div class="image-container">
  42.                   <img
  43.                     v-if="singleImage.processedPreview"
  44.                     :src="singleImage.processedPreview"
  45.                     :alt="singleImage.file.name + '(灰度)'"
  46.                   />
  47.                   <div v-else class="placeholder">
  48.                     <el-icon><picture-rounded /></el-icon>
  49.                     <span>待处理</span>
  50.                   </div>
  51.                 </div>
  52.               </div>
  53.             </div>
  54.           </div>
  55.         </el-tab-pane>
  56.         <el-tab-pane label="批量处理" name="batch">
  57.           <!-- 批量处理区域 -->
  58.           <div class="upload-section" v-if="!images.length">
  59.             <el-upload
  60.               class="upload-drop-zone"
  61.               drag
  62.               :auto-upload="false"
  63.               accept="image/*"
  64.               :show-file-list="false"
  65.               :multiple="true"
  66.               @change="handleBatchFileChange"
  67.             >
  68.               <el-icon class="upload-icon"><upload-filled /></el-icon>
  69.               <div class="upload-text">
  70.                 <h3>将图片拖到此处,或点击上传</h3>
  71.                 <p>支持批量上传多张图片,PNG、JPG、WebP 等常见格式</p>
  72.               </div>
  73.             </el-upload>
  74.           </div>
  75.           <div v-else class="process-section batch-mode">
  76.             <!-- 图片列表 -->
  77.             <div class="images-list">
  78.               <el-scrollbar height="600px">
  79.                 <div v-for="(image, index) in images" :key="index" class="image-item">
  80.                   <div class="image-comparison">
  81.                     <!-- 原图预览 -->
  82.                     <div class="image-preview original">
  83.                       <h3>原图</h3>
  84.                       <div class="image-container">
  85.                         <img :src="image.originalPreview" :alt="image.file.name" />
  86.                       </div>
  87.                     </div>
  88.                     <!-- 灰度图预览 -->
  89.                     <div class="image-preview processed">
  90.                       <h3>灰度效果</h3>
  91.                       <div class="image-container">
  92.                         <img
  93.                           v-if="image.processedPreview"
  94.                           :src="image.processedPreview"
  95.                           :alt="image.file.name + '(灰度)'"
  96.                         />
  97.                         <div v-else class="placeholder">
  98.                           <el-icon><picture-rounded /></el-icon>
  99.                           <span>待处理</span>
  100.                         </div>
  101.                       </div>
  102.                     </div>
  103.                   </div>
  104.                   
  105.                   <!-- 单张图片的转换按钮 -->
  106.                   <!-- <div class="image-actions">
  107.                     <el-button
  108.                       type="primary"
  109.                       @click="processSingleImageInBatch(image)"
  110.                       :loading="image.processing"
  111.                       :disabled="!!image.processedPreview"
  112.                     >
  113.                       {{ image.processing ? '转换中...' : '开始转换' }}
  114.                       转换此图
  115.                     </el-button>
  116.                   </div> -->
  117.                 </div>
  118.               </el-scrollbar>
  119.             </div>
  120.           </div>
  121.         </el-tab-pane>
  122.       </el-tabs>
  123.       <!-- 控制面板 -->
  124.       <div class="control-panel">
  125.         <el-form :model="settings" label-position="top">
  126.           <!-- 算法选择 -->
  127.           <el-form-item label="灰度算法">
  128.             <el-select v-model="settings.algorithm" class="algorithm-select">
  129.               <el-option
  130.                 v-for="algo in algorithms"
  131.                 :key="algo.value"
  132.                 :label="algo.label"
  133.                 :value="algo.value"
  134.               />
  135.             </el-select>
  136.           </el-form-item>
  137.           <!-- 亮度调节 -->
  138.           <el-form-item label="亮度调节">
  139.             <el-slider
  140.               v-model="settings.brightness"
  141.               :min="-100"
  142.               :max="100"
  143.               :format-tooltip="(val) => `${val > 0 ? '+' : ''}${val}%`"
  144.             />
  145.           </el-form-item>
  146.           <!-- 对比度调节 -->
  147.           <el-form-item label="对比度">
  148.             <el-slider
  149.               v-model="settings.contrast"
  150.               :min="-100"
  151.               :max="100"
  152.               :format-tooltip="(val) => `${val > 0 ? '+' : ''}${val}%`"
  153.             />
  154.           </el-form-item>
  155.         </el-form>
  156.         <!-- 操作按钮 -->
  157.         <div class="action-buttons">
  158.           <template v-if="activeTab === 'single'">
  159.             <el-button
  160.               type="primary"
  161.               @click="processSingleImage"
  162.               :loading="processing"
  163.               :disabled="!singleImage"
  164.             >
  165.               {{ processing ? '转换中...' : '开始转换' }}
  166.             </el-button>
  167.             <el-button @click="resetSingleImage">重新选择</el-button>
  168.             <el-button
  169.               type="success"
  170.               @click="downloadSingleImage"
  171.               :disabled="!singleImage?.processedPreview"
  172.             >
  173.               下载图片
  174.             </el-button>
  175.           </template>
  176.           <template v-else>
  177.             <el-button
  178.               type="primary"
  179.               @click="processAllImages"
  180.               :loading="batchProcessing"
  181.               :disabled="!hasUnprocessedImages"
  182.             >
  183.               {{ batchProcessing ? '批量转换中...' : '批量转换' }}
  184.             </el-button>
  185.             <el-button @click="resetImages">重新选择</el-button>
  186.             <el-button
  187.               type="success"
  188.               @click="downloadAllImages"
  189.               :disabled="!hasProcessedImages"
  190.             >
  191.               打包下载
  192.             </el-button>
  193.           </template>
  194.         </div>
  195.       </div>
  196.     </main>
  197.   </div>
  198. </template>
  199. <script setup>
  200. import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
  201. import { ElMessage } from 'element-plus'
  202. import { UploadFilled, PictureRounded } from '@element-plus/icons-vue'
  203. // Web Worker 实例
  204. let worker = null
  205. // 状态变量
  206. const images = ref([])
  207. const processing = ref(false)
  208. // 灰度算法选项
  209. const algorithms = [
  210.   { label: '加权平均法(推荐)', value: 'weighted' },
  211.   { label: '平均值法', value: 'average' },
  212.   { label: '亮度法', value: 'luminosity' }
  213. ]
  214. // 处理设置
  215. const settings = ref({
  216.   algorithm: 'weighted',
  217.   brightness: 0,
  218.   contrast: 0
  219. })
  220. // 计算属性:是否有已处理的图片
  221. const hasProcessedImages = computed(() => {
  222.   return images.value.some(img => img.processedPreview)
  223. })
  224. // 新增状态变量
  225. const activeTab = ref('single')
  226. const singleImage = ref(null)
  227. // 添加批量处理状态
  228. const batchProcessing = ref(false)
  229. // 计算是否有未处理的图片
  230. const hasUnprocessedImages = computed(() => {
  231.   return images.value.some(img => !img.processedPreview)
  232. })
  233. // 初始化 Web Worker
  234. onMounted(() => {
  235.   worker = new Worker(new URL('@/workers/grayscale.worker.js', import.meta.url))
  236.   
  237.   worker.onmessage = (e) => {
  238.     const canvas = document.createElement('canvas')
  239.     const ctx = canvas.getContext('2d')
  240.    
  241.     canvas.width = e.data.width
  242.     canvas.height = e.data.height
  243.     ctx.putImageData(e.data, 0, 0)
  244.    
  245.     processedPreview.value = canvas.toDataURL('image/png')
  246.     processing.value = false
  247.    
  248.     ElMessage.success('转换完成')
  249.   }
  250.   
  251.   worker.onerror = (error) => {
  252.     processing.value = false
  253.     ElMessage.error('处理失败:' + error.message)
  254.     console.error(error)
  255.   }
  256. })
  257. // 清理 Worker
  258. onUnmounted(() => {
  259.   resetSingleImage()
  260.   resetImages()
  261.   if (worker) {
  262.     worker.terminate()
  263.   }
  264. })
  265. // 处理单张文件上传
  266. const handleSingleFileChange = (file) => {
  267.   const fileObj = file.raw
  268.   if (!fileObj || !fileObj.type.startsWith('image/')) {
  269.     ElMessage.error('请上传图片文件')
  270.     return
  271.   }
  272.   
  273.   singleImage.value = {
  274.     file: fileObj,
  275.     originalPreview: URL.createObjectURL(fileObj),
  276.     processedPreview: ''
  277.   }
  278. }
  279. // 处理批量文件上传
  280. const handleBatchFileChange = (file) => {
  281.   const files = Array.isArray(file) ? file : [file]
  282.   
  283.   files.forEach(f => {
  284.     const fileObj = f.raw
  285.     if (!fileObj || !fileObj.type.startsWith('image/')) {
  286.       ElMessage.error(`${fileObj.name} 不是有效的图片文件`)
  287.       return
  288.     }
  289.    
  290.     images.value.push({
  291.       file: fileObj,
  292.       originalPreview: URL.createObjectURL(fileObj),
  293.       processedPreview: '',
  294.       processing: false
  295.     })
  296.   })
  297. }
  298. // 处理单张图片(单图模式)
  299. const processSingleImage = async () => {
  300.   if (!singleImage.value || processing.value) return
  301.   
  302.   processing.value = true
  303.   try {
  304.     await processImage(singleImage.value)
  305.     ElMessage.success('转换完成')
  306.   } catch (error) {
  307.     ElMessage.error('处理失败,请重试')
  308.     console.error(error)
  309.   } finally {
  310.     processing.value = false
  311.   }
  312. }
  313. // 处理批量模式中的单张图片
  314. const processSingleImageInBatch = async (image) => {
  315.   if (image.processing || image.processedPreview) return
  316.   
  317.   image.processing = true
  318.   try {
  319.     await processImage(image)
  320.     ElMessage.success('转换完成')
  321.   } catch (error) {
  322.     ElMessage.error('处理失败,请重试')
  323.     console.error(error)
  324.   } finally {
  325.     image.processing = false
  326.   }
  327. }
  328. // 重置单图状态
  329. const resetSingleImage = () => {
  330.   if (singleImage.value) {
  331.     URL.revokeObjectURL(singleImage.value.originalPreview)
  332.   }
  333.   singleImage.value = null
  334. }
  335. // 下载单张处理后的图片
  336. const downloadSingleImage = () => {
  337.   if (!singleImage.value?.processedPreview) return
  338.   
  339.   const link = document.createElement('a')
  340.   const fileName = singleImage.value.file.name.split('.')[0]
  341.   link.download = `${fileName}_grayscale.png`
  342.   link.href = singleImage.value.processedPreview
  343.   link.click()
  344. }
  345. // 处理文件上传
  346. const handleFileChange = (file) => {
  347.   const files = Array.isArray(file) ? file : [file]
  348.   
  349.   files.forEach(f => {
  350.     const fileObj = f.raw
  351.     if (!fileObj || !fileObj.type.startsWith('image/')) {
  352.       ElMessage.error(`${fileObj.name} 不是有效的图片文件`)
  353.       return
  354.     }
  355.    
  356.     images.value.push({
  357.       file: fileObj,
  358.       originalPreview: URL.createObjectURL(fileObj),
  359.       processedPreview: ''
  360.     })
  361.   })
  362. }
  363. // 批量处理图片
  364. const processAllImages = async () => {
  365.   if (!images.value.length || batchProcessing.value) return
  366.   
  367.   batchProcessing.value = true
  368.   
  369.   try {
  370.     const unprocessedImages = images.value.filter(img => !img.processedPreview)
  371.     for (const image of unprocessedImages) {
  372.       image.processing = true
  373.       await processImage(image)
  374.       image.processing = false
  375.     }
  376.     ElMessage.success('所有图片处理完成')
  377.   } catch (error) {
  378.     ElMessage.error('处理过程中出现错误')
  379.     console.error(error)
  380.   } finally {
  381.     batchProcessing.value = false
  382.   }
  383. }
  384. // 处理单张图片
  385. const processImage = (image) => {
  386.   return new Promise((resolve, reject) => {
  387.     const canvas = document.createElement('canvas')
  388.     const ctx = canvas.getContext('2d')
  389.     const img = new Image()
  390.    
  391.     img.onload = () => {
  392.       canvas.width = img.width
  393.       canvas.height = img.height
  394.       ctx.drawImage(img, 0, 0)
  395.       
  396.       const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  397.       
  398.       worker.onmessage = (e) => {
  399.         const resultCanvas = document.createElement('canvas')
  400.         const resultCtx = resultCanvas.getContext('2d')
  401.         
  402.         resultCanvas.width = e.data.width
  403.         resultCanvas.height = e.data.height
  404.         resultCtx.putImageData(e.data, 0, 0)
  405.         
  406.         image.processedPreview = resultCanvas.toDataURL('image/png')
  407.         resolve()
  408.       }
  409.       console.log('imageData',imageData);
  410.       
  411.       
  412.       worker.postMessage({
  413.         imageData,
  414.         algorithm: settings.value.algorithm,
  415.         brightness: settings.value.brightness,
  416.         contrast: settings.value.contrast
  417.       })
  418.     }
  419.    
  420.     img.onerror = reject
  421.     img.src = image.originalPreview
  422.   })
  423. }
  424. // 打包下载所有处理后的图片
  425. const downloadAllImages = async () => {
  426.   try {
  427.     const JSZip = (await import('jszip')).default
  428.     const zip = new JSZip()
  429.    
  430.     images.value.forEach((image, index) => {
  431.       if (image.processedPreview) {
  432.         const base64Data = image.processedPreview.split(',')[1]
  433.         const fileName = `${image.file.name.split('.')[0]}_grayscale.png`
  434.         zip.file(fileName, base64Data, { base64: true })
  435.       }
  436.     })
  437.    
  438.     const content = await zip.generateAsync({ type: 'blob' })
  439.     const link = document.createElement('a')
  440.     link.href = URL.createObjectURL(content)
  441.     link.download = 'grayscale_images.zip'
  442.     link.click()
  443.    
  444.     ElMessage.success('打包下载开始')
  445.   } catch (error) {
  446.     ElMessage.error('下载失败')
  447.     console.error(error)
  448.   }
  449. }
  450. // 重置状态
  451. const resetImages = () => {
  452.   images.value.forEach(image => {
  453.     URL.revokeObjectURL(image.originalPreview)
  454.   })
  455.   images.value = []
  456.   settings.value = {
  457.     algorithm: 'weighted',
  458.     brightness: 0,
  459.     contrast: 0
  460.   }
  461. }
  462. // 添加对设置变化的监听,自动重新处理图片
  463. watch(
  464.   () => settings.value,
  465.   () => {
  466.     if (images.value.length && hasProcessedImages.value) {
  467.       processAllImages()
  468.     }
  469.   },
  470.   { deep: true }
  471. )
  472. </script>
  473. <style scoped>
  474. .app-container {
  475.   max-width: 1200px;
  476.   margin: 0 auto;
  477.   padding: 2rem;
  478. }
  479. .app-header {
  480.   text-align: center;
  481.   margin-bottom: 3rem;
  482. }
  483. .app-header h1 {
  484.   font-size: 2.5rem;
  485.   font-weight: 600;
  486.   color: var(--el-text-color-primary);
  487.   margin-bottom: 0.5rem;
  488. }
  489. .subtitle {
  490.   color: var(--el-text-color-secondary);
  491.   font-size: 1.1rem;
  492. }
  493. .upload-section {
  494.   background: var(--el-bg-color);
  495.   border-radius: 12px;
  496.   padding: 2rem;
  497.   box-shadow: var(--el-box-shadow-light);
  498. }
  499. .upload-drop-zone {
  500.   border: 2px dashed var(--el-border-color);
  501.   border-radius: 8px;
  502.   padding: 3rem 1rem;
  503.   transition: all 0.3s ease;
  504. }
  505. .upload-drop-zone:hover {
  506.   border-color: var(--el-color-primary);
  507.   background: rgba(64, 158, 255, 0.04);
  508. }
  509. .upload-icon {
  510.   font-size: 3rem;
  511.   color: var(--el-text-color-secondary);
  512.   margin-bottom: 1rem;
  513. }
  514. .process-section {
  515.   display: grid;
  516.   grid-template-columns: 1fr;
  517.   gap: 2rem;
  518.   margin-top: 2rem;
  519. }
  520. .image-comparison {
  521.   display: grid;
  522.   grid-template-columns: 1fr 1fr;
  523.   gap: 1rem;
  524.   margin-bottom: 1rem;
  525. }
  526. .image-preview {
  527.   background: var(--el-bg-color);
  528.   border-radius: 12px;
  529.   padding: 1rem;
  530.   box-shadow: var(--el-box-shadow-light);
  531. }
  532. .image-preview h3 {
  533.   text-align: center;
  534.   margin-bottom: 1rem;
  535.   color: var(--el-text-color-primary);
  536. }
  537. .image-container {
  538.   aspect-ratio: 16/9;
  539.   display: flex;
  540.   align-items: center;
  541.   justify-content: center;
  542.   background: var(--el-fill-color-lighter);
  543.   border-radius: 8px;
  544.   overflow: hidden;
  545. }
  546. .image-container img {
  547.   max-width: 100%;
  548.   max-height: 100%;
  549.   object-fit: contain;
  550. }
  551. .placeholder {
  552.   display: flex;
  553.   flex-direction: column;
  554.   align-items: center;
  555.   gap: 0.5rem;
  556.   color: var(--el-text-color-secondary);
  557. }
  558. .control-panel {
  559.   background: var(--el-bg-color);
  560.   border-radius: 12px;
  561.   padding: 2rem;
  562.   box-shadow: var(--el-box-shadow-light);
  563.   margin-top: 2rem;
  564. }
  565. .algorithm-select {
  566.   width: 100%;
  567. }
  568. .action-buttons {
  569.   display: flex;
  570.   gap: 1rem;
  571.   justify-content: center;
  572.   margin-top: 2rem;
  573.   padding-top: 1rem;
  574.   border-top: 1px solid var(--el-border-color-lighter);
  575. }
  576. .action-buttons .el-button {
  577.   min-width: 100px;
  578. }
  579. /* :deep(.el-button--primary) {
  580.   background: var(--el-color-primary-gradient);
  581.   border: none;
  582. } */
  583. .images-list {
  584.   width: 100%;
  585. }
  586. .image-item {
  587.   margin-bottom: 2rem;
  588.   padding-bottom: 2rem;
  589.   border-bottom: 1px solid var(--el-border-color-lighter);
  590. }
  591. .image-item:last-child {
  592.   margin-bottom: 0;
  593.   padding-bottom: 0;
  594.   border-bottom: none;
  595. }
  596. /* 新增样式 */
  597. .image-tabs {
  598.   margin-bottom: 2rem;
  599. }
  600. .single-mode {
  601.   display: flex;
  602.   flex-direction: column;
  603.   gap: 2rem;
  604. }
  605. .batch-mode {
  606.   display: flex;
  607.   flex-direction: column;
  608.   gap: 2rem;
  609. }
  610. :deep(.el-tabs__nav-wrap::after) {
  611.   height: 1px;
  612.   background-color: var(--el-border-color-lighter);
  613. }
  614. :deep(.el-tabs__item) {
  615.   font-size: 1.1rem;
  616.   padding: 0 2rem;
  617. }
  618. :deep(.el-tabs__item.is-active) {
  619.   font-weight: 600;
  620. }
  621. .main-content {
  622.   max-width: 1200px;
  623.   margin: 0 auto;
  624. }
  625. :deep(.el-tabs__nav) {
  626.   margin-bottom: 1rem;
  627. }
  628. </style>
复制代码
结果截图




   转眼间2024年已经已往了,从2024年3月份开始写下第一篇博客开始,到如今也写了90篇博客了。在接下来的一年里,我会继续保持着分享技能、记录成长的习惯,也会在后面逐步加入一些更加深入的技能总结(努力每个月更新一篇干货吧)。
感谢阅读!盼望大家一起在IT之路上一路狂奔~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

我可以不吃啊

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表