|
|
|
@ -177,50 +177,12 @@
|
|
|
|
padding: 10px;
|
|
|
|
padding: 10px;
|
|
|
|
border-radius: 8px;
|
|
|
|
border-radius: 8px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.template-section {
|
|
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.template-info {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.template-btn {
|
|
|
|
|
|
|
|
background-color: #ffc107;
|
|
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.template-btn:hover {
|
|
|
|
|
|
|
|
background-color: #e0a800;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.match-result {
|
|
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
|
|
border: 1px solid #007bff;
|
|
|
|
|
|
|
|
background-color: #e6f2ff;
|
|
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
.loading {
|
|
|
|
color: #666;
|
|
|
|
color: #666;
|
|
|
|
font-style: italic;
|
|
|
|
font-style: italic;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.error {
|
|
|
|
.error {
|
|
|
|
color: #dc3545;
|
|
|
|
color: #dc3545;
|
|
|
|
font-size: 12px;
|
|
|
|
font-size: 12px;
|
|
|
|
@ -254,86 +216,62 @@
|
|
|
|
// 存储每个code的裁剪计数
|
|
|
|
// 存储每个code的裁剪计数
|
|
|
|
const cropCounters = {};
|
|
|
|
const cropCounters = {};
|
|
|
|
|
|
|
|
|
|
|
|
// 模板匹配相关
|
|
|
|
|
|
|
|
let templateData = null; // 存储模板的像素数据
|
|
|
|
|
|
|
|
let templateCode = null; // 模板所属的code
|
|
|
|
|
|
|
|
let templateStreet = null; // 模板所属的street
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化页面
|
|
|
|
// 初始化页面
|
|
|
|
function initializePage() {
|
|
|
|
function initializePage() {
|
|
|
|
const grid = document.getElementById('imagesGrid');
|
|
|
|
const grid = document.getElementById('imagesGrid');
|
|
|
|
grid.innerHTML = '';
|
|
|
|
grid.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 先按street分组,再按code分组
|
|
|
|
// 按code分组
|
|
|
|
const groupedByStreet = {};
|
|
|
|
const groupedData = {};
|
|
|
|
imagesData.forEach((item, index) => {
|
|
|
|
imagesData.forEach((item, index) => {
|
|
|
|
const street = item.street || 'unknown';
|
|
|
|
|
|
|
|
const code = item.code || `img_${index}`;
|
|
|
|
const code = item.code || `img_${index}`;
|
|
|
|
if (!groupedByStreet[street]) {
|
|
|
|
if (!groupedData[code]) {
|
|
|
|
groupedByStreet[street] = {};
|
|
|
|
groupedData[code] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!groupedByStreet[street][code]) {
|
|
|
|
|
|
|
|
groupedByStreet[street][code] = [];
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
groupedByStreet[street][code].push({ ...item, index });
|
|
|
|
groupedData[code].push({ ...item, index });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 为每个street添加分隔符和图片
|
|
|
|
// 为每个code添加分隔符和图片
|
|
|
|
Object.keys(groupedByStreet).forEach((street, streetIndex) => {
|
|
|
|
Object.keys(groupedData).forEach((code, codeIndex) => {
|
|
|
|
// 添加street分隔符(如果不是第一个)
|
|
|
|
// 添加分隔符(如果不是第一个)
|
|
|
|
if (streetIndex > 0) {
|
|
|
|
if (codeIndex > 0) {
|
|
|
|
const separator = document.createElement('div');
|
|
|
|
const separator = document.createElement('div');
|
|
|
|
separator.className = 'code-separator';
|
|
|
|
separator.className = 'code-separator';
|
|
|
|
separator.textContent = `Street: ${street}`;
|
|
|
|
separator.textContent = `Code: ${code}`;
|
|
|
|
separator.style.borderColor = '#28a745';
|
|
|
|
|
|
|
|
separator.style.color = '#28a745';
|
|
|
|
|
|
|
|
separator.style.backgroundColor = '#f0fff0';
|
|
|
|
|
|
|
|
grid.appendChild(separator);
|
|
|
|
grid.appendChild(separator);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 为该street下的每个code添加分隔符和图片
|
|
|
|
// 添加该code的所有图片
|
|
|
|
const codes = Object.keys(groupedByStreet[street]);
|
|
|
|
groupedData[code].forEach((item) => {
|
|
|
|
codes.forEach((code, codeIndex) => {
|
|
|
|
const key = `${item.code}_1${item.index + 1}`;
|
|
|
|
// 添加code分隔符
|
|
|
|
const url = item.url;
|
|
|
|
if (codeIndex > 0 || streetIndex > 0) {
|
|
|
|
const imageItem = createImageItem(key, url, code);
|
|
|
|
const separator = document.createElement('div');
|
|
|
|
grid.appendChild(imageItem);
|
|
|
|
separator.className = 'code-separator';
|
|
|
|
|
|
|
|
separator.textContent = `Code: ${code}`;
|
|
|
|
// 初始化裁剪计数器
|
|
|
|
grid.appendChild(separator);
|
|
|
|
if (!cropCounters[code]) {
|
|
|
|
|
|
|
|
cropCounters[code] = 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 在元素添加到DOM后,添加鼠标悬停事件
|
|
|
|
|
|
|
|
const container = document.getElementById(`container-${key}`);
|
|
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (container) {
|
|
|
|
|
|
|
|
container.addEventListener('mouseenter', function() {
|
|
|
|
|
|
|
|
if (!cropStates[key].cropping) {
|
|
|
|
|
|
|
|
startCrop(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加该code的所有图片
|
|
|
|
|
|
|
|
groupedByStreet[street][code].forEach((item) => {
|
|
|
|
|
|
|
|
const key = `${item.code}-1${item.index + 1}`;
|
|
|
|
|
|
|
|
const url = item.url;
|
|
|
|
|
|
|
|
const imageItem = createImageItem(key, url, code, item.street);
|
|
|
|
|
|
|
|
grid.appendChild(imageItem);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化裁剪计数器
|
|
|
|
|
|
|
|
if (!cropCounters[code]) {
|
|
|
|
|
|
|
|
cropCounters[code] = 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 在元素添加到DOM后,添加鼠标悬停事件
|
|
|
|
|
|
|
|
const container = document.getElementById(`container-${key}`);
|
|
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (container) {
|
|
|
|
|
|
|
|
container.addEventListener('mouseenter', function() {
|
|
|
|
|
|
|
|
if (!cropStates[key].cropping) {
|
|
|
|
|
|
|
|
startCrop(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建单个图片项目
|
|
|
|
// 创建单个图片项目
|
|
|
|
function createImageItem(key, url, code, street) {
|
|
|
|
function createImageItem(key, url, code) {
|
|
|
|
const div = document.createElement('div');
|
|
|
|
const div = document.createElement('div');
|
|
|
|
div.className = 'image-item';
|
|
|
|
div.className = 'image-item';
|
|
|
|
div.innerHTML = `
|
|
|
|
div.innerHTML = `
|
|
|
|
@ -341,18 +279,16 @@
|
|
|
|
<div class="image-container" id="container-${key}">
|
|
|
|
<div class="image-container" id="container-${key}">
|
|
|
|
<img class="image" id="img-${key}" src="${url}" alt="图片 ${key}" crossorigin="anonymous" onerror="handleImageError('${key}')">
|
|
|
|
<img class="image" id="img-${key}" src="${url}" alt="图片 ${key}" crossorigin="anonymous" onerror="handleImageError('${key}')">
|
|
|
|
<canvas class="canvas" id="canvas-${key}" style="display: none;"></canvas>
|
|
|
|
<canvas class="canvas" id="canvas-${key}" style="display: none;"></canvas>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="crop-info" id="info-${key}">鼠标悬停开始裁剪</div>
|
|
|
|
<div class="crop-info" id="info-${key}">鼠标悬停开始裁剪</div>
|
|
|
|
<div class="button-group">
|
|
|
|
<div class="button-group">
|
|
|
|
<button class="reset-btn" onclick="resetCrop('${key}')">重置裁剪</button>
|
|
|
|
<button class="reset-btn" onclick="resetCrop('${key}')">重置裁剪</button>
|
|
|
|
<button class="template-btn" onclick="matchTemplate('${key}', '${code}', '${street || ''}')">匹配并裁剪</button>
|
|
|
|
|
|
|
|
<button class="delete-btn" onclick="deleteImage('${key}')">删除图片</button>
|
|
|
|
<button class="delete-btn" onclick="deleteImage('${key}')">删除图片</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="match-result-${key}"></div>
|
|
|
|
|
|
|
|
<div id="cropped-preview-${key}"></div>
|
|
|
|
<div id="cropped-preview-${key}"></div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化裁剪状态
|
|
|
|
// 初始化裁剪状态
|
|
|
|
cropStates[key] = {
|
|
|
|
cropStates[key] = {
|
|
|
|
isSelecting: false,
|
|
|
|
isSelecting: false,
|
|
|
|
@ -362,7 +298,7 @@
|
|
|
|
endY: 0,
|
|
|
|
endY: 0,
|
|
|
|
cropping: false
|
|
|
|
cropping: false
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return div;
|
|
|
|
return div;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -475,33 +411,33 @@
|
|
|
|
function performCrop(key, x, y, width, height) {
|
|
|
|
function performCrop(key, x, y, width, height) {
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
const previewContainer = document.getElementById(`cropped-preview-${key}`);
|
|
|
|
const previewContainer = document.getElementById(`cropped-preview-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 提取code
|
|
|
|
// 提取code
|
|
|
|
const code = key.split('-')[0];
|
|
|
|
const code = key.split('_')[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 增加该code的裁剪计数
|
|
|
|
// 增加该code的裁剪计数
|
|
|
|
cropCounters[code] = (cropCounters[code] || 0) + 1;
|
|
|
|
cropCounters[code] = (cropCounters[code] || 0) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建canvas进行裁剪
|
|
|
|
// 创建canvas进行裁剪
|
|
|
|
const cropCanvas = document.createElement('canvas');
|
|
|
|
const cropCanvas = document.createElement('canvas');
|
|
|
|
const cropCtx = cropCanvas.getContext('2d');
|
|
|
|
const cropCtx = cropCanvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
// 计算实际图片坐标
|
|
|
|
// 计算实际图片坐标
|
|
|
|
const scaleX = img.naturalWidth / img.clientWidth;
|
|
|
|
const scaleX = img.naturalWidth / img.clientWidth;
|
|
|
|
const scaleY = img.naturalHeight / img.clientHeight;
|
|
|
|
const scaleY = img.naturalHeight / img.clientHeight;
|
|
|
|
|
|
|
|
|
|
|
|
const actualX = x * scaleX;
|
|
|
|
const actualX = x * scaleX;
|
|
|
|
const actualY = y * scaleY;
|
|
|
|
const actualY = y * scaleY;
|
|
|
|
const actualWidth = width * scaleX;
|
|
|
|
const actualWidth = width * scaleX;
|
|
|
|
const actualHeight = height * scaleY;
|
|
|
|
const actualHeight = height * scaleY;
|
|
|
|
|
|
|
|
|
|
|
|
cropCanvas.width = actualWidth;
|
|
|
|
cropCanvas.width = actualWidth;
|
|
|
|
cropCanvas.height = actualHeight;
|
|
|
|
cropCanvas.height = actualHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// 裁剪图片
|
|
|
|
// 裁剪图片
|
|
|
|
cropCtx.drawImage(img, actualX, actualY, actualWidth, actualHeight,
|
|
|
|
cropCtx.drawImage(img, actualX, actualY, actualWidth, actualHeight,
|
|
|
|
0, 0, actualWidth, actualHeight);
|
|
|
|
0, 0, actualWidth, actualHeight);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为blob并存储
|
|
|
|
// 转换为blob并存储
|
|
|
|
cropCanvas.toBlob(function(blob) {
|
|
|
|
cropCanvas.toBlob(function(blob) {
|
|
|
|
if (!blob) {
|
|
|
|
if (!blob) {
|
|
|
|
@ -509,11 +445,11 @@
|
|
|
|
document.getElementById(`info-${key}`).textContent = '裁剪失败:无法生成图片数据';
|
|
|
|
document.getElementById(`info-${key}`).textContent = '裁剪失败:无法生成图片数据';
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用code-1 + 序号的格式,如130665-11, 130665-12
|
|
|
|
// 使用code_1 + 序号的格式,如130665_11, 130665_12
|
|
|
|
const cropKey = `${code}-1${cropCounters[code]}`;
|
|
|
|
const cropKey = `${code}_1${cropCounters[code]}`;
|
|
|
|
croppedImages[cropKey] = blob;
|
|
|
|
croppedImages[cropKey] = blob;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示预览
|
|
|
|
// 显示预览
|
|
|
|
previewContainer.innerHTML = `
|
|
|
|
previewContainer.innerHTML = `
|
|
|
|
<div class="cropped-preview">
|
|
|
|
<div class="cropped-preview">
|
|
|
|
@ -521,8 +457,8 @@
|
|
|
|
<img class="cropped-img" src="${URL.createObjectURL(blob)}" alt="裁剪结果">
|
|
|
|
<img class="cropped-img" src="${URL.createObjectURL(blob)}" alt="裁剪结果">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById(`info-${key}`).textContent =
|
|
|
|
document.getElementById(`info-${key}`).textContent =
|
|
|
|
`裁剪完成: ${Math.round(actualWidth)}x${Math.round(actualHeight)} 像素 (${cropKey})`;
|
|
|
|
`裁剪完成: ${Math.round(actualWidth)}x${Math.round(actualHeight)} 像素 (${cropKey})`;
|
|
|
|
}, 'image/png');
|
|
|
|
}, 'image/png');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -557,311 +493,63 @@
|
|
|
|
info.textContent = '点击图片开始裁剪';
|
|
|
|
info.textContent = '点击图片开始裁剪';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 在裁剪完成后,使用当前裁剪作为模板,识别相同巷道、相同code的其他图片
|
|
|
|
|
|
|
|
function matchTemplate(key, code, street) {
|
|
|
|
|
|
|
|
// 检查是否有裁剪的图片作为模板
|
|
|
|
|
|
|
|
const croppedPreview = document.getElementById(`cropped-preview-${key}`);
|
|
|
|
|
|
|
|
const croppedImg = croppedPreview?.querySelector('.cropped-img');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!croppedImg) {
|
|
|
|
|
|
|
|
alert('请先在当前图片上裁剪出一个区域作为模板!');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模板图片
|
|
|
|
|
|
|
|
const templateImg = new Image();
|
|
|
|
|
|
|
|
templateImg.onload = function() {
|
|
|
|
|
|
|
|
// 提取模板像素数据
|
|
|
|
|
|
|
|
const templateCanvas = document.createElement('canvas');
|
|
|
|
|
|
|
|
templateCanvas.width = templateImg.naturalWidth;
|
|
|
|
|
|
|
|
templateCanvas.height = templateImg.naturalHeight;
|
|
|
|
|
|
|
|
const templateCtx = templateCanvas.getContext('2d');
|
|
|
|
|
|
|
|
templateCtx.drawImage(templateImg, 0, 0);
|
|
|
|
|
|
|
|
const templatePixels = templateCtx.getImageData(0, 0, templateCanvas.width, templateCanvas.height);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 保存模板数据
|
|
|
|
|
|
|
|
templateData = templatePixels;
|
|
|
|
|
|
|
|
templateCode = code;
|
|
|
|
|
|
|
|
templateStreet = street;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 在相同巷道、相同code的其他图片上进行匹配
|
|
|
|
|
|
|
|
autoMatchAllImages(code, street);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
templateImg.src = croppedImg.src;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 自动在所有匹配的图片上进行模板匹配和裁剪
|
|
|
|
|
|
|
|
function autoMatchAllImages(targetCode, targetStreet) {
|
|
|
|
|
|
|
|
if (!templateData) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
const matchedKeys = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有图片
|
|
|
|
|
|
|
|
imagesData.forEach((item, index) => {
|
|
|
|
|
|
|
|
const currentCode = item.code;
|
|
|
|
|
|
|
|
const currentStreet = item.street;
|
|
|
|
|
|
|
|
const key = `${item.code}-1${item.index + 1}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 只处理相同巷道和相同code的图片
|
|
|
|
|
|
|
|
if (currentCode === targetCode && currentStreet === targetStreet) {
|
|
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
|
|
if (!img || !img.complete) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 执行模板匹配
|
|
|
|
|
|
|
|
const matchResult = findBestMatch(img, templateData);
|
|
|
|
|
|
|
|
if (matchResult && matchResult.score > 0.7) {
|
|
|
|
|
|
|
|
results.push({
|
|
|
|
|
|
|
|
key: key,
|
|
|
|
|
|
|
|
score: matchResult.score,
|
|
|
|
|
|
|
|
x: matchResult.x,
|
|
|
|
|
|
|
|
y: matchResult.y,
|
|
|
|
|
|
|
|
width: matchResult.width,
|
|
|
|
|
|
|
|
height: matchResult.height
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
matchedKeys.push(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 根据匹配结果自动裁剪
|
|
|
|
|
|
|
|
results.forEach(result => {
|
|
|
|
|
|
|
|
if (result.key !== `${templateCode}-11`) { // 不裁剪模板图片本身
|
|
|
|
|
|
|
|
autoCropByMatch(result);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 提示结果
|
|
|
|
|
|
|
|
if (results.length > 0) {
|
|
|
|
|
|
|
|
alert(`在 ${results.length} 张图片中找到匹配并自动裁剪!`);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
alert('未找到匹配的图片!');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 使用像素对比进行模板匹配(不使用OpenCV)
|
|
|
|
|
|
|
|
function findBestMatch(img, templateData) {
|
|
|
|
|
|
|
|
const targetCanvas = document.createElement('canvas');
|
|
|
|
|
|
|
|
targetCanvas.width = img.naturalWidth;
|
|
|
|
|
|
|
|
targetCanvas.height = img.naturalHeight;
|
|
|
|
|
|
|
|
const targetCtx = targetCanvas.getContext('2d');
|
|
|
|
|
|
|
|
targetCtx.drawImage(img, 0, 0);
|
|
|
|
|
|
|
|
const targetPixels = targetCtx.getImageData(0, 0, targetCanvas.width, targetCanvas.height);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const templateWidth = templateData.width;
|
|
|
|
|
|
|
|
const templateHeight = templateData.height;
|
|
|
|
|
|
|
|
const targetWidth = targetPixels.width;
|
|
|
|
|
|
|
|
const targetHeight = targetPixels.height;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 模板不能比目标图片大
|
|
|
|
|
|
|
|
if (templateWidth > targetWidth || templateHeight > targetHeight) {
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let bestMatch = { score: 0, x: 0, y: 0, width: templateWidth, height: templateHeight };
|
|
|
|
|
|
|
|
const step = 5; // 步长,提高性能
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 滑动窗口匹配
|
|
|
|
|
|
|
|
for (let y = 0; y <= targetHeight - templateHeight; y += step) {
|
|
|
|
|
|
|
|
for (let x = 0; x <= targetWidth - templateWidth; x += step) {
|
|
|
|
|
|
|
|
const score = comparePixelRegion(targetPixels, templateData, x, y, templateWidth, templateHeight);
|
|
|
|
|
|
|
|
if (score > bestMatch.score) {
|
|
|
|
|
|
|
|
bestMatch = { score, x, y, width: templateWidth, height: templateHeight };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return bestMatch.score > 0.7 ? bestMatch : null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 比较两个像素区域的相似度
|
|
|
|
|
|
|
|
function comparePixelRegion(targetPixels, templatePixels, startX, startY, width, height) {
|
|
|
|
|
|
|
|
let totalDiff = 0;
|
|
|
|
|
|
|
|
let totalPixels = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < height; y += 2) { // 每2个像素采样一次,提高性能
|
|
|
|
|
|
|
|
for (let x = 0; x < width; x += 2) {
|
|
|
|
|
|
|
|
const targetIndex = ((startY + y) * targetPixels.width + (startX + x)) * 4;
|
|
|
|
|
|
|
|
const templateIndex = (y * templatePixels.width + x) * 4;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 计算RGB差异
|
|
|
|
|
|
|
|
const diffR = Math.abs(targetPixels.data[targetIndex] - templatePixels.data[templateIndex]);
|
|
|
|
|
|
|
|
const diffG = Math.abs(targetPixels.data[targetIndex + 1] - templatePixels.data[templateIndex + 1]);
|
|
|
|
|
|
|
|
const diffB = Math.abs(targetPixels.data[targetIndex + 2] - templatePixels.data[templateIndex + 2]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
totalDiff += (diffR + diffG + diffB) / 3;
|
|
|
|
|
|
|
|
totalPixels++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const avgDiff = totalDiff / totalPixels;
|
|
|
|
|
|
|
|
// 转换为相似度(0-1)
|
|
|
|
|
|
|
|
const similarity = Math.max(0, 1 - (avgDiff / 255));
|
|
|
|
|
|
|
|
return similarity;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 根据匹配结果自动裁剪
|
|
|
|
|
|
|
|
function autoCropByMatch(result) {
|
|
|
|
|
|
|
|
const { key, x, y, width, height } = result;
|
|
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!img) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 计算缩放比例
|
|
|
|
|
|
|
|
const scaleX = img.clientWidth / img.naturalWidth;
|
|
|
|
|
|
|
|
const scaleY = img.clientHeight / img.naturalHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 转换到显示坐标
|
|
|
|
|
|
|
|
const displayX = x * scaleX;
|
|
|
|
|
|
|
|
const displayY = y * scaleY;
|
|
|
|
|
|
|
|
const displayWidth = width * scaleX;
|
|
|
|
|
|
|
|
const displayHeight = height * scaleY;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 执行裁剪
|
|
|
|
|
|
|
|
performCrop(key, displayX, displayY, displayWidth, displayHeight);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 删除图片
|
|
|
|
// 删除图片
|
|
|
|
function deleteImage(key) {
|
|
|
|
function deleteImage(key) {
|
|
|
|
// 从imagesData中移除
|
|
|
|
// 从imagesData中移除
|
|
|
|
const index = imagesData.findIndex(item => {
|
|
|
|
const index = imagesData.findIndex(item => {
|
|
|
|
const itemKey = item.code ? `${item.code}-1${imagesData.indexOf(item) + 1}` : `img_${imagesData.indexOf(item) + 1}`;
|
|
|
|
const itemKey = item.code ? `${item.code}_1${imagesData.indexOf(item) + 1}` : `img_${imagesData.indexOf(item) + 1}`;
|
|
|
|
return itemKey === key;
|
|
|
|
return itemKey === key;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
if (index !== -1) {
|
|
|
|
imagesData.splice(index, 1);
|
|
|
|
imagesData.splice(index, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除相关数据
|
|
|
|
// 清除相关数据
|
|
|
|
delete croppedImages[key];
|
|
|
|
delete croppedImages[key];
|
|
|
|
delete cropStates[key];
|
|
|
|
delete cropStates[key];
|
|
|
|
|
|
|
|
|
|
|
|
// 重新初始化页面
|
|
|
|
// 重新初始化页面
|
|
|
|
initializePage();
|
|
|
|
initializePage();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按巷道导出
|
|
|
|
|
|
|
|
function exportByStreet(street) {
|
|
|
|
|
|
|
|
const croppedKeys = Object.keys(croppedImages);
|
|
|
|
|
|
|
|
const streetImages = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选出该巷道的图片
|
|
|
|
|
|
|
|
croppedKeys.forEach(key => {
|
|
|
|
|
|
|
|
const code = key.split('-')[0];
|
|
|
|
|
|
|
|
const item = imagesData.find(img => img.code === code && img.street === street);
|
|
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
|
|
streetImages.push(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (streetImages.length === 0) {
|
|
|
|
|
|
|
|
alert(`巷道 ${street} 没有裁剪的图片可以导出!`);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 创建ZIP文件
|
|
|
|
|
|
|
|
createZipFileByStreet(street, streetImages);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 为特定巷道创建ZIP文件
|
|
|
|
|
|
|
|
async function createZipFileByStreet(street, imageKeys) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 收集该巷道的文件数据
|
|
|
|
|
|
|
|
for (const key of imageKeys) {
|
|
|
|
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
|
|
|
|
const arrayBuffer = await blobToArrayBuffer(blob);
|
|
|
|
|
|
|
|
const fileName = `${key}.png`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
files.push({
|
|
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
|
|
data: new Uint8Array(arrayBuffer)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 生成ZIP文件
|
|
|
|
|
|
|
|
const zipBlob = createSimpleZip(files);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 下载ZIP文件
|
|
|
|
|
|
|
|
const fileName = `${street}.zip`;
|
|
|
|
|
|
|
|
downloadFile(zipBlob, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alert(`已导出巷道 ${street} 的 ${imageKeys.length} 个裁剪图片`);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('导出ZIP失败:', error);
|
|
|
|
|
|
|
|
alert('导出ZIP文件失败,请重试!');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 为每个巷道添加导出按钮
|
|
|
|
|
|
|
|
function addStreetExportButtons() {
|
|
|
|
|
|
|
|
const grid = document.getElementById('imagesGrid');
|
|
|
|
|
|
|
|
const streets = new Set(imagesData.map(item => item.street));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
streets.forEach(street => {
|
|
|
|
|
|
|
|
const exportBtn = document.createElement('button');
|
|
|
|
|
|
|
|
exportBtn.className = 'export-btn';
|
|
|
|
|
|
|
|
exportBtn.style.margin = '10px auto';
|
|
|
|
|
|
|
|
exportBtn.style.display = 'block';
|
|
|
|
|
|
|
|
exportBtn.textContent = `导出巷道 ${street}`;
|
|
|
|
|
|
|
|
exportBtn.onclick = () => exportByStreet(street);
|
|
|
|
|
|
|
|
grid.appendChild(exportBtn);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 修改初始化页面函数,添加导出按钮
|
|
|
|
|
|
|
|
const originalInitializePage = initializePage;
|
|
|
|
|
|
|
|
initializePage = function() {
|
|
|
|
|
|
|
|
originalInitializePage();
|
|
|
|
|
|
|
|
addStreetExportButtons();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 创建并下载所有裁剪图片为ZIP
|
|
|
|
// 创建并下载所有裁剪图片
|
|
|
|
async function exportAllCroppedImages() {
|
|
|
|
async function exportAllCroppedImages() {
|
|
|
|
const croppedKeys = Object.keys(croppedImages);
|
|
|
|
const croppedKeys = Object.keys(croppedImages);
|
|
|
|
|
|
|
|
|
|
|
|
if (croppedKeys.length === 0) {
|
|
|
|
if (croppedKeys.length === 0) {
|
|
|
|
alert('没有裁剪的图片可以导出!');
|
|
|
|
alert('没有裁剪的图片可以导出!');
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
// 逐个下载图片文件
|
|
|
|
// 创建ZIP文件
|
|
|
|
for (const key of croppedKeys) {
|
|
|
|
const zipBlob = await createZipFile();
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
|
|
|
|
const fileName = `${key}.png`;
|
|
|
|
// 下载ZIP文件
|
|
|
|
downloadFile(blob, fileName);
|
|
|
|
const fileName = `cropped_images_${new Date().getTime()}.zip`;
|
|
|
|
|
|
|
|
downloadFile(zipBlob, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alert(`已导出 ${croppedKeys.length} 个裁剪图片到ZIP文件\n文件格式:PNG\n文件名格式:130665-11.png, 130665-12.png`);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('导出ZIP失败:', error);
|
|
|
|
|
|
|
|
alert('导出ZIP文件失败,请重试!');
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alert(`已导出 ${croppedKeys.length} 个裁剪图片\n文件格式:PNG\n文件名格式:130665_11.png, 130665_12.png\n请手动创建文件夹整理`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建ZIP文件
|
|
|
|
// 创建ZIP文件
|
|
|
|
async function createZipFile() {
|
|
|
|
async function createZipFile() {
|
|
|
|
const files = [];
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有文件数据
|
|
|
|
// 收集所有文件数据
|
|
|
|
for (const key of Object.keys(croppedImages)) {
|
|
|
|
for (const key of Object.keys(croppedImages)) {
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
const arrayBuffer = await blobToArrayBuffer(blob);
|
|
|
|
const arrayBuffer = await blobToArrayBuffer(blob);
|
|
|
|
const fileName = `${key}.png`;
|
|
|
|
const fileName = `${key}/${key}_11.png`; // 改为PNG格式
|
|
|
|
|
|
|
|
|
|
|
|
files.push({
|
|
|
|
files.push({
|
|
|
|
name: fileName,
|
|
|
|
name: fileName,
|
|
|
|
data: new Uint8Array(arrayBuffer)
|
|
|
|
data: new Uint8Array(arrayBuffer)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成ZIP文件
|
|
|
|
// 生成ZIP文件
|
|
|
|
return createSimpleZip(files);
|
|
|
|
return createSimpleZip(files);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -1253,88 +941,11 @@
|
|
|
|
initializePage();
|
|
|
|
initializePage();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按巷道导出
|
|
|
|
|
|
|
|
function exportByStreet(street) {
|
|
|
|
|
|
|
|
const croppedKeys = Object.keys(croppedImages);
|
|
|
|
|
|
|
|
const streetImages = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选出该巷道的图片
|
|
|
|
|
|
|
|
croppedKeys.forEach(key => {
|
|
|
|
|
|
|
|
const code = key.split('-')[0];
|
|
|
|
|
|
|
|
const item = imagesData.find(img => img.code === code && img.street === street);
|
|
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
|
|
streetImages.push(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (streetImages.length === 0) {
|
|
|
|
|
|
|
|
alert(`巷道 ${street} 没有裁剪的图片可以导出!`);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 创建ZIP文件
|
|
|
|
|
|
|
|
createZipFileByStreet(street, streetImages);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 为特定巷道创建ZIP文件
|
|
|
|
|
|
|
|
async function createZipFileByStreet(street, imageKeys) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 收集该巷道的文件数据
|
|
|
|
|
|
|
|
for (const key of imageKeys) {
|
|
|
|
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
|
|
|
|
const arrayBuffer = await blobToArrayBuffer(blob);
|
|
|
|
|
|
|
|
const fileName = `${key}.png`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
files.push({
|
|
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
|
|
data: new Uint8Array(arrayBuffer)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 生成ZIP文件
|
|
|
|
|
|
|
|
const zipBlob = createSimpleZip(files);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 下载ZIP文件
|
|
|
|
|
|
|
|
const fileName = `${street}.zip`;
|
|
|
|
|
|
|
|
downloadFile(zipBlob, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alert(`已导出巷道 ${street} 的 ${imageKeys.length} 个裁剪图片`);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('导出ZIP失败:', error);
|
|
|
|
|
|
|
|
alert('导出ZIP文件失败,请重试!');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 为每个巷道添加导出按钮
|
|
|
|
|
|
|
|
function addStreetExportButtons() {
|
|
|
|
|
|
|
|
const grid = document.getElementById('imagesGrid');
|
|
|
|
|
|
|
|
const streets = new Set(imagesData.map(item => item.street));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
streets.forEach(street => {
|
|
|
|
|
|
|
|
const exportBtn = document.createElement('button');
|
|
|
|
|
|
|
|
exportBtn.className = 'export-btn';
|
|
|
|
|
|
|
|
exportBtn.style.margin = '10px auto';
|
|
|
|
|
|
|
|
exportBtn.style.display = 'block';
|
|
|
|
|
|
|
|
exportBtn.textContent = `导出巷道 ${street}`;
|
|
|
|
|
|
|
|
exportBtn.onclick = () => exportByStreet(street);
|
|
|
|
|
|
|
|
grid.appendChild(exportBtn);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 修改初始化页面函数,添加导出按钮
|
|
|
|
|
|
|
|
const originalInitializePage = initializePage;
|
|
|
|
|
|
|
|
initializePage = function() {
|
|
|
|
|
|
|
|
originalInitializePage();
|
|
|
|
|
|
|
|
addStreetExportButtons();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 页面加载完成后初始化
|
|
|
|
// 页面加载完成后初始化
|
|
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
|
|
// 图片数据对象
|
|
|
|
// 图片数据对象
|
|
|
|
const imagesData = [
|
|
|
|
const imagesData = [
|
|
|
|
{"code":"130665", "street":"20", "url":"http://10.17.152.53:8007/fileData/20251229206649/40858/130665_error.jpg"}
|
|
|
|
{"code": "130665", "url": "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"}
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|