<!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(无变化),或使用 “清空” 按钮移除所有图片。
所有处理均在您自己的浏览器内完成,图片不会上传至任何服务器,保障数据隐私。
标签: 伽马工具
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播