<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>在线图像直方图与调整</title>
<style>
body { font-family: sans-serif; padding: 20px; max-width: 1000px; margin: auto; }
#controls { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 20px; }
.control-group { display: flex; align-items: center; }
.control-group label { width: 80px; }
.control-group input[type=range] { flex: 1; margin: 0 10px; }
.control-group input[type=number] { width: 60px; }
canvas { border: 1px solid #ccc; display: block; margin-bottom: 20px; }
</style>
</head>
<body>
<h2>在线图像直方图与调整</h2>
<input type="file" id="fileInput" accept="image/*">
<div id="controls">
<div class="control-group">
<label for="exposureRange">曝光</label>
<input type="range" id="exposureRange" min="-2" max="2" step="0.1" value="0">
<input type="number" id="exposureNumber" min="-2" max="2" step="0.1" value="0">
</div>
<div class="control-group">
<label for="contrastRange">对比度</label>
<input type="range" id="contrastRange" min="-1" max="1" step="0.05" value="0">
<input type="number" id="contrastNumber" min="-1" max="1" step="0.05" value="0">
</div>
<div class="control-group">
<label for="highlightsRange">高光</label>
<input type="range" id="highlightsRange" min="-1" max="1" step="0.05" value="0">
<input type="number" id="highlightsNumber" min="-1" max="1" step="0.05" value="0">
</div>
<div class="control-group">
<label for="shadowsRange">阴影</label>
<input type="range" id="shadowsRange" min="-1" max="1" step="0.05" value="0">
<input type="number" id="shadowsNumber" min="-1" max="1" step="0.05" value="0">
</div>
<div class="control-group">
<label for="whitesRange">白色</label>
<input type="range" id="whitesRange" min="-1" max="1" step="0.05" value="0">
<input type="number" id="whitesNumber" min="-1" max="1" step="0.05" value="0">
</div>
<div class="control-group">
<label for="blacksRange">黑色</label>
<input type="range" id="blacksRange" min="-1" max="1" step="0.05" value="0">
<input type="number" id="blacksNumber" min="-1" max="1" step="0.05" value="0">
</div>
</div>

<canvas id="previewCanvas" width="800" height="600"></canvas>
<canvas id="histCanvas" width="800" height="200"></canvas>

<!-- 引入 Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let originalImageData = null;
const previewCanvas = document.getElementById('previewCanvas');
const previewCtx = previewCanvas.getContext('2d');
const histCanvas = document.getElementById('histCanvas');
const histCtx = histCanvas.getContext('2d');
let histChart = null;

// 读取控件元素
const controls = ['exposure','contrast','highlights','shadows','whites','blacks'];
const state = {};
controls.forEach(name => {
state[name] = 0;
const rangeEl = document.getElementById(name + 'Range');
const numEl = document.getElementById(name + 'Number');
// 同步滑块和数字
rangeEl.addEventListener('input', () => { numEl.value = rangeEl.value; updateState(); });
numEl.addEventListener('input', () => { rangeEl.value = numEl.value; updateState(); });
});

document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
// 调整画布大小
previewCanvas.width = img.width;
previewCanvas.height = img.height;
previewCtx.drawImage(img, 0, 0);
originalImageData = previewCtx.getImageData(0, 0, img.width, img.height);
applyAdjustments();
};
img.src = URL.createObjectURL(file);
});

function updateState() {
controls.forEach(name => {
state[name] = parseFloat(document.getElementById(name + 'Number').value);
});
applyAdjustments();
}

function applyAdjustments() {
if (!originalImageData) return;
const imgData = new ImageData(
new Uint8ClampedArray(originalImageData.data),
originalImageData.width,
originalImageData.height
);
const data = imgData.data;
const {exposure, contrast, highlights, shadows, whites, blacks} = state;
for (let i = 0; i < data.length; i += 4) {
let r = data[i] / 255;
let g = data[i+1] / 255;
let b = data[i+2] / 255;
// 亮度
const lum = 0.2126*r + 0.7152*g + 0.0722*b;
// 曝光:2^EV
const evFactor = Math.pow(2, exposure);
r *= evFactor; g *= evFactor; b *= evFactor;
// 对比度
const cFactor = contrast + 1;
r = (r - 0.5) * cFactor + 0.5;
g = (g - 0.5) * cFactor + 0.5;
b = (b - 0.5) * cFactor + 0.5;
// 高光/阴影
if (lum > 0.5) {
const factor = (lum - 0.5) * 2;
r += highlights * factor;
g += highlights * factor;
b += highlights * factor;
} else {
const factor = (0.5 - lum) * 2;
r += shadows * factor;
g += shadows * factor;
b += shadows * factor;
}
// 白色/黑色剪切
if (lum > 0.8) {
const factor = (lum - 0.8) * 5;
r += whites * factor; g += whites * factor; b += whites * factor;
}
if (lum < 0.2) {
const factor = (0.2 - lum) * 5;
r -= blacks * factor; g -= blacks * factor; b -= blacks * factor;
}
// 限幅
data[i]   = Math.min(255, Math.max(0, r * 255));
data[i+1] = Math.min(255, Math.max(0, g * 255));
data[i+2] = Math.min(255, Math.max(0, b * 255));
}
// 更新预览
previewCtx.putImageData(imgData, 0, 0);
updateHistogram(imgData);
}

function updateHistogram(imageData) {
const bins = 256;
const counts = new Array(bins).fill(0);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// 灰度值
const lum = Math.round(0.299*data[i] + 0.587*data[i+1] + 0.114*data[i+2]);
counts[lum]++;
}
const labels = counts.map((_,i) => i);
if (!histChart) {
histChart = new Chart(histCtx, {
type: 'bar',
data: {
labels,
datasets: [{ label: '像素数', data: counts, backgroundColor: 'rgba(0,0,0,0.5)' }]
},
options: {
responsive: true,
scales: { x: { display: false }, y: { beginAtZero: true } },
plugins: { legend: { display: false } }
}
});
} else {
histChart.data.datasets[0].data = counts;
histChart.update();
}
}
</script>
</body>
</html>
❤️ 转载文章请注明出处,谢谢!❤️