xujunwei пре 6 месеци
родитељ
комит
63d20992d2
1 измењених фајлова са 621 додато и 0 уклоњено
  1. 621 0
      point.html

+ 621 - 0
point.html

@@ -0,0 +1,621 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>YOLO图片标注工具</title>
+    <style>
+        body { font-family: Arial, sans-serif; margin: 0; background: #f7f7f7; }
+        #main-container {
+            display: flex;
+            height: 100vh;
+        }
+        #left-panel {
+            width: 220px;
+            background: #fff;
+            border-right: 1px solid #eee;
+            padding: 20px 10px 10px 10px;
+            box-sizing: border-box;
+            display: flex;
+            flex-direction: column;
+        }
+        #imgList {
+            flex: 1;
+            overflow-y: auto;
+            margin-top: 10px;
+        }
+        .img-url-item {
+            padding: 6px 8px;
+            border-radius: 4px;
+            cursor: pointer;
+            margin-bottom: 4px;
+            transition: background 0.2s;
+        }
+        .img-url-item.active, .img-url-item:hover {
+            background: #e6f7ff;
+        }
+        #center-panel {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: flex-start;
+            padding: 20px;
+            max-width: 900px;
+            max-height: 600px;
+            box-sizing: border-box;
+        }
+        #canvas {
+            border: 1px solid #ccc;
+            cursor: crosshair;
+            background: #fff;
+            box-shadow: 0 2px 8px #eee;
+            max-width: 900px;
+            max-height: 600px;
+            width: 100%;
+            height: auto;
+        }
+        #controls {
+            margin-bottom: 10px;
+            width: 100%;
+            display: flex;
+            gap: 8px;
+            justify-content: center;
+        }
+        #right-panel {
+            width: 260px;
+            background: #fff;
+            border-left: 1px solid #eee;
+            padding: 20px 10px 10px 10px;
+            box-sizing: border-box;
+            display: flex;
+            flex-direction: column;
+        }
+        #boxes-list {
+            margin-top: 10px;
+            flex: 1;
+            overflow-y: auto;
+        }
+        .box-item {
+            font-size: 14px;
+            background: #f5f5f5;
+            margin-bottom: 6px;
+            padding: 6px 8px;
+            border-radius: 4px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        .del-btn {
+            background: #ff7875;
+            color: #fff;
+            border: none;
+            border-radius: 3px;
+            padding: 2px 8px;
+            cursor: pointer;
+            font-size: 12px;
+        }
+        .del-btn:hover {
+            background: #d9363e;
+        }
+        #exportBtn {
+            margin-top: 10px;
+            width: 100%;
+            background: #1890ff;
+            color: #fff;
+            border: none;
+            border-radius: 4px;
+            padding: 8px 0;
+            font-size: 15px;
+            cursor: pointer;
+        }
+        #exportBtn:disabled {
+            background: #ccc;
+            color: #fff;
+            cursor: not-allowed;
+        }
+        h2 { margin: 0 0 18px 0; text-align: center; }
+        #imgUrlInput { width: 100%; box-sizing: border-box; }
+        #loadImgBtn { width: 100%; }
+        .img-thumb {
+            width: 100%;
+            max-width: 180px;
+            height: 90px;
+            object-fit: cover;
+            border-radius: 6px;
+            border: 2px solid transparent;
+            margin-bottom: 8px;
+            cursor: pointer;
+            transition: border 0.2s;
+            background: #f0f0f0;
+        }
+        .img-thumb.active {
+            border: 2px solid #1890ff;
+        }
+    </style>
+    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+</head>
+<body>
+<div id="main-container">
+    <!-- 左侧图片列表 -->
+    <div id="left-panel">
+        <h2>图片列表</h2>
+        <button id="clearBoxesBtn" style="width:100%;margin-bottom:10px;background:#ff7875;color:#fff;border:none;border-radius:4px;padding:8px 0;font-size:15px;cursor:pointer;">清空所有标记</button>
+        <div id="imgList"></div>
+    </div>
+    <!-- 中间标注区 -->
+    <div id="center-panel">
+        <h2>图片标注区</h2>
+        <div id="zoom-controls" style="margin-bottom:10px;display:flex;gap:8px;justify-content:center;">
+            <button id="zoomInBtn" style="padding:4px 14px;">放大</button>
+            <button id="zoomOutBtn" style="padding:4px 14px;">缩小</button>
+            <button id="zoomResetBtn" style="padding:4px 14px;">重置</button>
+        </div>
+        <canvas id="canvas"></canvas>
+    </div>
+    <!-- 右侧标注结果 -->
+    <div id="right-panel">
+        <h2>标注结果</h2>
+        <div id="boxes-list"></div>
+        <button id="exportBtn" disabled>导出YOLO标注</button>
+    </div>
+</div>
+
+<script>
+    let img = new window.Image();
+    let boxes = [];
+    let drawing = false;
+    let startX = 0, startY = 0, endX = 0, endY = 0;
+    let imgWidth = 0, imgHeight = 0;
+    let selectedBoxIndex = -1; // 当前选中的框索引
+    let dragMode = null; // 拖动模式:null/move/resize
+    let dragOffsetX = 0, dragOffsetY = 0; // 拖动时鼠标与框左上角的偏移
+    const HANDLE_SIZE = 10; // 手柄的像素大小
+    // 8个手柄类型,分别对应四个角和四条边中点
+    const HANDLE_TYPES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
+    // 缩放相关
+    let scale = 1.0;
+    let minScale = 0.2, maxScale = 5.0;
+    let offsetX = 0, offsetY = 0; // 平移偏移(可扩展拖动画布)
+    let isPanning = false; // 是否正在拖动画布
+    let panStart = {x: 0, y: 0}; // 拖拽起点
+    let panOffsetStart = {x: 0, y: 0}; // 拖拽时的初始偏移
+    let spacePressed = false; // 是否按下空格
+
+    // 判断点(x, y)是否在框box内部
+    function isInBox(x, y, box) {
+        return x >= box.x && x <= box.x + box.w && y >= box.y && y <= box.y + box.h;
+    }
+    // 获取8个手柄的中心坐标
+    function getHandles(box, size) {
+        const x = box.x, y = box.y, w = box.w, h = box.h;
+        const hs = (size || HANDLE_SIZE) / 2;
+        return {
+            nw: { x: x - hs, y: y - hs }, // 左上角
+            n:  { x: x + w/2 - hs, y: y - hs }, // 上中
+            ne: { x: x + w - hs, y: y - hs }, // 右上角
+            e:  { x: x + w - hs, y: y + h/2 - hs }, // 右中
+            se: { x: x + w - hs, y: y + h - hs }, // 右下角
+            s:  { x: x + w/2 - hs, y: y + h - hs }, // 下中
+            sw: { x: x - hs, y: y + h - hs }, // 左下角
+            w:  { x: x - hs, y: y + h/2 - hs } // 左中
+        };
+    }
+    // 判断点(x, y)是否在某个手柄上,返回手柄类型
+    function getHandleAt(x, y, box) {
+        let domW = $('#canvas').width();
+        let pixelRatio = $('#canvas')[0].width / domW;
+        let handleSize = 10 * pixelRatio;
+        const handles = getHandles(box, handleSize);
+        for (let type of HANDLE_TYPES) {
+            const hx = handles[type].x, hy = handles[type].y;
+            if (x >= hx && x <= hx + handleSize && y >= hy && y <= hy + handleSize) {
+                return type;
+            }
+        }
+        return null;
+    }
+    // 根据手柄类型设置鼠标指针样式
+    function setCursorByHandle(type) {
+        const map = {
+            nw: 'nwse-resize',
+            se: 'nwse-resize',
+            ne: 'nesw-resize',
+            sw: 'nesw-resize',
+            n: 'ns-resize',
+            s: 'ns-resize',
+            e: 'ew-resize',
+            w: 'ew-resize'
+        };
+        $('#canvas').css('cursor', map[type] || 'default');
+    }
+
+    // 绘制函数,支持缩放和平移
+    function drawAll() {
+        const ctx = $('#canvas')[0].getContext('2d');
+        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+        ctx.save();
+        ctx.scale(scale, scale);
+        ctx.translate(offsetX, offsetY);
+        ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
+        // 计算canvas显示缩放比例
+        let domW = $('#canvas').width();
+        let pixelRatio = $('#canvas')[0].width / domW;
+        let lineWidth = 2 * pixelRatio / scale; // 屏幕上2px
+        let handleSize = 10 * pixelRatio / scale; // 屏幕上10px
+        boxes.forEach((box, idx) => {
+            ctx.save();
+            if (idx === selectedBoxIndex) {
+                ctx.strokeStyle = 'orange'; // 选中框高亮
+                ctx.lineWidth = lineWidth;
+            } else {
+                ctx.strokeStyle = 'red';
+                ctx.lineWidth = lineWidth;
+            }
+            ctx.strokeRect(box.x, box.y, box.w, box.h);
+            // 绘制8个缩放手柄
+            if (idx === selectedBoxIndex) {
+                ctx.fillStyle = 'blue';
+                const handles = getHandles(box, handleSize);
+                for (let type of HANDLE_TYPES) {
+                    ctx.fillRect(handles[type].x, handles[type].y, handleSize, handleSize);
+                }
+            }
+            ctx.restore();
+        });
+        // 绘制新建时的临时框
+        if (drawing) {
+            ctx.strokeStyle = 'blue';
+            ctx.lineWidth = lineWidth;
+            ctx.strokeRect(
+                Math.min(startX, endX),
+                Math.min(startY, endY),
+                Math.abs(endX - startX),
+                Math.abs(endY - startY)
+            );
+        }
+        ctx.restore();
+    }
+
+    function updateBoxesList() {
+        let html = '';
+        boxes.forEach((box, idx) => {
+            html += `<div class="box-item">框${idx+1} [x:${box.x}, y:${box.y}, w:${box.w}, h:${box.h}] <button data-idx="${idx}" class="del-btn">删除</button></div>`;
+        });
+        $('#boxes-list').html(html);
+        $('#exportBtn').prop('disabled', boxes.length === 0);
+    }
+
+    // 工具函数:canvas坐标转原图坐标
+    function toImageCoord(canvasX, canvasY) {
+        // 需要考虑canvas缩放后的显示尺寸
+        let domW = $('#canvas').width();
+        let domH = $('#canvas').height();
+        let ratioX = imgWidth / domW;
+        let ratioY = imgHeight / domH;
+        return {
+            x: (canvasX * ratioX / scale - offsetX),
+            y: (canvasY * ratioY / scale - offsetY)
+        };
+    }
+
+    $(function() {
+        // 图片URL缩略图列表,默认一张图片
+        let imgUrlArr = [
+            'https://image.cszcyl.cn/2022/image/YfJA8eON3exqppNSQrW5EhE9rGdNS1qwou5dgj3L3651WcDDZEy89Hn1A296TXIx.png',
+            'https://image.cszcyl.cn/coze/%E9%AB%98%E8%80%83%E5%BF%85%E8%83%9C-%E8%B6%85%E6%B8%85.png',
+            'https://image.cszcyl.cn/2022/image/1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3.png',
+            'https://image.cszcyl.cn/2022/image/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567abc890def123.png',
+            'https://image.cszcyl.cn/2022/image/sample2.png'
+        ];
+        let currentImgIndex = 0;
+
+        // 渲染左侧图片缩略图列表
+        function renderImgList() {
+            let html = '';
+            imgUrlArr.forEach((url, idx) => {
+                html += `<img src="${url}" class="img-thumb${idx===currentImgIndex?' active':''}" data-idx="${idx}" title="${url}">`;
+            });
+            $('#imgList').html(html);
+        }
+        renderImgList();
+
+        // 点击图片缩略图切换图片
+        $('#imgList').on('click', '.img-thumb', function() {
+            const idx = $(this).data('idx');
+            if (currentImgIndex === idx) return;
+            currentImgIndex = idx;
+            renderImgList();
+            loadCurrentImage();
+        });
+
+        // 加载当前选中图片到canvas
+        function loadCurrentImage() {
+            const url = imgUrlArr[currentImgIndex];
+            img.onload = function() {
+                imgWidth = img.width;
+                imgHeight = img.height;
+                // 计算缩放比例,canvas最大900x600
+                let maxW = 900, maxH = 600;
+                let ratio = Math.min(1, maxW / imgWidth, maxH / imgHeight);
+                let showW = Math.round(imgWidth * ratio);
+                let showH = Math.round(imgHeight * ratio);
+                $('#canvas').attr({ width: imgWidth, height: imgHeight }); // 逻辑像素
+                $('#canvas').css({ width: showW + 'px', height: showH + 'px' }); // 显示像素
+                scale = 1.0;
+                offsetX = 0;
+                offsetY = 0;
+                boxes = [];
+                drawAll();
+                updateBoxesList();
+            };
+            img.onerror = function() {
+                alert('图片加载失败,请检查URL是否正确');
+            };
+            img.src = url;
+        }
+        // 页面加载时自动加载第一张图片
+        loadCurrentImage();
+
+        let resizeHandle = null; // 当前正在拖动的手柄类型
+        // 监听键盘空格按下/松开
+        $(document).on('keydown', function(e) {
+            if (e.code === 'Space') spacePressed = true;
+        });
+        $(document).on('keyup', function(e) {
+            if (e.code === 'Space') spacePressed = false;
+        });
+
+        // 鼠标按下事件
+        $('#canvas').on('mousedown', function(e) {
+            if (!imgWidth) return;
+            const rect = this.getBoundingClientRect();
+            const canvasX = e.clientX - rect.left;
+            const canvasY = e.clientY - rect.top;
+            const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
+            // 新增:空格+左键 或 右键拖动画布
+            if ((spacePressed && e.button === 0) || e.button === 2) {
+                isPanning = true;
+                panStart = {x: e.clientX, y: e.clientY};
+                panOffsetStart = {x: offsetX, y: offsetY};
+                $('#canvas').css('cursor', 'grab');
+                return;
+            }
+            let found = false;
+            resizeHandle = null;
+            // 优先判断是否点中手柄
+            for (let i = boxes.length - 1; i >= 0; i--) {
+                const handleType = getHandleAt(mouseX, mouseY, boxes[i]);
+                if (handleType) {
+                    selectedBoxIndex = i;
+                    dragMode = 'resize';
+                    resizeHandle = handleType;
+                    found = true;
+                    break;
+                } else if (isInBox(mouseX, mouseY, boxes[i])) {
+                    // 点中框内部,准备移动
+                    selectedBoxIndex = i;
+                    dragMode = 'move';
+                    dragOffsetX = mouseX - boxes[i].x;
+                    dragOffsetY = mouseY - boxes[i].y;
+                    found = true;
+                    break;
+                }
+            }
+            if (found) {
+                drawing = false;
+                drawAll();
+                return;
+            }
+            // 没点中任何框,开始新建
+            selectedBoxIndex = -1;
+            drawing = true;
+            startX = endX = mouseX;
+            startY = endY = mouseY;
+            drawAll();
+        });
+
+        // 鼠标移动事件
+        $('#canvas').on('mousemove', function(e) {
+            const rect = this.getBoundingClientRect();
+            const canvasX = e.clientX - rect.left;
+            const canvasY = e.clientY - rect.top;
+            const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
+            // 新增:画布拖拽
+            if (isPanning) {
+                let domW = $('#canvas').width();
+                let domH = $('#canvas').height();
+                let canvasW = $('#canvas')[0].width;
+                let canvasH = $('#canvas')[0].height;
+                let ratioX = canvasW / domW;
+                let ratioY = canvasH / domH;
+                let dx = (e.clientX - panStart.x) * ratioX / scale;
+                let dy = (e.clientY - panStart.y) * ratioY / scale;
+                offsetX = panOffsetStart.x + dx;
+                offsetY = panOffsetStart.y + dy;
+                drawAll();
+                return;
+            }
+            if (drawing) {
+                // 新建框时,动态绘制,限制不出界
+                endX = Math.max(0, Math.min(mouseX, imgWidth));
+                endY = Math.max(0, Math.min(mouseY, imgHeight));
+                drawAll();
+            } else if (selectedBoxIndex !== -1 && dragMode) {
+                let box = boxes[selectedBoxIndex];
+                let minW = 10, minH = 10;
+                let x = box.x, y = box.y, w = box.w, h = box.h;
+                if (dragMode === 'move') {
+                    // 拖动移动框
+                    let newX = mouseX - dragOffsetX;
+                    let newY = mouseY - dragOffsetY;
+                    newX = Math.max(0, Math.min(newX, imgWidth - w));
+                    newY = Math.max(0, Math.min(newY, imgHeight - h));
+                    box.x = newX;
+                    box.y = newY;
+                } else if (dragMode === 'resize' && resizeHandle) {
+                    // 8方向缩放
+                    switch (resizeHandle) {
+                        case 'nw': // 左上角
+                            box.x = Math.min(mouseX, x + w - minW);
+                            box.y = Math.min(mouseY, y + h - minH);
+                            box.w = w + (x - box.x);
+                            box.h = h + (y - box.y);
+                            break;
+                        case 'n': // 上中
+                            box.y = Math.min(mouseY, y + h - minH);
+                            box.h = h + (y - box.y);
+                            break;
+                        case 'ne': // 右上角
+                            box.y = Math.min(mouseY, y + h - minH);
+                            box.w = Math.max(minW, mouseX - x);
+                            box.h = h + (y - box.y);
+                            break;
+                        case 'e': // 右中
+                            box.w = Math.max(minW, mouseX - x);
+                            break;
+                        case 'se': // 右下角
+                            box.w = Math.max(minW, mouseX - x);
+                            box.h = Math.max(minH, mouseY - y);
+                            break;
+                        case 's': // 下中
+                            box.h = Math.max(minH, mouseY - y);
+                            break;
+                        case 'sw': // 左下角
+                            box.x = Math.min(mouseX, x + w - minW);
+                            box.w = w + (x - box.x);
+                            box.h = Math.max(minH, mouseY - y);
+                            break;
+                        case 'w': // 左中
+                            box.x = Math.min(mouseX, x + w - minW);
+                            box.w = w + (x - box.x);
+                            break;
+                    }
+                    // 限制不出界
+                    if (box.x < 0) { box.w += box.x; box.x = 0; }
+                    if (box.y < 0) { box.h += box.y; box.y = 0; }
+                    if (box.x + box.w > imgWidth) box.w = imgWidth - box.x;
+                    if (box.y + box.h > imgHeight) box.h = imgHeight - box.y;
+                }
+                drawAll();
+                updateBoxesList();
+            } else if (selectedBoxIndex !== -1) {
+                // 悬停手柄时改变鼠标样式
+                const box = boxes[selectedBoxIndex];
+                const handleType = getHandleAt(mouseX, mouseY, box);
+                setCursorByHandle(handleType);
+            } else {
+                $('#canvas').css('cursor', 'crosshair');
+            }
+        });
+
+        // 鼠标松开事件
+        $(document).on('mouseup', function(e) {
+            if (isPanning) {
+                isPanning = false;
+                $('#canvas').css('cursor', spacePressed ? 'grab' : 'crosshair');
+                return;
+            }
+            const rect = $('#canvas')[0].getBoundingClientRect();
+            const canvasX = e.clientX - rect.left;
+            const canvasY = e.clientY - rect.top;
+            const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
+            if (drawing) {
+                drawing = false;
+                // 限制endX、endY不出界
+                endX = Math.max(0, Math.min(mouseX, imgWidth));
+                endY = Math.max(0, Math.min(mouseY, imgHeight));
+                const x = Math.max(0, Math.min(startX, endX));
+                const y = Math.max(0, Math.min(startY, endY));
+                let w = Math.abs(endX - startX);
+                let h = Math.abs(endY - startY);
+                // 再次限制宽高不超界
+                if (x + w > imgWidth) w = imgWidth - x;
+                if (y + h > imgHeight) h = imgHeight - y;
+                if (w > 5 && h > 5) {
+                    boxes.push({ x, y, w, h, classId: 0 });
+                }
+                drawAll();
+                updateBoxesList();
+            } else if (selectedBoxIndex !== -1 && dragMode) {
+                dragMode = null;
+                resizeHandle = null;
+            }
+        });
+
+        // 删除按钮事件
+        $('#boxes-list').on('click', '.del-btn', function() {
+            const idx = $(this).data('idx');
+            boxes.splice(idx, 1);
+            if (selectedBoxIndex === idx) selectedBoxIndex = -1;
+            drawAll();
+            updateBoxesList();
+        });
+
+        // 导出YOLO格式标注
+        $('#exportBtn').on('click', function() {
+            // YOLO格式: class x_center y_center width height (均为归一化)
+            let lines = boxes.map(box => {
+                const x_center = (box.x + box.w / 2) / imgWidth;
+                const y_center = (box.y + box.h / 2) / imgHeight;
+                const w = box.w / imgWidth;
+                const h = box.h / imgHeight;
+                return `${box.classId} ${x_center} ${y_center} ${w} ${h}`;
+            });
+            const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
+            const a = document.createElement('a');
+            a.href = URL.createObjectURL(blob);
+            a.download = 'labels.txt';
+            a.click();
+        });
+
+        // 清空所有标记按钮
+        $('#clearBoxesBtn').on('click', function() {
+            boxes = [];
+            drawAll();
+            updateBoxesList();
+        });
+
+        // 缩放按钮事件
+        $('#zoomInBtn').on('click', function() {
+            scale = Math.min(maxScale, scale * 1.2);
+            drawAll();
+        });
+        $('#zoomOutBtn').on('click', function() {
+            scale = Math.max(minScale, scale / 1.2);
+            drawAll();
+        });
+        $('#zoomResetBtn').on('click', function() {
+            scale = 1.0;
+            offsetX = 0;
+            offsetY = 0;
+            drawAll();
+        });
+
+        // 鼠标滚轮缩放
+        $('#canvas').on('wheel', function(e) {
+            e.preventDefault();
+            let mouseX = e.offsetX, mouseY = e.offsetY;
+            // 以鼠标为中心缩放
+            let {x: imgX, y: imgY} = toImageCoord(mouseX, mouseY);
+            let oldScale = scale;
+            if (e.originalEvent.deltaY < 0) {
+                scale = Math.min(maxScale, scale * 1.1);
+            } else {
+                scale = Math.max(minScale, scale / 1.1);
+            }
+            // 修正:缩放后让imgX、imgY依然在(mouseX, mouseY)位置
+            let domW = $('#canvas').width();
+            let domH = $('#canvas').height();
+            let ratioX = imgWidth / domW;
+            let ratioY = imgHeight / domH;
+            offsetX = (mouseX * ratioX) / scale - imgX;
+            offsetY = (mouseY * ratioY) / scale - imgY;
+            drawAll();
+        });
+
+        // 禁止右键菜单弹出
+        $('#canvas').on('contextmenu', function(e) { e.preventDefault(); });
+    });
+</script>
+</body>
+</html>