feat: 增加点云显示和打包下载功能
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ __pycache__/
|
||||
/static/received/right/
|
||||
/static/received/left_marked/
|
||||
/static/received/right_marked/
|
||||
# /static/pcd/*.*
|
||||
|
||||
# 数据文件及数据库
|
||||
*.zip
|
||||
|
||||
54
cam_web.py
54
cam_web.py
@@ -255,6 +255,10 @@ def export_images_api():
|
||||
zipf.write(left_export_path, os.path.join('left', left_export_fn))
|
||||
if os.path.exists(right_export_path):
|
||||
zipf.write(right_export_path, os.path.join('right', right_export_fn))
|
||||
pcd_img_path = os.path.join('static', 'received', 'pcd_img', 'pointcloud.png')
|
||||
if os.path.exists(pcd_img_path):
|
||||
zipf.write(pcd_img_path, os.path.join('pcd_img', 'pointcloud.png'))
|
||||
|
||||
|
||||
logger.info(f"Exported {len(rows)} image pairs to {temp_zip_path}")
|
||||
return send_file(temp_zip_path, as_attachment=True, download_name='exported_images.zip')
|
||||
@@ -515,6 +519,56 @@ def simple_view():
|
||||
"""查看图片列表页面"""
|
||||
return render_template('view.html')
|
||||
|
||||
@app.route('/view_pcd')
|
||||
@auth.login_required
|
||||
def pointcloud_viewer():
|
||||
logger.info(f"User {auth.current_user()} accessed the point cloud viewer page.")
|
||||
return render_template('view_pcd.html')
|
||||
|
||||
@app.route('/api/pcd_files', methods=['GET'])
|
||||
@auth.login_required
|
||||
def get_pcd_files():
|
||||
logger.info(f"User {auth.current_user()} requested PCD file list.")
|
||||
pcd_dir = os.path.join(app.static_folder, 'pcd')
|
||||
try:
|
||||
pcd_files = [f for f in os.listdir(pcd_dir) if f.lower().endswith('.pcd')]
|
||||
pcd_files.sort()
|
||||
logger.info(f"Found {len(pcd_files)} PCD files in {pcd_dir}.")
|
||||
return jsonify(pcd_files)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"PCD directory {pcd_dir} does not exist.")
|
||||
return jsonify([])
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing PCD files: {e}")
|
||||
return jsonify({"error": "Failed to list PCD files"}), 500
|
||||
|
||||
@app.route('/api/load_pcd_stream/<filename>', methods=['GET'])
|
||||
@auth.login_required
|
||||
def load_pcd_stream(filename):
|
||||
if '..' in filename or filename.startswith('/'):
|
||||
logger.warning(f"Invalid filename requested: {filename}")
|
||||
return jsonify({"error": "Invalid filename"}), 400
|
||||
|
||||
import os
|
||||
pcd_dir = os.path.join(app.static_folder, 'pcd')
|
||||
requested_path = os.path.abspath(os.path.join(pcd_dir, filename))
|
||||
base_dir = os.path.abspath(pcd_dir)
|
||||
|
||||
if not requested_path.startswith(base_dir + os.sep):
|
||||
logger.warning(f"Attempted path traversal with filename: {filename}")
|
||||
return jsonify({"error": "Invalid filename"}), 400
|
||||
|
||||
if not os.path.exists(requested_path):
|
||||
logger.error(f"PCD file not found: {requested_path}")
|
||||
return jsonify({"error": "File not found"}), 404
|
||||
|
||||
return send_file(
|
||||
requested_path,
|
||||
mimetype='application/octet-stream',
|
||||
as_attachment=False,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
# --- SocketIO 事件处理程序 ---
|
||||
@socketio.event
|
||||
def connect():
|
||||
|
||||
8
static/js/OrbitControls.min.js
vendored
Normal file
8
static/js/OrbitControls.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
static/js/PCDLoader.min.js
vendored
Normal file
8
static/js/PCDLoader.min.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Minified by jsDelivr using Terser v5.37.0.
|
||||
* Original file: /npm/three@0.128.0/examples/js/loaders/PCDLoader.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(){class t extends THREE.Loader{constructor(t){super(t),this.littleEndian=!0}load(t,e,i,n){const s=this,r=new THREE.FileLoader(s.manager);r.setPath(s.path),r.setResponseType("arraybuffer"),r.setRequestHeader(s.requestHeader),r.setWithCredentials(s.withCredentials),r.load(t,(function(i){try{e(s.parse(i,t))}catch(e){n?n(e):console.error(e),s.manager.itemError(t)}}),i,n)}parse(t,e){const i=THREE.LoaderUtils.decodeText(new Uint8Array(t)),n=function(t){const e={},i=t.search(/[\r\n]DATA\s(\S*)\s/i),n=/[\r\n]DATA\s(\S*)\s/i.exec(t.substr(i-1));if(e.data=n[1],e.headerLen=n[0].length+i,e.str=t.substr(0,e.headerLen),e.str=e.str.replace(/\#.*/gi,""),e.version=/VERSION (.*)/i.exec(e.str),e.fields=/FIELDS (.*)/i.exec(e.str),e.size=/SIZE (.*)/i.exec(e.str),e.type=/TYPE (.*)/i.exec(e.str),e.count=/COUNT (.*)/i.exec(e.str),e.width=/WIDTH (.*)/i.exec(e.str),e.height=/HEIGHT (.*)/i.exec(e.str),e.viewpoint=/VIEWPOINT (.*)/i.exec(e.str),e.points=/POINTS (.*)/i.exec(e.str),null!==e.version&&(e.version=parseFloat(e.version[1])),null!==e.fields&&(e.fields=e.fields[1].split(" ")),null!==e.type&&(e.type=e.type[1].split(" ")),null!==e.width&&(e.width=parseInt(e.width[1])),null!==e.height&&(e.height=parseInt(e.height[1])),null!==e.viewpoint&&(e.viewpoint=e.viewpoint[1]),null!==e.points&&(e.points=parseInt(e.points[1],10)),null===e.points&&(e.points=e.width*e.height),null!==e.size&&(e.size=e.size[1].split(" ").map((function(t){return parseInt(t,10)}))),null!==e.count)e.count=e.count[1].split(" ").map((function(t){return parseInt(t,10)}));else{e.count=[];for(let t=0,i=e.fields.length;t<i;t++)e.count.push(1)}e.offset={};let s=0;for(let t=0,i=e.fields.length;t<i;t++)"ascii"===e.data?e.offset[e.fields[t]]=t:(e.offset[e.fields[t]]=s,s+=e.size[t]*e.count[t]);return e.rowSize=s,e}(i),s=[],r=[],o=[];if("ascii"===n.data){const t=n.offset,e=i.substr(n.headerLen).split("\n");for(let i=0,n=e.length;i<n;i++){if(""===e[i])continue;const n=e[i].split(" ");if(void 0!==t.x&&(s.push(parseFloat(n[t.x])),s.push(parseFloat(n[t.y])),s.push(parseFloat(n[t.z]))),void 0!==t.rgb){const e=parseFloat(n[t.rgb]),i=e>>16&255,s=e>>8&255,r=255&e;o.push(i/255,s/255,r/255)}void 0!==t.normal_x&&(r.push(parseFloat(n[t.normal_x])),r.push(parseFloat(n[t.normal_y])),r.push(parseFloat(n[t.normal_z])))}}if("binary_compressed"===n.data){const e=new Uint32Array(t.slice(n.headerLen,n.headerLen+8)),i=e[0],l=e[1],a=function(t,e){const i=t.length,n=new Uint8Array(e);let s,r,o,l=0,a=0;do{if(s=t[l++],s<32){if(s++,a+s>e)throw new Error("Output buffer is not large enough");if(l+s>i)throw new Error("Invalid compressed data");do{n[a++]=t[l++]}while(--s)}else{if(r=s>>5,o=a-((31&s)<<8)-1,l>=i)throw new Error("Invalid compressed data");if(7===r&&(r+=t[l++],l>=i))throw new Error("Invalid compressed data");if(o-=t[l++],a+r+2>e)throw new Error("Output buffer is not large enough");if(o<0)throw new Error("Invalid compressed data");if(o>=a)throw new Error("Invalid compressed data");do{n[a++]=n[o++]}while(2+--r)}}while(l<i);return n}(new Uint8Array(t,n.headerLen+8,i),l),p=new DataView(a.buffer),h=n.offset;for(let t=0;t<n.points;t++)void 0!==h.x&&(s.push(p.getFloat32(n.points*h.x+n.size[0]*t,this.littleEndian)),s.push(p.getFloat32(n.points*h.y+n.size[1]*t,this.littleEndian)),s.push(p.getFloat32(n.points*h.z+n.size[2]*t,this.littleEndian))),void 0!==h.rgb&&(o.push(p.getUint8(n.points*h.rgb+n.size[3]*t+0)/255),o.push(p.getUint8(n.points*h.rgb+n.size[3]*t+1)/255),o.push(p.getUint8(n.points*h.rgb+n.size[3]*t+2)/255)),void 0!==h.normal_x&&(r.push(p.getFloat32(n.points*h.normal_x+n.size[4]*t,this.littleEndian)),r.push(p.getFloat32(n.points*h.normal_y+n.size[5]*t,this.littleEndian)),r.push(p.getFloat32(n.points*h.normal_z+n.size[6]*t,this.littleEndian)))}if("binary"===n.data){const e=new DataView(t,n.headerLen),i=n.offset;for(let t=0,l=0;t<n.points;t++,l+=n.rowSize)void 0!==i.x&&(s.push(e.getFloat32(l+i.x,this.littleEndian)),s.push(e.getFloat32(l+i.y,this.littleEndian)),s.push(e.getFloat32(l+i.z,this.littleEndian))),void 0!==i.rgb&&(o.push(e.getUint8(l+i.rgb+2)/255),o.push(e.getUint8(l+i.rgb+1)/255),o.push(e.getUint8(l+i.rgb+0)/255)),void 0!==i.normal_x&&(r.push(e.getFloat32(l+i.normal_x,this.littleEndian)),r.push(e.getFloat32(l+i.normal_y,this.littleEndian)),r.push(e.getFloat32(l+i.normal_z,this.littleEndian)))}const l=new THREE.BufferGeometry;s.length>0&&l.setAttribute("position",new THREE.Float32BufferAttribute(s,3)),r.length>0&&l.setAttribute("normal",new THREE.Float32BufferAttribute(r,3)),o.length>0&&l.setAttribute("color",new THREE.Float32BufferAttribute(o,3)),l.computeBoundingSphere();const a=new THREE.PointsMaterial({size:.005});o.length>0?a.vertexColors=!0:a.color.setHex(16777215*Math.random());const p=new THREE.Points(l,a);let h=e.split("").reverse().join("");return h=/([^\/]*)/.exec(h),h=h[1].split("").reverse().join(""),p.name=h,p}}THREE.PCDLoader=t}();
|
||||
//# sourceMappingURL=/sm/8ce4feda329ae6ffa4a8ab906318e4470c0a2ccdcb9f200b1f33cc79763deb1a.map
|
||||
6046
static/js/socket.io.js
Normal file
6046
static/js/socket.io.js
Normal file
File diff suppressed because it is too large
Load Diff
6
static/js/three.min.js
vendored
Normal file
6
static/js/three.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
0
static/pcd/.gitkeep
Normal file
0
static/pcd/.gitkeep
Normal file
BIN
static/pcd/desample.pcd
Normal file
BIN
static/pcd/desample.pcd
Normal file
Binary file not shown.
BIN
static/received/pcd_img/pointcloud.png
Normal file
BIN
static/received/pcd_img/pointcloud.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
@@ -112,7 +112,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Capture View</h1>
|
||||
|
||||
<p>
|
||||
<a href="/view_pcd">pcd_view</a>
|
||||
</p>
|
||||
<div id="status">Loading images...</div>
|
||||
|
||||
<div class="controls">
|
||||
|
||||
230
templates/view_pcd.html
Normal file
230
templates/view_pcd.html
Normal file
@@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Point Cloud Viewer (Updated for Binary PCD)</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#info {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#controls select,
|
||||
#controls button {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<div id="info">
|
||||
<h3>Point Cloud Viewer</h3>
|
||||
<p>Use mouse to orbit, zoom, and pan.</p>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<label for="pcdSelect">Select PCD File:</label>
|
||||
<select id="pcdSelect">
|
||||
<option value="">-- Loading files... --</option>
|
||||
</select>
|
||||
<button id="screenshotBtn">Take Screenshot</button>
|
||||
</div>
|
||||
<div id="loading">Loading Point Cloud...</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/three.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/OrbitControls.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/PCDLoader.min.js') }}"></script>
|
||||
|
||||
<script>
|
||||
let scene, camera, renderer, controls, pointCloud = null;
|
||||
const container = document.getElementById('container');
|
||||
const pcdSelect = document.getElementById('pcdSelect');
|
||||
const screenshotBtn = document.getElementById('screenshotBtn');
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
|
||||
init();
|
||||
loadPCDFileList();
|
||||
|
||||
function init() {
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0xdddddd);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.z = 20;
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = false;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = true;
|
||||
controls.minDistance = 1;
|
||||
controls.maxDistance = 1000;
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
|
||||
directionalLight.position.set(10, 20, 15);
|
||||
scene.add(directionalLight);
|
||||
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
animate();
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
function loadPCDFileList() {
|
||||
fetch('/api/pcd_files')
|
||||
.then(response => response.json())
|
||||
.then(files => {
|
||||
pcdSelect.innerHTML = '';
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.text = '-- Select a PCD file --';
|
||||
defaultOption.value = '';
|
||||
pcdSelect.appendChild(defaultOption);
|
||||
|
||||
files.forEach(file => {
|
||||
const option = document.createElement('option');
|
||||
option.text = file;
|
||||
option.value = file;
|
||||
pcdSelect.appendChild(option);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading PCD file list:', error);
|
||||
pcdSelect.innerHTML = '<option value="">-- Error loading files --</option>';
|
||||
});
|
||||
}
|
||||
|
||||
pcdSelect.addEventListener('change', function () {
|
||||
const selectedFile = this.value;
|
||||
if (selectedFile) {
|
||||
loadAndDisplayPCD(selectedFile);
|
||||
} else {
|
||||
if (pointCloud) {
|
||||
scene.remove(pointCloud);
|
||||
pointCloud.geometry.dispose();
|
||||
pointCloud.material.dispose();
|
||||
pointCloud = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function loadAndDisplayPCD(filename) {
|
||||
if (pointCloud) {
|
||||
scene.remove(pointCloud);
|
||||
pointCloud.geometry.dispose();
|
||||
pointCloud.material.dispose();
|
||||
}
|
||||
|
||||
loadingDiv.style.display = 'block';
|
||||
|
||||
const loader = new THREE.PCDLoader();
|
||||
loader.load(
|
||||
`/api/load_pcd_stream/${encodeURIComponent(filename)}`,
|
||||
function (pointsData) {
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
if (pointsData.material) {
|
||||
pointsData.material.size = 0.05;
|
||||
} else {
|
||||
pointsData.material = new THREE.PointsMaterial({ size: 0.05, color: 0x00aaff });
|
||||
}
|
||||
|
||||
pointCloud = pointsData;
|
||||
scene.add(pointCloud);
|
||||
|
||||
if (pointCloud.geometry.boundingBox) {
|
||||
pointCloud.geometry.computeBoundingBox();
|
||||
const size = pointCloud.geometry.boundingBox.getSize(new THREE.Vector3());
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
camera.position.set(0, 0, maxDim * 1.5);
|
||||
controls.target.copy(pointCloud.geometry.boundingBox.getCenter(new THREE.Vector3()));
|
||||
} else {
|
||||
camera.position.set(0, 0, 20);
|
||||
controls.target.set(0, 0, 0);
|
||||
}
|
||||
controls.update();
|
||||
},
|
||||
undefined,
|
||||
function (error) {
|
||||
loadingDiv.style.display = 'none';
|
||||
console.error('Error loading PCD file:', error);
|
||||
alert(`Failed to load PCD file: ${error.message}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
screenshotBtn.addEventListener('click', function () {
|
||||
const canvas = renderer.domElement;
|
||||
const link = document.createElement('a');
|
||||
link.download = `pointcloud_screenshot_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.png`;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user