<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>64线激光雷达投影模拟器</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #1a1a1a; color: #e0e0e0; }
canvas { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5); }
.slider-container { margin-bottom: 1rem; }
input[type=range] { width: 100%; accent-color: #22c55e; }
</style>
</head>
<body class="flex flex-col h-screen overflow-hidden">
<!-- 顶部导航栏 -->
<div class="bg-gray-800 p-4 shadow-md z-10 flex justify-between items-center">
<h1 class="text-xl font-bold text-green-400 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
LiDAR 投影模拟器
</h1>
<label class="cursor-pointer bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded shadow transition">
<span>上传图片</span>
<input type="file" id="imageUpload" accept="image/*" class="hidden">
</label>
</div>
<div class="flex flex-1 overflow-hidden">
<!-- 左侧控制面板 -->
<div class="w-80 bg-gray-900 p-6 overflow-y-auto border-r border-gray-700 flex-shrink-0">
<h2 class="text-lg font-semibold mb-4 text-gray-300">参数控制</h2>
<!-- 深度控制 -->
<div class="slider-container">
<label class="block text-sm font-medium mb-1 flex justify-between">
模拟平面深度 (米)
<span id="val-depth" class="text-green-400">5.0</span>
</label>
<input type="range" id="depth" min="1" max="50" step="0.5" value="5.0">
<p class="text-xs text-gray-500 mt-1">模拟激光打在前方多远的平面上</p>
</div>
<!-- 焦距控制 -->
<div class="slider-container">
<label class="block text-sm font-medium mb-1 flex justify-between">
相机焦距系数 (Zoom)
<span id="val-focal" class="text-green-400">0.8</span>
</label>
<input type="range" id="focal" min="0.1" max="2.0" step="0.05" value="0.8">
<p class="text-xs text-gray-500 mt-1">值越大,视野越窄 (Telephoto)</p>
</div>
<!-- 点大小 -->
<div class="slider-container">
<label class="block text-sm font-medium mb-1 flex justify-between">
激光点大小
<span id="val-size" class="text-green-400">4</span>
</label>
<input type="range" id="pointSize" min="1" max="20" step="1" value="4">
</div>
<!-- 弯曲度控制 (新增) -->
<div class="slider-container">
<label class="block text-sm font-medium mb-1 flex justify-between">
线束弯曲度 (Curvature)
<span id="val-curvature" class="text-green-400">0.05</span>
</label>
<input type="range" id="curvature" min="0" max="0.3" step="0.01" value="0.05">
<p class="text-xs text-gray-500 mt-1">模拟广角畸变或扫描轨迹弯曲</p>
</div>
<!-- 垂直FOV微调 -->
<div class="slider-container border-t border-gray-700 pt-4 mt-4">
<label class="block text-sm font-medium mb-1 flex justify-between">
垂直视场角偏移 (Pitch)
<span id="val-pitch" class="text-green-400">2.0</span>
</label>
<input type="range" id="pitchOffset" min="-10" max="10" step="0.1" value="2.0">
<p class="text-xs text-gray-500 mt-1">模拟雷达安装的俯仰角微调</p>
</div>
<!-- 水平密度 -->
<div class="slider-container">
<label class="block text-sm font-medium mb-1 flex justify-between">
水平扫描密度
<span id="val-density" class="text-green-400">High</span>
</label>
<input type="range" id="density" min="0.1" max="1.0" step="0.1" value="0.2">
<p class="text-xs text-gray-500 mt-1">值越小点越密</p>
</div>
<div class="mt-6 p-3 bg-gray-800 rounded text-xs text-gray-400">
<p>💡 说明:本工具基于针孔相机模型,假设激光雷达与相机光心重合。</p>
</div>
</div>
<!-- 右侧画布区域 -->
<div class="flex-1 bg-black flex items-center justify-center p-4 relative overflow-auto" id="canvasContainer">
<div id="placeholderText" class="text-gray-500 text-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p>请点击右上角上传图片</p>
</div>
<canvas id="canvas" class="max-w-full max-h-full hidden"></canvas>
</div>
</div>
<script>
// DOM 元素
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const uploadInput = document.getElementById('imageUpload');
const placeholder = document.getElementById('placeholderText');
// 参数元素
const params = {
depth: document.getElementById('depth'),
focal: document.getElementById('focal'),
pointSize: document.getElementById('pointSize'),
pitchOffset: document.getElementById('pitchOffset'),
density: document.getElementById('density'),
curvature: document.getElementById('curvature') // 新增
};
// 显示数值的元素
const displays = {
depth: document.getElementById('val-depth'),
focal: document.getElementById('val-focal'),
pointSize: document.getElementById('val-size'),
pitchOffset: document.getElementById('val-pitch'),
density: document.getElementById('val-density'),
curvature: document.getElementById('val-curvature') // 新增
};
// 状态
let currentImage = null;
// 雷达常量
const LIDAR_LINES = 20;
const V_FOV_UP = 2.0;
const V_FOV_DOWN = -24.8;
const H_FOV = 90.0;
// 初始化监听器
uploadInput.addEventListener('change', handleImageUpload);
// 为所有滑块添加监听
Object.keys(params).forEach(key => {
params[key].addEventListener('input', (e) => {
// 更新数值显示
displays[key].innerText = e.target.value;
if(key === 'density') {
displays[key].innerText = parseFloat(e.target.value) < 0.3 ? "High" : "Low";
}
// 重绘
draw();
});
});
function handleImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
currentImage = img;
placeholder.style.display = 'none';
canvas.classList.remove('hidden');
// 设置画布尺寸与图片一致
canvas.width = img.width;
canvas.height = img.height;
draw();
}
img.src = event.target.result;
}
reader.readAsDataURL(file);
}
function deg2rad(deg) {
return deg * Math.PI / 180;
}
function draw() {
if (!currentImage) return;
const width = canvas.width;
const height = canvas.height;
// 1. 清除画布并绘制原图
ctx.clearRect(0, 0, width, height);
ctx.drawImage(currentImage, 0, 0, width, height);
// 获取参数
const depthBase = parseFloat(params.depth.value);
const focalFactor = parseFloat(params.focal.value);
const pointSize = parseInt(params.pointSize.value);
const pitchOffset = parseFloat(params.pitchOffset.value);
const densityStep = parseFloat(params.density.value);
const curvature = parseFloat(params.curvature.value); // 获取弯曲度
// 相机内参
const fx = width * focalFactor;
const fy = width * focalFactor;
const cx = width / 2.0;
const cy = height / 2.0;
// 绘制样式
ctx.fillStyle = '#00ff00'; // 激光绿色
// 2. 模拟激光雷达投影
const angleStepV = (V_FOV_UP - V_FOV_DOWN) / (LIDAR_LINES - 1);
for (let i = 0; i < LIDAR_LINES; i++) {
// 计算垂直角度 (加上用户的微调偏移)
const angleV = V_FOV_DOWN + i * angleStepV + pitchOffset;
const radV = deg2rad(angleV);
// 水平扫描循环
for (let angleH = -H_FOV / 2.0; angleH <= H_FOV / 2.0; angleH += densityStep) {
const radH = deg2rad(angleH);
// --- 3D 坐标计算 (模拟平面投影) ---
// Z轴向前,X轴向右,Y轴向下
// 添加 +/- 1.5% 的深度噪声
const noiseFactor = 0.015;
const depthNoise = depthBase * noiseFactor * (Math.random() - 0.5) * 2;
const Z = depthBase + depthNoise;
// 计算弯曲后的垂直角度
// 随着水平角 radH 的增大,让垂直角 radV 也稍微增大(向上抬)
// 使用平方项来实现抛物线式的弯曲效果
const radVCurved = radV + curvature * Math.pow(radH, 2);
const X = Z * Math.tan(radH);
// 真实世界Y轴向上为正,相机坐标系Y轴向下为正,雷达仰角对应负Y
const Y = -Z * Math.tan(radVCurved);
// --- 投影到 2D 像素 ---
const u = fx * (X / Z) + cx;
const v = fy * (Y / Z) + cy;
// 绘制点 (简单的边界检查)
if (u >= 0 && u < width && v >= 0 && v < height) {
ctx.beginPath();
ctx.arc(u, v, pointSize, 0, 2 * Math.PI);
ctx.fill();
}
}
}
}
</script>
</body>
</html>