<!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>💡 伽马 < 1 提亮中间调,> 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>
工具功能与操作说明
您可以通过调节滑块实时预览伽马校正效果,并批量处理或下载图片。

-
核心调节:中间亮度(伽马值)
拖动 γ 滑块(范围 0.10 ~ 2.50),数值小于 1 会提亮中间调,大于 1 则压暗中间调,当前值会清晰显示在滑块右侧,方便精确控制。 -
图片管理与批量处理
- 添加图片:点击上传区域或直接拖拽图片到虚线框内,支持 JPG、PNG、WebP 等常见格式。
- 实时预览:所有已添加的图片会以缩略图网格形式显示,并且每次调整伽马滑块后,所有缩略图都会自动更新为校正后的效果。
- 单独或批量下载:点击任意缩略图可单独下载该图;点击底部 “批量下载全部” 按钮,所有图片会按顺序依次保存到本地,文件名会自动包含当前伽马值。
-
状态与重置
顶部状态栏会显示当前图片数量,您可以使用 “重置” 按钮将伽马值恢复为 1.0(无变化),或使用 “清空” 按钮移除所有图片。
所有处理均在您自己的浏览器内完成,图片不会上传至任何服务器,保障数据隐私。
标签: 伽马工具
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。