Nguyễn Bá Hùng commited on
Commit ·
6a6419e
1
Parent(s): 7f2e279
hbfshdb
Browse files- label_viewer.py +2 -2
- templates/index.html +223 -4
label_viewer.py
CHANGED
|
@@ -12,8 +12,8 @@ app = Flask(__name__)
|
|
| 12 |
|
| 13 |
# Cấu hình đường dẫn
|
| 14 |
BASE_DIR = Path(__file__).parent.parent # Lên một cấp từ danh_nhan về project_ss
|
| 15 |
-
IMAGES_DIR = BASE_DIR / "data" / "images_crop" / "
|
| 16 |
-
LABELS_DIR = BASE_DIR / "data" / "labels_crop" / "
|
| 17 |
|
| 18 |
# Danh sách các class
|
| 19 |
CLASSES = {
|
|
|
|
| 12 |
|
| 13 |
# Cấu hình đường dẫn
|
| 14 |
BASE_DIR = Path(__file__).parent.parent # Lên một cấp từ danh_nhan về project_ss
|
| 15 |
+
IMAGES_DIR = BASE_DIR / "data" / "images_crop" / "val"
|
| 16 |
+
LABELS_DIR = BASE_DIR / "data" / "labels_crop" / "val"
|
| 17 |
|
| 18 |
# Danh sách các class
|
| 19 |
CLASSES = {
|
templates/index.html
CHANGED
|
@@ -78,15 +78,67 @@
|
|
| 78 |
background: #f8f9fa;
|
| 79 |
border-radius: 8px;
|
| 80 |
position: relative;
|
| 81 |
-
overflow:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
|
| 84 |
.image-container img {
|
| 85 |
-
max-width:
|
| 86 |
-
max-height:
|
| 87 |
object-fit: contain;
|
| 88 |
border-radius: 8px;
|
| 89 |
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
.label-container {
|
|
@@ -336,9 +388,22 @@
|
|
| 336 |
<h2 class="panel-title">🖼️ Ảnh</h2>
|
| 337 |
</div>
|
| 338 |
<div class="image-name" id="imageName">Chưa chọn ảnh</div>
|
| 339 |
-
<div class="image-container">
|
| 340 |
<img id="currentImage" src="" alt="Chọn ảnh để xem" style="display: none;">
|
| 341 |
<div id="imagePlaceholder" class="loading">Chọn ảnh từ danh sách bên trên</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
|
|
@@ -390,6 +455,9 @@
|
|
| 390 |
let imageFiles = {{ image_files | tojson }};
|
| 391 |
let currentImageIndex = -1;
|
| 392 |
let currentImageName = '';
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
// Khởi tạo
|
| 395 |
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -399,8 +467,135 @@
|
|
| 399 |
loadImage(selectedImage);
|
| 400 |
}
|
| 401 |
});
|
|
|
|
|
|
|
|
|
|
| 402 |
});
|
| 403 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
function loadImage(imageName) {
|
| 405 |
currentImageName = imageName;
|
| 406 |
currentImageIndex = imageFiles.indexOf(imageName);
|
|
@@ -413,6 +608,18 @@
|
|
| 413 |
// Hiển thị ảnh
|
| 414 |
const img = document.getElementById('currentImage');
|
| 415 |
const placeholder = document.getElementById('imagePlaceholder');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
img.src = `/images/${imageName}`;
|
| 418 |
img.style.display = 'block';
|
|
@@ -600,6 +807,18 @@
|
|
| 600 |
} else if (e.key === 'Enter' && e.target.id === 'labelEditor') {
|
| 601 |
e.preventDefault();
|
| 602 |
saveAndNext();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
}
|
| 604 |
});
|
| 605 |
</script>
|
|
|
|
| 78 |
background: #f8f9fa;
|
| 79 |
border-radius: 8px;
|
| 80 |
position: relative;
|
| 81 |
+
overflow: auto;
|
| 82 |
+
cursor: grab;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.image-container:active {
|
| 86 |
+
cursor: grabbing;
|
| 87 |
}
|
| 88 |
|
| 89 |
.image-container img {
|
| 90 |
+
max-width: none;
|
| 91 |
+
max-height: none;
|
| 92 |
object-fit: contain;
|
| 93 |
border-radius: 8px;
|
| 94 |
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 95 |
+
transition: transform 0.1s ease;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.zoom-controls {
|
| 99 |
+
position: absolute;
|
| 100 |
+
top: 10px;
|
| 101 |
+
right: 10px;
|
| 102 |
+
display: flex;
|
| 103 |
+
flex-direction: column;
|
| 104 |
+
gap: 5px;
|
| 105 |
+
background: rgba(255, 255, 255, 0.9);
|
| 106 |
+
border-radius: 8px;
|
| 107 |
+
padding: 5px;
|
| 108 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.zoom-btn {
|
| 112 |
+
width: 35px;
|
| 113 |
+
height: 35px;
|
| 114 |
+
border: none;
|
| 115 |
+
border-radius: 6px;
|
| 116 |
+
background: #667eea;
|
| 117 |
+
color: white;
|
| 118 |
+
font-size: 16px;
|
| 119 |
+
font-weight: bold;
|
| 120 |
+
cursor: pointer;
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
justify-content: center;
|
| 124 |
+
transition: all 0.2s;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.zoom-btn:hover {
|
| 128 |
+
background: #5a6fd8;
|
| 129 |
+
transform: scale(1.1);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.zoom-info {
|
| 133 |
+
position: absolute;
|
| 134 |
+
top: 10px;
|
| 135 |
+
left: 10px;
|
| 136 |
+
background: rgba(0, 0, 0, 0.7);
|
| 137 |
+
color: white;
|
| 138 |
+
padding: 5px 10px;
|
| 139 |
+
border-radius: 6px;
|
| 140 |
+
font-size: 12px;
|
| 141 |
+
font-weight: 500;
|
| 142 |
}
|
| 143 |
|
| 144 |
.label-container {
|
|
|
|
| 388 |
<h2 class="panel-title">🖼️ Ảnh</h2>
|
| 389 |
</div>
|
| 390 |
<div class="image-name" id="imageName">Chưa chọn ảnh</div>
|
| 391 |
+
<div class="image-container" id="imageContainer">
|
| 392 |
<img id="currentImage" src="" alt="Chọn ảnh để xem" style="display: none;">
|
| 393 |
<div id="imagePlaceholder" class="loading">Chọn ảnh từ danh sách bên trên</div>
|
| 394 |
+
|
| 395 |
+
<!-- Zoom controls -->
|
| 396 |
+
<div class="zoom-controls" id="zoomControls" style="display: none;">
|
| 397 |
+
<button class="zoom-btn" onclick="zoomIn()" title="Phóng to">+</button>
|
| 398 |
+
<button class="zoom-btn" onclick="zoomOut()" title="Thu nhỏ">−</button>
|
| 399 |
+
<button class="zoom-btn" onclick="resetZoom()" title="Kích thước gốc">⌂</button>
|
| 400 |
+
<button class="zoom-btn" onclick="fitToContainer()" title="Vừa khung">⊞</button>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
<!-- Zoom info -->
|
| 404 |
+
<div class="zoom-info" id="zoomInfo" style="display: none;">
|
| 405 |
+
<span id="zoomLevel">100%</span>
|
| 406 |
+
</div>
|
| 407 |
</div>
|
| 408 |
</div>
|
| 409 |
|
|
|
|
| 455 |
let imageFiles = {{ image_files | tojson }};
|
| 456 |
let currentImageIndex = -1;
|
| 457 |
let currentImageName = '';
|
| 458 |
+
let currentZoom = 1;
|
| 459 |
+
let isDragging = false;
|
| 460 |
+
let startX, startY, scrollLeft, scrollTop;
|
| 461 |
|
| 462 |
// Khởi tạo
|
| 463 |
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
| 467 |
loadImage(selectedImage);
|
| 468 |
}
|
| 469 |
});
|
| 470 |
+
|
| 471 |
+
// Thêm event listeners cho drag và zoom
|
| 472 |
+
setupImageInteraction();
|
| 473 |
});
|
| 474 |
|
| 475 |
+
function setupImageInteraction() {
|
| 476 |
+
const imageContainer = document.getElementById('imageContainer');
|
| 477 |
+
const image = document.getElementById('currentImage');
|
| 478 |
+
|
| 479 |
+
// Mouse wheel zoom
|
| 480 |
+
imageContainer.addEventListener('wheel', function(e) {
|
| 481 |
+
e.preventDefault();
|
| 482 |
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
| 483 |
+
const newZoom = Math.max(0.1, Math.min(5, currentZoom + delta));
|
| 484 |
+
setZoom(newZoom);
|
| 485 |
+
});
|
| 486 |
+
|
| 487 |
+
// Drag functionality
|
| 488 |
+
imageContainer.addEventListener('mousedown', function(e) {
|
| 489 |
+
if (e.target === image) {
|
| 490 |
+
isDragging = true;
|
| 491 |
+
startX = e.pageX - imageContainer.offsetLeft;
|
| 492 |
+
startY = e.pageY - imageContainer.offsetTop;
|
| 493 |
+
scrollLeft = imageContainer.scrollLeft;
|
| 494 |
+
scrollTop = imageContainer.scrollTop;
|
| 495 |
+
imageContainer.style.cursor = 'grabbing';
|
| 496 |
+
}
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
document.addEventListener('mousemove', function(e) {
|
| 500 |
+
if (!isDragging) return;
|
| 501 |
+
e.preventDefault();
|
| 502 |
+
const x = e.pageX - imageContainer.offsetLeft;
|
| 503 |
+
const y = e.pageY - imageContainer.offsetTop;
|
| 504 |
+
const walkX = (x - startX) * 2;
|
| 505 |
+
const walkY = (y - startY) * 2;
|
| 506 |
+
imageContainer.scrollLeft = scrollLeft - walkX;
|
| 507 |
+
imageContainer.scrollTop = scrollTop - walkY;
|
| 508 |
+
});
|
| 509 |
+
|
| 510 |
+
document.addEventListener('mouseup', function() {
|
| 511 |
+
isDragging = false;
|
| 512 |
+
imageContainer.style.cursor = 'grab';
|
| 513 |
+
});
|
| 514 |
+
|
| 515 |
+
// Touch events for mobile
|
| 516 |
+
let lastTouchDistance = 0;
|
| 517 |
+
|
| 518 |
+
imageContainer.addEventListener('touchstart', function(e) {
|
| 519 |
+
if (e.touches.length === 2) {
|
| 520 |
+
lastTouchDistance = getTouchDistance(e.touches);
|
| 521 |
+
}
|
| 522 |
+
});
|
| 523 |
+
|
| 524 |
+
imageContainer.addEventListener('touchmove', function(e) {
|
| 525 |
+
e.preventDefault();
|
| 526 |
+
if (e.touches.length === 2) {
|
| 527 |
+
const currentDistance = getTouchDistance(e.touches);
|
| 528 |
+
const delta = currentDistance - lastTouchDistance;
|
| 529 |
+
const zoomDelta = delta * 0.001;
|
| 530 |
+
const newZoom = Math.max(0.1, Math.min(5, currentZoom + zoomDelta));
|
| 531 |
+
setZoom(newZoom);
|
| 532 |
+
lastTouchDistance = currentDistance;
|
| 533 |
+
}
|
| 534 |
+
});
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
function getTouchDistance(touches) {
|
| 538 |
+
const dx = touches[0].clientX - touches[1].clientX;
|
| 539 |
+
const dy = touches[0].clientY - touches[1].clientY;
|
| 540 |
+
return Math.sqrt(dx * dx + dy * dy);
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
function setZoom(zoom) {
|
| 544 |
+
currentZoom = zoom;
|
| 545 |
+
const image = document.getElementById('currentImage');
|
| 546 |
+
image.style.transform = `scale(${zoom})`;
|
| 547 |
+
|
| 548 |
+
// Update zoom info
|
| 549 |
+
document.getElementById('zoomLevel').textContent = Math.round(zoom * 100) + '%';
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
function zoomIn() {
|
| 553 |
+
const newZoom = Math.min(5, currentZoom * 1.2);
|
| 554 |
+
setZoom(newZoom);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
function zoomOut() {
|
| 558 |
+
const newZoom = Math.max(0.1, currentZoom / 1.2);
|
| 559 |
+
setZoom(newZoom);
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
function resetZoom() {
|
| 563 |
+
setZoom(1);
|
| 564 |
+
centerImage();
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
function fitToContainer() {
|
| 568 |
+
const image = document.getElementById('currentImage');
|
| 569 |
+
const container = document.getElementById('imageContainer');
|
| 570 |
+
|
| 571 |
+
if (image.naturalWidth && image.naturalHeight) {
|
| 572 |
+
const containerRatio = container.clientWidth / container.clientHeight;
|
| 573 |
+
const imageRatio = image.naturalWidth / image.naturalHeight;
|
| 574 |
+
|
| 575 |
+
let zoom;
|
| 576 |
+
if (imageRatio > containerRatio) {
|
| 577 |
+
zoom = (container.clientWidth - 40) / image.naturalWidth;
|
| 578 |
+
} else {
|
| 579 |
+
zoom = (container.clientHeight - 40) / image.naturalHeight;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
setZoom(zoom);
|
| 583 |
+
centerImage();
|
| 584 |
+
}
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
function centerImage() {
|
| 588 |
+
const container = document.getElementById('imageContainer');
|
| 589 |
+
const image = document.getElementById('currentImage');
|
| 590 |
+
|
| 591 |
+
setTimeout(() => {
|
| 592 |
+
const scrollX = (image.offsetWidth * currentZoom - container.clientWidth) / 2;
|
| 593 |
+
const scrollY = (image.offsetHeight * currentZoom - container.clientHeight) / 2;
|
| 594 |
+
container.scrollLeft = Math.max(0, scrollX);
|
| 595 |
+
container.scrollTop = Math.max(0, scrollY);
|
| 596 |
+
}, 50);
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
function loadImage(imageName) {
|
| 600 |
currentImageName = imageName;
|
| 601 |
currentImageIndex = imageFiles.indexOf(imageName);
|
|
|
|
| 608 |
// Hiển thị ảnh
|
| 609 |
const img = document.getElementById('currentImage');
|
| 610 |
const placeholder = document.getElementById('imagePlaceholder');
|
| 611 |
+
const zoomControls = document.getElementById('zoomControls');
|
| 612 |
+
const zoomInfo = document.getElementById('zoomInfo');
|
| 613 |
+
|
| 614 |
+
img.onload = function() {
|
| 615 |
+
// Reset zoom khi load ảnh mới
|
| 616 |
+
setZoom(1);
|
| 617 |
+
centerImage();
|
| 618 |
+
|
| 619 |
+
// Show zoom controls
|
| 620 |
+
zoomControls.style.display = 'flex';
|
| 621 |
+
zoomInfo.style.display = 'block';
|
| 622 |
+
};
|
| 623 |
|
| 624 |
img.src = `/images/${imageName}`;
|
| 625 |
img.style.display = 'block';
|
|
|
|
| 807 |
} else if (e.key === 'Enter' && e.target.id === 'labelEditor') {
|
| 808 |
e.preventDefault();
|
| 809 |
saveAndNext();
|
| 810 |
+
} else if (e.key === '=' || e.key === '+') {
|
| 811 |
+
// Zoom in với phím +
|
| 812 |
+
zoomIn();
|
| 813 |
+
} else if (e.key === '-' || e.key === '_') {
|
| 814 |
+
// Zoom out với phím -
|
| 815 |
+
zoomOut();
|
| 816 |
+
} else if (e.key === '0' && !e.ctrlKey) {
|
| 817 |
+
// Reset zoom với phím 0
|
| 818 |
+
resetZoom();
|
| 819 |
+
} else if (e.key === '9') {
|
| 820 |
+
// Fit to container với phím 9
|
| 821 |
+
fitToContainer();
|
| 822 |
}
|
| 823 |
});
|
| 824 |
</script>
|