当前位置: 首页 > article >正文

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

开篇

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

原理与作用

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

功能实现逻辑

考虑到图片处理时需要计算的数据量较大,为了避免阻塞主线程的渲染,我采用了WebWorker的方式在后台线程中执行图像处理操作。
下面将简单接受一下webworker文件中,主要的功能的实现逻辑代码。

灰度值查找表预计算

为了优化灰度化处理性能,这里首先创建了一个grayLookupTable查找表。这张表预计算了每个RGB值对应的灰度值(按照不同的算法),这样可以在实际处理时直接查找对应的值,避免重复计算。

// 预计算灰度值查找表
const grayLookupTable = new Uint8Array(256 * 3);
const weights = {
  weighted: [0.2126, 0.7152, 0.0722],
  average: [0.3333, 0.3333, 0.3334],
  luminosity: [0.299, 0.587, 0.114]
};

// 初始化查找表
function initLookupTables() {
  Object.keys(weights).forEach(method => {
    const [r, g, b] = weights[method];
    for (let i = 0; i < 256; i++) {
      grayLookupTable[i * 3] = i * r;
      grayLookupTable[i * 3 + 1] = i * g;
      grayLookupTable[i * 3 + 2] = i * b;
    }
  });
}

initLookupTables();

SIMD优化的灰度处理

在浏览器支持SIMD(单指令多数据)时,使用SMD加速了灰度计算。SIMD允许一次操作多个数据元素,因此可以显著提高性能。
processGrayscaleSIMD方法使用SIMD操作来并行处理图像中的多个像素。具体步骤为:
1.加载权重:将算法选择的RGB权重通过SIMD.Float32x4.splat()方法加载到向量中。
2.处理图像数据:使用SIMD.Float32x4来加载多个像素的数据(每4个像素),进行加权计算,最终得到灰度值。
3.保存结果:将计算后的灰度值存回到图像数据中。
如果浏览器不支持SIMD,则回退到标准的灰度处理方法(见下文)。

// 使用 SIMD 优化的灰度处理(如果浏览器支持)
function processGrayscaleSIMD(imageData, algorithm = 'weighted') {
  const data = imageData.data;
  const len = data.length;
  const [r, g, b] = weights[algorithm];
  
  // 使用 SIMD 优化
  if (typeof SIMD !== 'undefined' && SIMD.Float32x4) {
    const rWeight = SIMD.Float32x4.splat(r);
    const gWeight = SIMD.Float32x4.splat(g);
    const bWeight = SIMD.Float32x4.splat(b);
    
    for (let i = 0; i < len; i += 16) {
      const rgba0 = SIMD.Float32x4.load(data, i);
      const rgba1 = SIMD.Float32x4.load(data, i + 4);
      const rgba2 = SIMD.Float32x4.load(data, i + 8);
      const rgba3 = SIMD.Float32x4.load(data, i + 12);
      
      const gray0 = SIMD.Float32x4.add(
        SIMD.Float32x4.mul(rgba0, rWeight),
        SIMD.Float32x4.add(
          SIMD.Float32x4.mul(rgba1, gWeight),
          SIMD.Float32x4.mul(rgba2, bWeight)
        )
      );
      
      SIMD.Float32x4.store(data, i, gray0);
      SIMD.Float32x4.store(data, i + 4, gray0);
      SIMD.Float32x4.store(data, i + 8, gray0);
    }
    return imageData;
  }
  
  return processGrayscaleStandard(imageData, algorithm);
}

标准灰度处理(查找表优化)

本方法为标准灰度处理方法,通过查找表加速了每个像素的灰度值计算。具体步骤如下:
1.查找表选择:根据所选的算法(如加权法、平均值法等),确定使用的查找表的偏移量。
2.遍历图像数据中的每个像素,提取RGB值。
3.查找灰度值:使用查找表中的预计算结果来获得每个像素的灰度值。
4.更新像素值:将计算得到的灰度值赋给RGB通道,保持好Alpha通道不变。

// 标准灰度处理(使用查找表优化)
function processGrayscaleStandard(imageData, algorithm = 'weighted') {
  const data = imageData.data;
  const len = data.length;
  const tableOffset = algorithm === 'weighted' ? 0 : (algorithm === 'average' ? 256 : 512);
  
  // 使用 Uint32Array 视图加速访问
  const pixels = new Uint32Array(data.buffer);
  const pixelCount = len >> 2;
  
  for (let i = 0; i < pixelCount; i++) {
    const offset = i << 2;
    const r = data[offset];
    const g = data[offset + 1];
    const b = data[offset + 2];
    
    // 使用查找表计算灰度值
    const gray = (
      grayLookupTable[tableOffset + r] +
      grayLookupTable[tableOffset + g + 1] +
      grayLookupTable[tableOffset + b + 2]
    ) | 0;
    
    // 一次性设置 RGB 值(保持 Alpha 不变)
    pixels[i] = (data[offset + 3] << 24) | // Alpha
                (gray << 16) |             // Red
                (gray << 8) |              // Green
                gray;                      // Blue
  }
  
  return imageData;
}

亮度和对比度调整

最后便是对于亮度和对比度的调整。adjustBrightnessContrast函数应用了预计算的亮度和对比度查找表。具体步骤如下:
1.计算调整因子:使用对比度因子公式来计算处理图像的对比度。
2.亮度调整:根据传入的亮度值调整每个像素的亮度。
3.对比度调整:根据对比度公式对每个像素进行调整。
4.限制范围:确保调整后的像素值在0到255的有效范围内。
5.批量更新像素值:使用unit32Array加速图像数据更新。

// 优化的亮度和对比度处理
function adjustBrightnessContrast(imageData, brightness, contrast) {
  const data = imageData.data;
  const len = data.length;
  const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
  
  // 预计算亮度和对比度查找表
  const lookupTable = new Uint8Array(256);
  for (let i = 0; i < 256; i++) {
    let value = i;
    // 应用亮度
    value += brightness;
    // 应用对比度
    value = factor * (value - 128) + 128;
    // 限制在有效范围内
    lookupTable[i] = Math.max(0, Math.min(255, value)) | 0;
  }
  
  // 使用 Uint32Array 视图加速访问
  const pixels = new Uint32Array(data.buffer);
  const pixelCount = len >> 2;
  
  for (let i = 0; i < pixelCount; i++) {
    const offset = i << 2;
    const r = lookupTable[data[offset]];
    const g = lookupTable[data[offset + 1]];
    const b = lookupTable[data[offset + 2]];
    
    pixels[i] = (data[offset + 3] << 24) | // Alpha
                (r << 16) |                // Red
                (g << 8) |                 // Green
                b;                         // Blue
  }
  
  return imageData;
}

Worker与主线程通信

// 接收主线程消息
self.onmessage = function(e) {
  const { imageData, algorithm, brightness, contrast } = e.data;
  
  // 使用优化后的灰度处理
  let processedData = processGrayscaleSIMD(imageData, algorithm);
  
  // 使用优化后的亮度和对比度处理
  if (brightness !== 0 || contrast !== 0) {
    processedData = adjustBrightnessContrast(processedData, brightness, contrast);
  }
  
  // 返回处理后的数据
  self.postMessage(processedData);
} 

完整代码

  • 在项目根目录下新建workers文件夹,并增加grayscale.worker.js文件
// 处理图片转灰度的 Worker

// 预计算灰度值查找表
const grayLookupTable = new Uint8Array(256 * 3);
const weights = {
  weighted: [0.2126, 0.7152, 0.0722],
  average: [0.3333, 0.3333, 0.3334],
  luminosity: [0.299, 0.587, 0.114]
};

// 初始化查找表
function initLookupTables() {
  Object.keys(weights).forEach(method => {
    const [r, g, b] = weights[method];
    for (let i = 0; i < 256; i++) {
      grayLookupTable[i * 3] = i * r;
      grayLookupTable[i * 3 + 1] = i * g;
      grayLookupTable[i * 3 + 2] = i * b;
    }
  });
}

initLookupTables();

// 使用 SIMD 优化的灰度处理(如果浏览器支持)
function processGrayscaleSIMD(imageData, algorithm = 'weighted') {
  const data = imageData.data;
  const len = data.length;
  const [r, g, b] = weights[algorithm];
  
  // 使用 SIMD 优化
  if (typeof SIMD !== 'undefined' && SIMD.Float32x4) {
    const rWeight = SIMD.Float32x4.splat(r);
    const gWeight = SIMD.Float32x4.splat(g);
    const bWeight = SIMD.Float32x4.splat(b);
    
    for (let i = 0; i < len; i += 16) {
      const rgba0 = SIMD.Float32x4.load(data, i);
      const rgba1 = SIMD.Float32x4.load(data, i + 4);
      const rgba2 = SIMD.Float32x4.load(data, i + 8);
      const rgba3 = SIMD.Float32x4.load(data, i + 12);
      
      const gray0 = SIMD.Float32x4.add(
        SIMD.Float32x4.mul(rgba0, rWeight),
        SIMD.Float32x4.add(
          SIMD.Float32x4.mul(rgba1, gWeight),
          SIMD.Float32x4.mul(rgba2, bWeight)
        )
      );
      
      SIMD.Float32x4.store(data, i, gray0);
      SIMD.Float32x4.store(data, i + 4, gray0);
      SIMD.Float32x4.store(data, i + 8, gray0);
    }
    return imageData;
  }
  
  return processGrayscaleStandard(imageData, algorithm);
}

// 标准灰度处理(使用查找表优化)
function processGrayscaleStandard(imageData, algorithm = 'weighted') {
  const data = imageData.data;
  const len = data.length;
  const tableOffset = algorithm === 'weighted' ? 0 : (algorithm === 'average' ? 256 : 512);
  
  // 使用 Uint32Array 视图加速访问
  const pixels = new Uint32Array(data.buffer);
  const pixelCount = len >> 2;
  
  for (let i = 0; i < pixelCount; i++) {
    const offset = i << 2;
    const r = data[offset];
    const g = data[offset + 1];
    const b = data[offset + 2];
    
    // 使用查找表计算灰度值
    const gray = (
      grayLookupTable[tableOffset + r] +
      grayLookupTable[tableOffset + g + 1] +
      grayLookupTable[tableOffset + b + 2]
    ) | 0;
    
    // 一次性设置 RGB 值(保持 Alpha 不变)
    pixels[i] = (data[offset + 3] << 24) | // Alpha
                (gray << 16) |             // Red
                (gray << 8) |              // Green
                gray;                      // Blue
  }
  
  return imageData;
}

// 优化的亮度和对比度处理
function adjustBrightnessContrast(imageData, brightness, contrast) {
  const data = imageData.data;
  const len = data.length;
  const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
  
  // 预计算亮度和对比度查找表
  const lookupTable = new Uint8Array(256);
  for (let i = 0; i < 256; i++) {
    let value = i;
    // 应用亮度
    value += brightness;
    // 应用对比度
    value = factor * (value - 128) + 128;
    // 限制在有效范围内
    lookupTable[i] = Math.max(0, Math.min(255, value)) | 0;
  }
  
  // 使用 Uint32Array 视图加速访问
  const pixels = new Uint32Array(data.buffer);
  const pixelCount = len >> 2;
  
  for (let i = 0; i < pixelCount; i++) {
    const offset = i << 2;
    const r = lookupTable[data[offset]];
    const g = lookupTable[data[offset + 1]];
    const b = lookupTable[data[offset + 2]];
    
    pixels[i] = (data[offset + 3] << 24) | // Alpha
                (r << 16) |                // Red
                (g << 8) |                 // Green
                b;                         // Blue
  }
  
  return imageData;
}

// 接收主线程消息
self.onmessage = function(e) {
  const { imageData, algorithm, brightness, contrast } = e.data;
  
  // 使用优化后的灰度处理
  let processedData = processGrayscaleSIMD(imageData, algorithm);
  
  // 使用优化后的亮度和对比度处理
  if (brightness !== 0 || contrast !== 0) {
    processedData = adjustBrightnessContrast(processedData, brightness, contrast);
  }
  
  // 返回处理后的数据
  self.postMessage(processedData);
} 
  • 新建vue组件,并引用worker文件,绘制转灰度组件的UI
<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"><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 batch-mode">
            <!-- 图片列表 -->
            <div class="images-list">
              <el-scrollbar height="600px">
                <div v-for="(image, index) in images" :key="index" class="image-item">
                  <div class="image-comparison">
                    <!-- 原图预览 -->
                    <div class="image-preview original">
                      <h3>原图</h3>
                      <div class="image-container">
                        <img :src="image.originalPreview" :alt="image.file.name" />
                      </div>
                    </div>

                    <!-- 灰度图预览 -->
                    <div class="image-preview processed">
                      <h3>灰度效果</h3>
                      <div class="image-container">
                        <img 
                          v-if="image.processedPreview" 
                          :src="image.processedPreview" 
                          :alt="image.file.name + '(灰度)'" 
                        />
                        <div v-else class="placeholder">
                          <el-icon><picture-rounded /></el-icon>
                          <span>待处理</span>
                        </div>
                      </div>
                    </div>
                  </div>
                  
                  <!-- 单张图片的转换按钮 -->
                  <!-- <div class="image-actions">
                    <el-button 
                      type="primary"
                      @click="processSingleImageInBatch(image)"
                      :loading="image.processing"
                      :disabled="!!image.processedPreview"
                    >
                      {{ image.processing ? '转换中...' : '开始转换' }}
                      转换此图
                    </el-button>
                  </div> -->
                </div>
              </el-scrollbar>
            </div>
          </div>
        </el-tab-pane>
      </el-tabs>

      <!-- 控制面板 -->
      <div class="control-panel">
        <el-form :model="settings" label-position="top">
          <!-- 算法选择 -->
          <el-form-item label="灰度算法">
            <el-select v-model="settings.algorithm" class="algorithm-select">
              <el-option
                v-for="algo in algorithms"
                :key="algo.value"
                :label="algo.label"
                :value="algo.value"
              />
            </el-select>
          </el-form-item>

          <!-- 亮度调节 -->
          <el-form-item label="亮度调节">
            <el-slider
              v-model="settings.brightness"
              :min="-100"
              :max="100"
              :format-tooltip="(val) => `${val > 0 ? '+' : ''}${val}%`"
            />
          </el-form-item>

          <!-- 对比度调节 -->
          <el-form-item label="对比度">
            <el-slider
              v-model="settings.contrast"
              :min="-100"
              :max="100"
              :format-tooltip="(val) => `${val > 0 ? '+' : ''}${val}%`"
            />
          </el-form-item>
        </el-form>

        <!-- 操作按钮 -->
        <div class="action-buttons">
          <template v-if="activeTab === 'single'">
            <el-button 
              type="primary"
              @click="processSingleImage"
              :loading="processing"
              :disabled="!singleImage"
            >
              {{ processing ? '转换中...' : '开始转换' }}
            </el-button>
            <el-button @click="resetSingleImage">重新选择</el-button>
            <el-button
              type="success"
              @click="downloadSingleImage"
              :disabled="!singleImage?.processedPreview"
            >
              下载图片
            </el-button>
          </template>
          <template v-else>
            <el-button
              type="primary"
              @click="processAllImages"
              :loading="batchProcessing"
              :disabled="!hasUnprocessedImages"
            >
              {{ batchProcessing ? '批量转换中...' : '批量转换' }}
            </el-button>
            <el-button @click="resetImages">重新选择</el-button>
            <el-button
              type="success"
              @click="downloadAllImages"
              :disabled="!hasProcessedImages"
            >
              打包下载
            </el-button>
          </template>
        </div>
      </div>
    </main>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { UploadFilled, PictureRounded } from '@element-plus/icons-vue'

// Web Worker 实例
let worker = null

// 状态变量
const images = ref([])
const processing = ref(false)

// 灰度算法选项
const algorithms = [
  { label: '加权平均法(推荐)', value: 'weighted' },
  { label: '平均值法', value: 'average' },
  { label: '亮度法', value: 'luminosity' }
]

// 处理设置
const settings = ref({
  algorithm: 'weighted',
  brightness: 0,
  contrast: 0
})

// 计算属性:是否有已处理的图片
const hasProcessedImages = computed(() => {
  return images.value.some(img => img.processedPreview)
})

// 新增状态变量
const activeTab = ref('single')
const singleImage = ref(null)

// 添加批量处理状态
const batchProcessing = ref(false)

// 计算是否有未处理的图片
const hasUnprocessedImages = computed(() => {
  return images.value.some(img => !img.processedPreview)
})

// 初始化 Web Worker
onMounted(() => {
  worker = new Worker(new URL('@/workers/grayscale.worker.js', import.meta.url))
  
  worker.onmessage = (e) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    
    canvas.width = e.data.width
    canvas.height = e.data.height
    ctx.putImageData(e.data, 0, 0)
    
    processedPreview.value = canvas.toDataURL('image/png')
    processing.value = false
    
    ElMessage.success('转换完成')
  }
  
  worker.onerror = (error) => {
    processing.value = false
    ElMessage.error('处理失败:' + error.message)
    console.error(error)
  }
})

// 清理 Worker
onUnmounted(() => {
  resetSingleImage()
  resetImages()
  if (worker) {
    worker.terminate()
  }
})

// 处理单张文件上传
const handleSingleFileChange = (file) => {
  const fileObj = file.raw
  if (!fileObj || !fileObj.type.startsWith('image/')) {
    ElMessage.error('请上传图片文件')
    return
  }
  
  singleImage.value = {
    file: fileObj,
    originalPreview: URL.createObjectURL(fileObj),
    processedPreview: ''
  }
}

// 处理批量文件上传
const handleBatchFileChange = (file) => {
  const files = Array.isArray(file) ? file : [file]
  
  files.forEach(f => {
    const fileObj = f.raw
    if (!fileObj || !fileObj.type.startsWith('image/')) {
      ElMessage.error(`${fileObj.name} 不是有效的图片文件`)
      return
    }
    
    images.value.push({
      file: fileObj,
      originalPreview: URL.createObjectURL(fileObj),
      processedPreview: '',
      processing: false
    })
  })
}

// 处理单张图片(单图模式)
const processSingleImage = async () => {
  if (!singleImage.value || processing.value) return
  
  processing.value = true
  try {
    await processImage(singleImage.value)
    ElMessage.success('转换完成')
  } catch (error) {
    ElMessage.error('处理失败,请重试')
    console.error(error)
  } finally {
    processing.value = false
  }
}

// 处理批量模式中的单张图片
const processSingleImageInBatch = async (image) => {
  if (image.processing || image.processedPreview) return
  
  image.processing = true
  try {
    await processImage(image)
    ElMessage.success('转换完成')
  } catch (error) {
    ElMessage.error('处理失败,请重试')
    console.error(error)
  } finally {
    image.processing = false
  }
}

// 重置单图状态
const resetSingleImage = () => {
  if (singleImage.value) {
    URL.revokeObjectURL(singleImage.value.originalPreview)
  }
  singleImage.value = null
}

// 下载单张处理后的图片
const downloadSingleImage = () => {
  if (!singleImage.value?.processedPreview) return
  
  const link = document.createElement('a')
  const fileName = singleImage.value.file.name.split('.')[0]
  link.download = `${fileName}_grayscale.png`
  link.href = singleImage.value.processedPreview
  link.click()
}

// 处理文件上传
const handleFileChange = (file) => {
  const files = Array.isArray(file) ? file : [file]
  
  files.forEach(f => {
    const fileObj = f.raw
    if (!fileObj || !fileObj.type.startsWith('image/')) {
      ElMessage.error(`${fileObj.name} 不是有效的图片文件`)
      return
    }
    
    images.value.push({
      file: fileObj,
      originalPreview: URL.createObjectURL(fileObj),
      processedPreview: ''
    })
  })
}

// 批量处理图片
const processAllImages = async () => {
  if (!images.value.length || batchProcessing.value) return
  
  batchProcessing.value = true
  
  try {
    const unprocessedImages = images.value.filter(img => !img.processedPreview)
    for (const image of unprocessedImages) {
      image.processing = true
      await processImage(image)
      image.processing = false
    }
    ElMessage.success('所有图片处理完成')
  } catch (error) {
    ElMessage.error('处理过程中出现错误')
    console.error(error)
  } finally {
    batchProcessing.value = false
  }
}

// 处理单张图片
const processImage = (image) => {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    const img = new Image()
    
    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.onmessage = (e) => {
        const resultCanvas = document.createElement('canvas')
        const resultCtx = resultCanvas.getContext('2d')
        
        resultCanvas.width = e.data.width
        resultCanvas.height = e.data.height
        resultCtx.putImageData(e.data, 0, 0)
        
        image.processedPreview = resultCanvas.toDataURL('image/png')
        resolve()
      }

      console.log('imageData',imageData);
      
      
      worker.postMessage({
        imageData,
        algorithm: settings.value.algorithm,
        brightness: settings.value.brightness,
        contrast: settings.value.contrast
      })
    }
    
    img.onerror = reject
    img.src = image.originalPreview
  })
}

// 打包下载所有处理后的图片
const downloadAllImages = async () => {
  try {
    const JSZip = (await import('jszip')).default
    const zip = new JSZip()
    
    images.value.forEach((image, index) => {
      if (image.processedPreview) {
        const base64Data = image.processedPreview.split(',')[1]
        const fileName = `${image.file.name.split('.')[0]}_grayscale.png`
        zip.file(fileName, base64Data, { base64: true })
      }
    })
    
    const content = await zip.generateAsync({ type: 'blob' })
    const link = document.createElement('a')
    link.href = URL.createObjectURL(content)
    link.download = 'grayscale_images.zip'
    link.click()
    
    ElMessage.success('打包下载开始')
  } catch (error) {
    ElMessage.error('下载失败')
    console.error(error)
  }
}

// 重置状态
const resetImages = () => {
  images.value.forEach(image => {
    URL.revokeObjectURL(image.originalPreview)
  })
  images.value = []
  settings.value = {
    algorithm: 'weighted',
    brightness: 0,
    contrast: 0
  }
}

// 添加对设置变化的监听,自动重新处理图片
watch(
  () => settings.value,
  () => {
    if (images.value.length && hasProcessedImages.value) {
      processAllImages()
    }
  },
  { deep: true }
)
</script>

<style scoped>
.app-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.app-header {
  text-align: center;
  margin-bottom: 3rem;
}

.app-header h1 {
  font-size: 2.5rem;
  font-weight: 600;
  color: var(--el-text-color-primary);
  margin-bottom: 0.5rem;
}

.subtitle {
  color: var(--el-text-color-secondary);
  font-size: 1.1rem;
}

.upload-section {
  background: var(--el-bg-color);
  border-radius: 12px;
  padding: 2rem;
  box-shadow: var(--el-box-shadow-light);
}

.upload-drop-zone {
  border: 2px dashed var(--el-border-color);
  border-radius: 8px;
  padding: 3rem 1rem;
  transition: all 0.3s ease;
}

.upload-drop-zone:hover {
  border-color: var(--el-color-primary);
  background: rgba(64, 158, 255, 0.04);
}

.upload-icon {
  font-size: 3rem;
  color: var(--el-text-color-secondary);
  margin-bottom: 1rem;
}

.process-section {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  margin-top: 2rem;
}

.image-comparison {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-bottom: 1rem;
}

.image-preview {
  background: var(--el-bg-color);
  border-radius: 12px;
  padding: 1rem;
  box-shadow: var(--el-box-shadow-light);
}

.image-preview h3 {
  text-align: center;
  margin-bottom: 1rem;
  color: var(--el-text-color-primary);
}

.image-container {
  aspect-ratio: 16/9;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--el-fill-color-lighter);
  border-radius: 8px;
  overflow: hidden;
}

.image-container img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  color: var(--el-text-color-secondary);
}

.control-panel {
  background: var(--el-bg-color);
  border-radius: 12px;
  padding: 2rem;
  box-shadow: var(--el-box-shadow-light);
  margin-top: 2rem;
}

.algorithm-select {
  width: 100%;
}

.action-buttons {
  display: flex;
  gap: 1rem;
  justify-content: center;
  margin-top: 2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--el-border-color-lighter);
}

.action-buttons .el-button {
  min-width: 100px;
}

/* :deep(.el-button--primary) {
  background: var(--el-color-primary-gradient);
  border: none;
} */

.images-list {
  width: 100%;
}

.image-item {
  margin-bottom: 2rem;
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--el-border-color-lighter);
}

.image-item:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
}

/* 新增样式 */
.image-tabs {
  margin-bottom: 2rem;
}

.single-mode {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.batch-mode {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

:deep(.el-tabs__nav-wrap::after) {
  height: 1px;
  background-color: var(--el-border-color-lighter);
}

:deep(.el-tabs__item) {
  font-size: 1.1rem;
  padding: 0 2rem;
}

:deep(.el-tabs__item.is-active) {
  font-weight: 600;
}

.main-content {
  max-width: 1200px;
  margin: 0 auto;
}

:deep(.el-tabs__nav) {
  margin-bottom: 1rem;
}
</style>

效果截图

在这里插入图片描述

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


http://www.kler.cn/a/471216.html

相关文章:

  • [python3]Excel解析库-xlwt
  • Android NDK开发实战之环境搭建篇(so库,Gemini ai)
  • HarmonyOS开发:传参方式
  • 【连续学习之LwM算法】2019年CVPR顶会论文:Learning without memorizing
  • 面试高阶问题:对称加密与非对称加密的原理及其应用场景
  • 物联网:七天构建一个闭环的物联网DEMO
  • MongoDB-文章目录
  • R语言的语法糖
  • 深入浅出Node.js-1(node.js入门)
  • 如何提升RAG系统整体效果:从索引构建-问句理解-混合搜索+语义排序着手,评估系统
  • 编排式 Saga 模式
  • WLAN基本原理与配置
  • C++ 数据结构与算法——寻找最大素数因子的整数
  • FPGA实现UART对应的电路和单片机内部配合寄存器实现的电路到底有何区别?
  • Hadoop解决数据倾斜方法
  • git版本管理
  • 电力领域检索增强生成框架
  • 2025最新版Python 3.13.1安装使用指南
  • linux音视频采集技术: v4l2
  • Oracle Dataguard(主库为 RAC 双节点集群)配置详解(1):安装 Oracle11g RAC 双节点集群
  • 在DVWA靶机从渗透到控制(weevely和中国蚁剑)
  • Taro地图组件和小程序定位
  • 十五、Vue 响应接口
  • [大模型开源]SecGPT 网络安全大模型
  • java调用外部API,通过http请求,HttpGet和HttpPost方式实现
  • Elixir语言的正则表达式