feat: 增加监视页面
This commit is contained in:
@@ -666,12 +666,15 @@ def regenerate_marked_images(image_id, detections, side):
|
|||||||
marked_right_img = draw_detections_on_image(img_right, detections, left_position=None, right_position=right_position)
|
marked_right_img = draw_detections_on_image(img_right, detections, left_position=None, right_position=right_position)
|
||||||
cv2.imwrite(right_marked_path, marked_right_img)
|
cv2.imwrite(right_marked_path, marked_right_img)
|
||||||
|
|
||||||
# 添加人工标注页面路由
|
|
||||||
@app.route('/manual-annotation')
|
@app.route('/manual-annotation')
|
||||||
def manual_annotation():
|
def manual_annotation():
|
||||||
"""人工标注页面"""
|
"""标注页面"""
|
||||||
return render_template('manual_annotation.html')
|
return render_template('manual_annotation.html')
|
||||||
|
|
||||||
|
@app.route('/view')
|
||||||
|
def simple_view():
|
||||||
|
"""查看图片列表页面"""
|
||||||
|
return render_template('view.html')
|
||||||
|
|
||||||
# --- SocketIO 事件处理程序 ---
|
# --- SocketIO 事件处理程序 ---
|
||||||
@socketio.event
|
@socketio.event
|
||||||
|
|||||||
387
templates/view.html
Normal file
387
templates/view.html
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Capture View</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-right: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center; /* 居中对齐 */
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
.image-preview {
|
||||||
|
width: 150px; /* 调整预览图宽度 */
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto; /* 图片居中 */
|
||||||
|
}
|
||||||
|
input[type="checkbox"] {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
/* 样式化自动刷新开关 */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Capture View</h1>
|
||||||
|
|
||||||
|
<div id="status">Loading images...</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button id="refreshBtn">Refresh List</button>
|
||||||
|
<button id="exportBtn" disabled>Export Selected</button>
|
||||||
|
<button id="exportAllBtn">Export All</button> <!-- 新增导出全部按钮 -->
|
||||||
|
<!-- 自动刷新开关 -->
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="autoRefreshToggle">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span>Auto Refresh (3s)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="imagesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<input type="checkbox" id="selectAllCheckbox">
|
||||||
|
<label for="selectAllCheckbox" style="display: inline-block; margin-left: 5px; cursor: pointer;">Select All</label>
|
||||||
|
</th>
|
||||||
|
<th>Left Marked Image</th>
|
||||||
|
<th>Right Marked Image</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="imagesTableBody">
|
||||||
|
<!-- 动态加载行 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let allImages = []; // 存储所有图片数据
|
||||||
|
let autoRefreshInterval = null; // 存储自动刷新定时器 ID
|
||||||
|
let isAutoRefreshEnabled = false; // 标记自动刷新是否启用
|
||||||
|
|
||||||
|
// 加载图片列表
|
||||||
|
async function loadImages() {
|
||||||
|
document.getElementById('status').textContent = 'Loading images...';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/images');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
allImages = await response.json();
|
||||||
|
renderTable(allImages);
|
||||||
|
document.getElementById('status').textContent = `Loaded ${allImages.length} image pairs.`;
|
||||||
|
updateSelectAllCheckbox();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading images:', error);
|
||||||
|
document.getElementById('status').textContent = 'Error loading images: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染表格
|
||||||
|
function renderTable(images) {
|
||||||
|
const tbody = document.getElementById('imagesTableBody');
|
||||||
|
tbody.innerHTML = ''; // 清空现有内容
|
||||||
|
|
||||||
|
images.forEach(image => {
|
||||||
|
const row = tbody.insertRow();
|
||||||
|
|
||||||
|
// Checkbox 列
|
||||||
|
const checkboxCell = row.insertCell(0);
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.className = 'selectCheckbox';
|
||||||
|
checkbox.dataset.id = image.id;
|
||||||
|
checkboxCell.appendChild(checkbox);
|
||||||
|
|
||||||
|
// Left Marked Image 列
|
||||||
|
const leftMarkedCell = row.insertCell(1);
|
||||||
|
if (image.left_marked_filename) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `/static/received/left_marked/${image.left_marked_filename}`;
|
||||||
|
img.alt = `Left Marked Image ID ${image.id}`;
|
||||||
|
img.className = 'image-preview';
|
||||||
|
img.onerror = function () {
|
||||||
|
this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="150" height="112" viewBox="0 0 150 112"><rect width="150" height="112" fill="%23eee"/><text x="75" y="56" font-family="Arial" font-size="14" fill="%23999" text-anchor="middle" dominant-baseline="middle">No Image</text></svg>';
|
||||||
|
};
|
||||||
|
leftMarkedCell.appendChild(img);
|
||||||
|
} else {
|
||||||
|
// 如果没有标记图片,显示原图或占位符
|
||||||
|
// 优先显示原图
|
||||||
|
if (image.left_filename) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `/static/received/left/${image.left_filename}`;
|
||||||
|
img.alt = `Left Original Image ID ${image.id}`;
|
||||||
|
img.className = 'image-preview';
|
||||||
|
img.onerror = function () {
|
||||||
|
this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="150" height="112" viewBox="0 0 150 112"><rect width="150" height="112" fill="%23eee"/><text x="75" y="56" font-family="Arial" font-size="14" fill="%23999" text-anchor="middle" dominant-baseline="middle">No Image</text></svg>';
|
||||||
|
};
|
||||||
|
leftMarkedCell.appendChild(img);
|
||||||
|
} else {
|
||||||
|
leftMarkedCell.textContent = "N/A";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Marked Image 列
|
||||||
|
const rightMarkedCell = row.insertCell(2);
|
||||||
|
if (image.right_marked_filename) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `/static/received/right_marked/${image.right_marked_filename}`;
|
||||||
|
img.alt = `Right Marked Image ID ${image.id}`;
|
||||||
|
img.className = 'image-preview';
|
||||||
|
img.onerror = function () {
|
||||||
|
this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="150" height="112" viewBox="0 0 150 112"><rect width="150" height="112" fill="%23eee"/><text x="75" y="56" font-family="Arial" font-size="14" fill="%23999" text-anchor="middle" dominant-baseline="middle">No Image</text></svg>';
|
||||||
|
};
|
||||||
|
rightMarkedCell.appendChild(img);
|
||||||
|
} else {
|
||||||
|
// 如果没有标记图片,显示原图或占位符
|
||||||
|
if (image.right_filename) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `/static/received/right/${image.right_filename}`;
|
||||||
|
img.alt = `Right Original Image ID ${image.id}`;
|
||||||
|
img.className = 'image-preview';
|
||||||
|
img.onerror = function () {
|
||||||
|
this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="150" height="112" viewBox="0 0 150 112"><rect width="150" height="112" fill="%23eee"/><text x="75" y="56" font-family="Arial" font-size="14" fill="%23999" text-anchor="middle" dominant-baseline="middle">No Image</text></svg>';
|
||||||
|
};
|
||||||
|
rightMarkedCell.appendChild(img);
|
||||||
|
} else {
|
||||||
|
rightMarkedCell.textContent = "N/A";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新“全选”复选框状态
|
||||||
|
function updateSelectAllCheckbox() {
|
||||||
|
const checkboxes = document.querySelectorAll('.selectCheckbox');
|
||||||
|
const allSelected = checkboxes.length > 0 && checkboxes.length === document.querySelectorAll('.selectCheckbox:checked').length;
|
||||||
|
document.getElementById('selectAllCheckbox').checked = allSelected;
|
||||||
|
updateExportDeleteButtons(); // 导出按钮依赖选中状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新导出按钮状态
|
||||||
|
function updateExportDeleteButtons() {
|
||||||
|
const selectedCount = getSelectedIds().length;
|
||||||
|
document.getElementById('exportBtn').disabled = selectedCount === 0;
|
||||||
|
// 导出全部按钮始终启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中图片的 ID 列表
|
||||||
|
function getSelectedIds() {
|
||||||
|
const checkboxes = document.querySelectorAll('.selectCheckbox:checked');
|
||||||
|
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换自动刷新
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
const toggle = document.getElementById('autoRefreshToggle');
|
||||||
|
isAutoRefreshEnabled = toggle.checked;
|
||||||
|
|
||||||
|
if (isAutoRefreshEnabled) {
|
||||||
|
// 启用自动刷新
|
||||||
|
if (!autoRefreshInterval) { // 防止重复设置
|
||||||
|
autoRefreshInterval = setInterval(loadImages, 3000);
|
||||||
|
console.log("Auto-refresh started.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 禁用自动刷新
|
||||||
|
if (autoRefreshInterval) {
|
||||||
|
clearInterval(autoRefreshInterval);
|
||||||
|
autoRefreshInterval = null;
|
||||||
|
console.log("Auto-refresh stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新按钮事件
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', () => {
|
||||||
|
loadImages();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全选复选框事件
|
||||||
|
document.getElementById('selectAllCheckbox').addEventListener('change', function () {
|
||||||
|
const isChecked = this.checked;
|
||||||
|
const checkboxes = document.querySelectorAll('.selectCheckbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = isChecked;
|
||||||
|
});
|
||||||
|
updateExportDeleteButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格行内复选框事件(用于更新全选按钮和导出按钮)
|
||||||
|
document.getElementById('imagesTable').addEventListener('change', function (e) {
|
||||||
|
if (e.target.classList.contains('selectCheckbox')) {
|
||||||
|
updateSelectAllCheckbox();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动刷新开关事件
|
||||||
|
document.getElementById('autoRefreshToggle').addEventListener('change', toggleAutoRefresh);
|
||||||
|
|
||||||
|
// 导出按钮事件 (导出选中)
|
||||||
|
document.getElementById('exportBtn').addEventListener('click', async function () {
|
||||||
|
const selectedIds = getSelectedIds();
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
alert('Please select at least one image pair to export.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
document.getElementById('status').textContent = 'Preparing export...';
|
||||||
|
const response = await fetch('/api/images/export', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ids: selectedIds })
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const exportTime = new Date().toLocaleString();
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'Selected' + exportTime + '.zip';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
document.getElementById('status').textContent = `Exported ${selectedIds.length} selected image pairs.`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting selected images:', error);
|
||||||
|
document.getElementById('status').textContent = 'Error exporting selected images: ' + error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出全部按钮事件
|
||||||
|
document.getElementById('exportAllBtn').addEventListener('click', async function () {
|
||||||
|
if (allImages.length === 0) {
|
||||||
|
alert('No images available to export.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取所有图片的 ID
|
||||||
|
const allIds = allImages.map(image => image.id);
|
||||||
|
try {
|
||||||
|
document.getElementById('status').textContent = 'Preparing export...';
|
||||||
|
const response = await fetch('/api/images/export', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ids: allIds }) // 发送所有 ID
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const exportTime = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'All_' + exportTime + '.zip';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
document.getElementById('status').textContent = `Exported all ${allIds.length} image pairs.`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting all images:', error);
|
||||||
|
document.getElementById('status').textContent = 'Error exporting all images: ' + error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面加载时获取图片
|
||||||
|
loadImages();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user