图片批量伽马工具中间亮度

联启 电脑工具 17
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">图片批量伽马工具 · 中间亮度</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            background: #1a1a2e;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
            padding: 16px;
        }
        .card {
            max-width: 960px;
            width: 100%;
            background: #16213e;
            border-radius: 32px;
            padding: 28px 24px;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
            color: #e0e0e0;
            transition: 0.2s;
        }
        h1 {
            font-weight: 500;
            font-size: 1.8rem;
            letter-spacing: 0.5px;
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 20px;
            border-left: 6px solid #ffb347;
            padding-left: 18px;
        }
        h1 small {
            font-size: 0.9rem;
            font-weight: 300;
            color: #a0b0c0;
            margin-left: 8px;
        }
        .control-panel {
            background: #1a2740;
            border-radius: 24px;
            padding: 20px 22px;
            margin-bottom: 28px;
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            gap: 18px 10px;
        }
        .gamma-group {
            display: flex;
            align-items: center;
            gap: 16px;
            flex-wrap: wrap;
            flex: 2 1 280px;
        }
        .gamma-group label {
            font-weight: 400;
            color: #b8c8dd;
            letter-spacing: 0.3px;
        }
        .gamma-group input[type="range"] {
            width: 200px;
            height: 6px;
            -webkit-appearance: none;
            background: linear-gradient(90deg, #444c6e, #f7b731);
            border-radius: 10px;
            outline: none;
            transition: background 0.1s;
        }
        .gamma-group input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            background: #ffb347;
            border-radius: 50%;
            cursor: pointer;
            box-shadow: 0 0 0 3px #16213e, 0 2px 8px rgba(0,0,0,0.6);
            transition: 0.07s;
        }
        .gamma-group input[type="range"]::-webkit-slider-thumb:hover {
            transform: scale(1.12);
            background: #ffa726;
        }
        .gamma-value {
            min-width: 48px;
            font-weight: 600;
            font-size: 1.3rem;
            color: #f7d794;
            background: #0f1a2b;
            padding: 4px 12px;
            border-radius: 40px;
            text-align: center;
            letter-spacing: 0.5px;
        }
        .action-group {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            align-items: center;
        }
        .btn {
            background: #1f3a5f;
            border: none;
            padding: 10px 22px;
            border-radius: 60px;
            font-weight: 500;
            color: #eef3fc;
            cursor: pointer;
            transition: 0.15s;
            font-size: 0.95rem;
            display: inline-flex;
            align-items: center;
            gap: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.3);
            letter-spacing: 0.4px;
            border: 1px solid rgba(255,255,255,0.05);
        }
        .btn-primary {
            background: #f39c12;
            color: #141d2b;
            font-weight: 600;
            border: 1px solid #f1b14a;
        }
        .btn-primary:hover {
            background: #f7b731;
            transform: scale(1.02);
            box-shadow: 0 6px 14px rgba(243, 156, 18, 0.3);
        }
        .btn-outline {
            background: transparent;
            border: 1px solid #3f5579;
        }
        .btn-outline:hover {
            background: #253a5a;
            border-color: #7a92b9;
        }
        .btn:active {
            transform: scale(0.96);
        }
        .upload-area {
            background: #101a2b;
            border: 2px dashed #2f4570;
            border-radius: 28px;
            padding: 26px 16px;
            text-align: center;
            transition: 0.2s;
            margin-bottom: 24px;
            cursor: pointer;
        }
        .upload-area:hover {
            border-color: #f7b731;
            background: #121e33;
        }
        .upload-area p {
            font-size: 1rem;
            color: #95a9c9;
        }
        .upload-area .icon {
            font-size: 2.4rem;
            margin-bottom: 6px;
        }
        #fileInput {
            display: none;
        }
        .preview-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
            gap: 16px;
            margin: 18px 0 22px;
            max-height: 400px;
            overflow-y: auto;
            padding: 6px 2px;
        }
        .preview-item {
            background: #0b1423;
            border-radius: 18px;
            padding: 8px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.5);
            transition: 0.1s;
            border: 1px solid #2c3d5e;
            position: relative;
        }
        .preview-item img {
            width: 100%;
            height: 100px;
            object-fit: cover;
            border-radius: 12px;
            display: block;
            background: #1f2a44;
        }
        .preview-item .file-name {
            font-size: 0.7rem;
            color: #a0b5d6;
            padding: 6px 4px 2px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .badge-count {
            background: #1f3a5f;
            border-radius: 60px;
            padding: 2px 14px;
            font-size: 0.85rem;
            color: #c8daf5;
        }
        .status-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 12px;
            background: #0f182a;
            padding: 12px 20px;
            border-radius: 40px;
            margin-top: 10px;
        }
        .status-bar span {
            color: #aac0de;
        }
        .download-all {
            background: #27ae60;
            border: none;
            padding: 12px 28px;
            border-radius: 60px;
            font-weight: 600;
            color: white;
            cursor: pointer;
            transition: 0.15s;
            font-size: 1rem;
            box-shadow: 0 4px 10px rgba(39, 174, 96, 0.2);
        }
        .download-all:hover {
            background: #2ecc71;
            transform: scale(1.02);
        }
        .download-all:disabled {
            opacity: 0.4;
            transform: none;
            cursor: not-allowed;
            background: #1e5a3a;
        }
        .footer-hint {
            display: flex;
            gap: 18px;
            flex-wrap: wrap;
            margin-top: 12px;
            font-size: 0.8rem;
            color: #6b7f9f;
            justify-content: center;
        }
        @media (max-width: 640px) {
            .card { padding: 18px 12px; }
            .gamma-group input[type="range"] { width: 140px; }
            .control-panel { flex-direction: column; align-items: stretch; }
        }
    </style>
</head>
<body>
<div class="card">
    <h1>
        ⚡ 批量伽马
        <small>中间亮度 · 实时预览</small>
    </h1>
    <!-- 控制区 -->
    <div class="control-panel">
        <div class="gamma-group">
            <label for="gammaSlider">伽马 (γ)</label>
            <input type="range" id="gammaSlider" min="0.10" max="2.50" step="0.01" value="1.00">
            <span class="gamma-value" id="gammaDisplay">1.00</span>
        </div>
        <div class="action-group">
            <button class="btn btn-outline" id="resetGammaBtn">↺ 重置</button>
            <button class="btn btn-outline" id="clearBtn">✕ 清空</button>
        </div>
    </div>
    <!-- 上传区域 -->
    <div class="upload-area" id="dropZone">
        <div class="icon">🖼️</div>
        <p><strong>点击选择</strong> 或拖拽图片至此处<br><span style="font-size:0.8rem; opacity:0.7;">支持 JPG / PNG / WebP / BMP</span></p>
        <input type="file" id="fileInput" accept="image/*" multiple>
    </div>
    <!-- 缩略图网格 + 批量下载 -->
    <div id="imageContainer">
        <div style="display: flex; justify-content: space-between; align-items: center; margin: 6px 4px;">
            <span class="badge-count" id="imageCount">0 张</span>
            <span style="color:#6a7fa0; font-size:0.9rem;">👆 点击缩略图可单独下载</span>
        </div>
        <div class="preview-grid" id="previewGrid"></div>
    </div>
    <!-- 底部操作栏 -->
    <div class="status-bar">
        <span>💡 伽马 &lt; 1 提亮中间调,&gt; 1 压暗中间调</span>
        <button class="download-all" id="downloadAllBtn" disabled>📥 批量下载全部</button>
    </div>
    <div class="footer-hint">
        <span>所有处理在浏览器本地完成 · 图片不上传服务器</span>
    </div>
</div>
<script>
    (function() {
        // ---------- DOM 元素 ----------
        const gammaSlider = document.getElementById('gammaSlider');
        const gammaDisplay = document.getElementById('gammaDisplay');
        const resetGammaBtn = document.getElementById('resetGammaBtn');
        const clearBtn = document.getElementById('clearBtn');
        const fileInput = document.getElementById('fileInput');
        const dropZone = document.getElementById('dropZone');
        const previewGrid = document.getElementById('previewGrid');
        const imageCount = document.getElementById('imageCount');
        const downloadAllBtn = document.getElementById('downloadAllBtn');
        // ---------- 状态 ----------
        let imageFiles = [];              // 存储原始 File 对象 (用于重处理)
        let processedBlobs = new Map();   // fileIndex -> Blob (处理后的图片)
        let currentGamma = 1.0;
        // ---------- 辅助函数 ----------
        function updateGammaDisplay(val) {
            gammaDisplay.textContent = parseFloat(val).toFixed(2);
        }
        // 核心:伽马校正 (中间亮度/整体)  公式: out = 255 * (in/255)^(1/gamma)
        function applyGammaToImageData(imageData, gamma) {
            const data = imageData.data;
            const invGamma = 1 / gamma;
            const gammaLUT = new Uint8Array(256);
            for (let i = 0; i < 256; i++) {
                let normalized = i / 255;
                let corrected = Math.pow(normalized, invGamma);
                gammaLUT[i] = Math.min(255, Math.max(0, Math.round(corrected * 255)));
            }
            for (let i = 0; i < data.length; i += 4) {
                data[i]     = gammaLUT[data[i]];     // R
                data[i + 1] = gammaLUT[data[i + 1]]; // G
                data[i + 2] = gammaLUT[data[i + 2]]; // B
                // alpha 保持不变
            }
            return imageData;
        }
        // 处理单个图片 (返回 Blob)
        function processImage(file, gamma) {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.onload = () => {
                    // 使用 canvas 绘制并伽马校正
                    const canvas = document.createElement('canvas');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    const ctx = canvas.getContext('2d', { willReadFrequently: true });
                    if (!ctx) {
                        reject(new Error('无法创建 canvas 上下文'));
                        return;
                    }
                    // 绘制原图
                    ctx.drawImage(img, 0, 0);
                    // 获取图像数据
                    let imageData;
                    try {
                        imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    } catch (e) {
                        reject(new Error('图片跨域或像素错误'));
                        return;
                    }
                    // 应用伽马
                    const correctedData = applyGammaToImageData(imageData, gamma);
                    ctx.putImageData(correctedData, 0, 0);
                    // 导出为 blob (保持原格式或PNG)
                    const mimeType = file.type || 'image/png';
                    // 对于 JPEG / WebP 等可以保持,但 canvas 默认 toBlob 可能降级
                    canvas.toBlob((blob) => {
                        if (blob) {
                            // 保留原始文件名(仅内容变化)
                            blob.name = file.name; 
                            resolve(blob);
                        } else {
                            reject(new Error('Blob 转换失败'));
                        }
                    }, mimeType, 0.92);
                };
                img.onerror = () => reject(new Error('图片加载失败: ' + file.name));
                // 用 URL 读取文件
                const url = URL.createObjectURL(file);
                img.src = url;
            });
        }
        // 重新处理所有图片 (根据当前 gamma)
        async function reprocessAllImages() {
            if (imageFiles.length === 0) {
                processedBlobs.clear();
                renderThumbnails();
                updateUI();
                return;
            }
            const gamma = currentGamma;
            const promises = imageFiles.map((file, index) => 
                processImage(file, gamma).then(blob => ({ index, blob })).catch(err => {
                    console.warn(`处理失败 ${file.name}:`, err);
                    return { index, blob: null };
                })
            );
            const results = await Promise.all(promises);
            // 更新 map
            processedBlobs.clear();
            for (const { index, blob } of results) {
                if (blob) {
                    processedBlobs.set(index, blob);
                } else {
                    // 失败时用原始文件作为兜底? 但保留原图避免空白
                    // 我们可以把原始文件转成blob
                    const file = imageFiles[index];
                    const fallbackBlob = file.slice(0, file.size, file.type);
                    fallbackBlob.name = file.name;
                    processedBlobs.set(index, fallbackBlob);
                }
            }
            renderThumbnails();
            updateUI();
        }
        // 渲染缩略图 (显示处理后图像)
        function renderThumbnails() {
            previewGrid.innerHTML = '';
            if (imageFiles.length === 0) {
                imageCount.textContent = '0 张';
                return;
            }
            let validCount = 0;
            imageFiles.forEach((file, idx) => {
                const blob = processedBlobs.get(idx);
                if (!blob) return;
                validCount++;
                const url = URL.createObjectURL(blob);
                const item = document.createElement('div');
                item.className = 'preview-item';
                item.setAttribute('data-index', idx);
                const img = document.createElement('img');
                img.src = url;
                img.alt = file.name;
                const nameSpan = document.createElement('div');
                nameSpan.className = 'file-name';
                // 显示简短文件名
                let displayName = file.name;
                if (displayName.length > 18) {
                    displayName = displayName.substring(0, 8) + '…' + displayName.substring(displayName.length - 8);
                }
                nameSpan.textContent = displayName;
                item.appendChild(img);
                item.appendChild(nameSpan);
                // 点击缩略图下载单张
                item.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const blobItem = processedBlobs.get(idx);
                    if (blobItem) {
                        const blobUrl = URL.createObjectURL(blobItem);
                        const a = document.createElement('a');
                        a.href = blobUrl;
                        a.download = file.name.replace(/\.[^.]+$/, '_gamma.png'); // 统一加后缀
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                        // 稍后释放 URL
                        setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
                    }
                });
                previewGrid.appendChild(item);
                // 释放预览url (注意: 每次重新渲染会重新生成,之前的自动释放)
                // 但需要注意内存: 这里不主动revoke,因为每次渲染会创建新的,旧的就失效了
            });
            imageCount.textContent = `${validCount} 张`;
        }
        // 添加文件 (去重)
        function addFiles(files) {
            const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/bmp', 'image/gif'];
            let added = 0;
            for (const file of files) {
                if (!allowedTypes.includes(file.type)) continue;
                // 避免完全重名 (简单去重)
                const exists = imageFiles.some(f => f.name === file.name && f.size === file.size);
                if (!exists) {
                    imageFiles.push(file);
                    added++;
                }
            }
            if (added === 0) return;
            // 重置 processedBlobs 重新处理
            reprocessAllImages();
        }
        // UI 更新 (下载按钮状态)
        function updateUI() {
            const hasImages = imageFiles.length > 0;
            downloadAllBtn.disabled = !hasImages;
        }
        // ---------- 事件绑定 ----------
        // 伽马滑块
        gammaSlider.addEventListener('input', function() {
            const val = parseFloat(this.value);
            currentGamma = val;
            updateGammaDisplay(val);
            // 实时处理 (防抖: 用 requestAnimationFrame 或者 简单延迟)
            // 简单 throttle: 每次滑动都重新处理 (图片多会卡,但用户预期实时)
            // 为了流畅,使用防抖: 300ms
            clearTimeout(window._gammaTimer);
            window._gammaTimer = setTimeout(() => {
                reprocessAllImages();
            }, 180);
        });
        // 重置伽马
        resetGammaBtn.addEventListener('click', () => {
            gammaSlider.value = '1.00';
            currentGamma = 1.0;
            updateGammaDisplay('1.00');
            clearTimeout(window._gammaTimer);
            reprocessAllImages();
        });
        // 清空图片
        clearBtn.addEventListener('click', () => {
            imageFiles = [];
            processedBlobs.clear();
            previewGrid.innerHTML = '';
            imageCount.textContent = '0 张';
            updateUI();
            // 释放所有 objectURL 由垃圾回收处理
        });
        // 上传文件
        fileInput.addEventListener('change', (e) => {
            if (e.target.files.length > 0) {
                addFiles(Array.from(e.target.files));
                fileInput.value = ''; // 允许重复选择相同文件
            }
        });
        // 拖拽上传
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#f7b731';
            dropZone.style.background = '#182843';
        });
        dropZone.addEventListener('dragleave', () => {
            dropZone.style.borderColor = '#2f4570';
            dropZone.style.background = '#101a2b';
        });
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#2f4570';
            dropZone.style.background = '#101a2b';
            if (e.dataTransfer.files.length > 0) {
                addFiles(Array.from(e.dataTransfer.files));
            }
        });
        dropZone.addEventListener('click', () => {
            fileInput.click();
        });
        // 批量下载 (将所有处理后的图片打包为 zip? 简单起见逐个下载并略有延迟)
        downloadAllBtn.addEventListener('click', async () => {
            if (imageFiles.length === 0) return;
            // 使用延时逐个触发下载 (更稳健)
            for (let i = 0; i < imageFiles.length; i++) {
                const blob = processedBlobs.get(i);
                if (!blob) continue;
                const file = imageFiles[i];
                const blobUrl = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = blobUrl;
                // 避免重名加前缀
                const baseName = file.name.replace(/\.[^.]+$/, '');
                a.download = `${baseName}_gamma${currentGamma.toFixed(2)}.png`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                // 延迟释放 (为了避免浏览器拦截)
                await new Promise(resolve => setTimeout(resolve, 150));
                URL.revokeObjectURL(blobUrl);
            }
        });
        // 初始化
        updateGammaDisplay(gammaSlider.value);
        updateUI();
        // 清理timer
        window.addEventListener('beforeunload', function() {
            clearTimeout(window._gammaTimer);
        });
    })();
</script>
</body>
</html>

工具功能与操作说明

您可以通过调节滑块实时预览伽马校正效果,并批量处理或下载图片。

图片批量伽马工具中间亮度-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  • 核心调节:中间亮度(伽马值)
    拖动 γ 滑块(范围 0.10 ~ 2.50),数值小于 1 会提亮中间调,大于 1 则压暗中间调,当前值会清晰显示在滑块右侧,方便精确控制。

  • 图片管理与批量处理

    • 添加图片:点击上传区域或直接拖拽图片到虚线框内,支持 JPG、PNG、WebP 等常见格式。
    • 实时预览:所有已添加的图片会以缩略图网格形式显示,并且每次调整伽马滑块后,所有缩略图都会自动更新为校正后的效果。
    • 单独或批量下载:点击任意缩略图可单独下载该图;点击底部 “批量下载全部” 按钮,所有图片会按顺序依次保存到本地,文件名会自动包含当前伽马值。
  • 状态与重置
    顶部状态栏会显示当前图片数量,您可以使用 “重置” 按钮将伽马值恢复为 1.0(无变化),或使用 “清空” 按钮移除所有图片。

所有处理均在您自己的浏览器内完成,图片不会上传至任何服务器,保障数据隐私。

标签: 伽马工具

抱歉,评论功能暂时关闭!