Sfoglia il codice sorgente

相对完善的一版本

xujunwei 6 mesi fa
parent
commit
572ff21fc9
1 ha cambiato i file con 225 aggiunte e 127 eliminazioni
  1. 225 127
      point.html

+ 225 - 127
point.html

@@ -35,24 +35,30 @@
         }
         #center-panel {
             flex: 1;
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            justify-content: flex-start;
-            padding: 20px;
-            max-width: 900px;
-            max-height: 600px;
+            display: block;
+            min-height: 600px;
+            height: 100%;
+            background: #f5f5f5;
             box-sizing: border-box;
         }
+        .canvas-wrapper {
+            width: 900px;
+            height: 600px;
+            background: #f5f5f5;
+            position: relative;
+            overflow: hidden;
+            margin: 0 auto;
+            display: block;
+        }
         #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;
+            background: #acacac;
+            box-shadow: 0 2px 8px #222;
+            display: block;
+            width: 900px;
+            height: 600px;
+            /* 宽高固定 */
         }
         #controls {
             margin-bottom: 10px;
@@ -85,6 +91,10 @@
             justify-content: space-between;
             align-items: center;
         }
+        .box-item.active {
+            background: #ffe58f;
+            border: 1.5px solid #faad14;
+        }
         .del-btn {
             background: #ff7875;
             color: #fff;
@@ -131,6 +141,12 @@
         .img-thumb.active {
             border: 2px solid #1890ff;
         }
+        #filename {
+            color: #ccc;
+            font-size: 15px;
+            margin-top: 10px;
+            text-align: center;
+        }
     </style>
     <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
 </head>
@@ -144,13 +160,18 @@
     <!-- 中间标注区 -->
     <div id="center-panel">
         <h2>图片标注区</h2>
-        <div id="zoom-controls" style="margin-bottom:10px;display:flex;gap:8px;justify-content:center;">
+        <div id="zoom-controls" style="margin:18px 0 10px 0;display:flex;gap:8px;justify-content:center;">
+            <button id="prevImgBtn" style="padding:6px 18px;font-size:15px;border-radius:4px;border:none;background:#1890ff;color:#fff;cursor:pointer;">上一张</button>
             <button id="zoomInBtn" style="padding:4px 14px;">放大</button>
             <button id="zoomOutBtn" style="padding:4px 14px;">缩小</button>
             <button id="zoomResetBtn" style="padding:4px 14px;">重置</button>
+            <button id="nextImgBtn" style="padding:6px 18px;font-size:15px;border-radius:4px;border:none;background:#1890ff;color:#fff;cursor:pointer;">下一张</button>
+        </div>
+        <div class="canvas-wrapper">
+            <canvas id="canvas"></canvas>
+            <div id="coordTip" style="position:fixed;display:none;pointer-events:none;z-index:9999;font-size:12px;color:#fff;background:rgba(0,0,0,0.7);padding:1px 6px;border-radius:4px;"></div>
         </div>
-        <canvas id="canvas"></canvas>
-        <div id="coordTip" style="position:fixed;display:none;pointer-events:none;z-index:9999;font-size:12px;color:#fff;background:rgba(0,0,0,0.7);padding:1px 6px;border-radius:4px;"></div>
+        <div id="filename" style="color:#ccc;font-size:15px;margin:16px 0 0 0;text-align:center;"></div>
     </div>
     <!-- 右侧标注结果 -->
     <div id="right-panel">
@@ -162,11 +183,12 @@
 </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 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; // 拖动时鼠标与框左上角的偏移
@@ -174,19 +196,21 @@
     // 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 imgScale = 1.0; // 图片缩放比例
+    let minImgScale = 0.2, maxImgScale = 5.0; // 缩放上下限
+    let imgOffsetX = 0, imgOffsetY = 0; // 图片在canvas内的平移
+    let baseScale = 1.0; // 图片全图自适应时的缩放比例
     let spacePressed = false; // 是否按下空格
+    let isPanning = false; // 是否正在拖动画布
+    let panStart = {x: 0, y: 0}; // 拖动画布起点
+    let panOffsetStart = {x: 0, y: 0}; // 拖动画布时的初始偏移
 
-    // 判断点(x, y)是否在框box内部
+    // -------------------- 工具函数 --------------------
+    // 判断点(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个手柄的中心坐标
+    // 获取8个手柄的中心坐标(用于缩放)
     function getHandles(box, size) {
         const x = box.x, y = box.y, w = box.w, h = box.h;
         const hs = (size || HANDLE_SIZE) / 2;
@@ -201,7 +225,7 @@
             w:  { x: x - hs, y: y + h/2 - hs } // 左中
         };
     }
-    // 判断点(x, y)是否在某个手柄上,返回手柄类型
+    // 判断点(x, y)是否在某个手柄上,返回手柄类型(canvas坐标)
     function getHandleAt(x, y, box) {
         let domW = $('#canvas').width();
         let pixelRatio = $('#canvas')[0].width / domW;
@@ -230,85 +254,116 @@
         $('#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
+        const info = window._imgDrawInfo;
+        if (info) {
+            // 计算图片在canvas上的显示区域
+            let drawW = info.imgWidth * imgScale;
+            let drawH = info.imgHeight * imgScale;
+            let drawX = (900 - drawW) / 2 + imgOffsetX;
+            let drawY = (600 - drawH) / 2 + imgOffsetY;
+            ctx.drawImage(img, drawX, drawY, drawW, drawH);
+        }
+        let lineWidth = 2;
+        let handleSize = 10;
+        // 绘制所有标注框
         boxes.forEach((box, idx) => {
             ctx.save();
+            let c = toCanvasCoord(box.x, box.y); // 左上角canvas坐标
+            let cw = box.w * imgScale;
+            let ch = box.h * imgScale;
+            // 先绘制半透明背景色
             if (idx === selectedBoxIndex) {
-                ctx.strokeStyle = 'orange'; // 选中框高亮
+                ctx.fillStyle = 'rgba(255,200,0,0.18)';
+            } else {
+                ctx.fillStyle = 'rgba(255,0,0,0.18)';
+            }
+            ctx.fillRect(c.x, c.y, cw, ch);
+            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个缩放手柄
+            ctx.strokeRect(c.x, c.y, cw, ch);
+            // 绘制选中框的8个手柄
             if (idx === selectedBoxIndex) {
                 ctx.fillStyle = 'blue';
-                const handles = getHandles(box, handleSize);
+                const handles = getHandles({x: c.x, y: c.y, w: cw, h: ch}, 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;
+            let c1 = toCanvasCoord(startX, startY);
+            let c2 = toCanvasCoord(endX, endY);
             ctx.strokeRect(
-                Math.min(startX, endX),
-                Math.min(startY, endY),
-                Math.abs(endX - startX),
-                Math.abs(endY - startY)
+                Math.min(c1.x, c2.x),
+                Math.min(c1.y, c2.y),
+                Math.abs(c2.x - c1.x),
+                Math.abs(c2.y - c1.y)
             );
         }
         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>`;
+            const active = (idx === selectedBoxIndex) ? ' active' : '';
+            html += `<div class="box-item${active}">框${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坐标转原图坐标
+    // -------------------- 坐标换算函数 --------------------
+    // 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)
-        };
+        const info = window._imgDrawInfo;
+        if (!info) return {x: 0, y: 0};
+        let drawW = info.imgWidth * imgScale;
+        let drawH = info.imgHeight * imgScale;
+        let drawX = (900 - drawW) / 2 + imgOffsetX;
+        let drawY = (600 - drawH) / 2 + imgOffsetY;
+        let x = (canvasX - drawX) / imgScale;
+        let y = (canvasY - drawY) / imgScale;
+        return {x, y};
+    }
+    // 图片坐标转canvas坐标
+    function toCanvasCoord(imgX, imgY) {
+        const info = window._imgDrawInfo;
+        if (!info) return {x: 0, y: 0};
+        let drawW = info.imgWidth * imgScale;
+        let drawH = info.imgHeight * imgScale;
+        let drawX = (900 - drawW) / 2 + imgOffsetX;
+        let drawY = (600 - drawH) / 2 + imgOffsetY;
+        let x = imgX * imgScale + drawX;
+        let y = imgY * imgScale + drawY;
+        return {x, y};
     }
 
     $(function() {
-        // 图片URL缩略图列表,默认一张图片
+        // -------------------- 图片列表与mock数据 --------------------
+        // 图片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'
+            'https://image.cszcyl.cn/coze/%E9%AB%98%E8%80%83%E5%BF%85%E8%83%9C-%E8%B6%85%E6%B8%85.png'
         ];
-        let currentImgIndex = 0;
+        let currentImgIndex = 0; // 当前图片索引
 
         // mock 标注数据,key为图片url
         let mockBoxes = {
@@ -341,24 +396,36 @@
             loadCurrentImage();
         });
 
-        // 加载当前选中图片到canvas
+        // 加载当前选中图片到canvas,并初始化标注框
         function loadCurrentImage() {
             const url = imgUrlArr[currentImgIndex];
+            // 显示文件名
+            const name = url.split('/').pop();
+            $('#filename').text(name);
+            // 更新按钮禁用状态
+            $('#prevImgBtn').prop('disabled', currentImgIndex === 0);
+            $('#nextImgBtn').prop('disabled', currentImgIndex === imgUrlArr.length - 1);
+            // 先解绑,防止多次触发
+            img.onload = null;
+            img.onerror = null;
             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;
-                // 根据mock数据初始化boxes
+                // 计算自适应缩放比例
+                if (imgWidth <= 900 && imgHeight <= 600) {
+                    baseScale = 1;
+                } else {
+                    baseScale = Math.min(900 / imgWidth, 600 / imgHeight);
+                }
+                imgScale = baseScale;
+                imgOffsetX = 0;
+                imgOffsetY = 0;
+                window._imgDrawInfo = {imgWidth, imgHeight};
+                $('#canvas').attr({ width: 900, height: 600 });
+                $('#canvas').css({ width: '900px', height: '600px', margin: 0, padding: 0 });
+                // 初始化标注框
                 boxes = mockBoxes[url] ? JSON.parse(JSON.stringify(mockBoxes[url])) : [];
+                selectedBoxIndex = -1;
                 drawAll();
                 updateBoxesList();
             };
@@ -371,7 +438,7 @@
         loadCurrentImage();
 
         let resizeHandle = null; // 当前正在拖动的手柄类型
-        // 监听键盘空格按下/松开
+        // 监听键盘空格按下/松开(用于画布拖动)
         $(document).on('keydown', function(e) {
             if (e.code === 'Space') spacePressed = true;
         });
@@ -379,53 +446,65 @@
             if (e.code === 'Space') spacePressed = false;
         });
 
+        // -------------------- canvas鼠标事件 --------------------
         // 鼠标按下事件
         $('#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};
+                panOffsetStart = {x: imgOffsetX, y: imgOffsetY};
                 $('#canvas').css('cursor', 'grab');
                 return;
             }
             let found = false;
             resizeHandle = null;
-            // 优先判断是否点中手柄
+            // 优先判断是否点中手柄或框(全部用canvas坐标判定)
             for (let i = boxes.length - 1; i >= 0; i--) {
-                const handleType = getHandleAt(mouseX, mouseY, boxes[i]);
+                let cbox = toCanvasCoord(boxes[i].x, boxes[i].y);
+                let cw = boxes[i].w * imgScale;
+                let ch = boxes[i].h * imgScale;
+                const handleType = getHandleAt(canvasX, canvasY, {x: cbox.x, y: cbox.y, w: cw, h: ch});
                 if (handleType) {
                     selectedBoxIndex = i;
                     dragMode = 'resize';
                     resizeHandle = handleType;
                     found = true;
                     break;
-                } else if (isInBox(mouseX, mouseY, boxes[i])) {
-                    // 点中框内部,准备移动
+                } else if (
+                    canvasX >= cbox.x && canvasX <= cbox.x + cw &&
+                    canvasY >= cbox.y && canvasY <= cbox.y + ch
+                ) {
                     selectedBoxIndex = i;
                     dragMode = 'move';
-                    dragOffsetX = mouseX - boxes[i].x;
-                    dragOffsetY = mouseY - boxes[i].y;
+                    dragOffsetX = toImageCoord(canvasX, canvasY).x - boxes[i].x;
+                    dragOffsetY = toImageCoord(canvasX, canvasY).y - boxes[i].y;
                     found = true;
                     break;
                 }
             }
             if (found) {
                 drawing = false;
-                drawAll();
-                return;
+            } else {
+                // 没点中任何框,开始新建或取消选中
+                selectedBoxIndex = -1;
+                // 新建时,只有初始点在图片区域内才允许框选
+                const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
+                if (mouseX >= 0 && mouseX <= imgWidth && mouseY >= 0 && mouseY <= imgHeight) {
+                    drawing = true;
+                    startX = mouseX;
+                    startY = mouseY;
+                    endX = mouseX;
+                    endY = mouseY;
+                }
             }
-            // 没点中任何框,开始新建
-            selectedBoxIndex = -1;
-            drawing = true;
-            startX = endX = mouseX;
-            startY = endY = mouseY;
+            // 无论如何都刷新一次,保证视觉同步
             drawAll();
+            updateBoxesList();
         });
 
         // 鼠标移动事件
@@ -450,16 +529,10 @@
             }
             // 新增:画布拖拽
             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;
+                let dx = e.clientX - panStart.x;
+                let dy = e.clientY - panStart.y;
+                imgOffsetX = panOffsetStart.x + dx;
+                imgOffsetY = panOffsetStart.y + dy;
                 drawAll();
                 return;
             }
@@ -549,7 +622,7 @@
             const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
             if (drawing) {
                 drawing = false;
-                // 限制endX、endY不出界
+                // 重新用toImageCoord获取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));
@@ -564,10 +637,10 @@
                 }
                 drawAll();
                 updateBoxesList();
-            } else if (selectedBoxIndex !== -1 && dragMode) {
-                dragMode = null;
-                resizeHandle = null;
             }
+            // 鼠标松开后,始终清空拖动/缩放状态
+            dragMode = null;
+            resizeHandle = null;
         });
 
         // 删除按钮事件
@@ -579,6 +652,17 @@
             updateBoxesList();
         });
 
+        // 右侧标注项点击选中,联动canvas高亮
+        $('#boxes-list').on('click', '.box-item', function(e) {
+            // 避免点击删除按钮时触发
+            if ($(e.target).hasClass('del-btn')) return;
+            const idx = $(this).index();
+            selectedBoxIndex = idx;
+            drawAll();
+            updateBoxesList();
+        });
+
+        // -------------------- 导出/清空/缩放/切换图片等事件 --------------------
         // 导出YOLO格式标注
         $('#exportBtn').on('click', function() {
             // YOLO格式: class x_center y_center width height (均为归一化)
@@ -603,51 +687,65 @@
             updateBoxesList();
         });
 
-        // 放按钮事件
+        // 放大缩小按钮事件
         $('#zoomInBtn').on('click', function() {
-            scale = Math.min(maxScale, scale * 1.2);
+            imgScale = Math.min(maxImgScale, imgScale * 1.2);
             drawAll();
         });
         $('#zoomOutBtn').on('click', function() {
-            scale = Math.max(minScale, scale / 1.2);
+            imgScale = Math.max(minImgScale, imgScale / 1.2);
             drawAll();
         });
         $('#zoomResetBtn').on('click', function() {
-            scale = 1.0;
-            offsetX = 0;
-            offsetY = 0;
+            imgScale = baseScale;
+            imgOffsetX = 0;
+            imgOffsetY = 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;
+            // 1. 记录缩放前鼠标在图片上的坐标
+            let before = toImageCoord(mouseX, mouseY);
+            // 2. 计算缩放
             if (e.originalEvent.deltaY < 0) {
-                scale = Math.min(maxScale, scale * 1.1);
+                imgScale = Math.min(maxImgScale, imgScale * 1.1);
             } else {
-                scale = Math.max(minScale, scale / 1.1);
+                imgScale = Math.max(minImgScale, imgScale / 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;
+            // 3. 计算缩放后该点在canvas上的新位置
+            let after = toCanvasCoord(before.x, before.y);
+            // 4. 调整offset,使鼠标下的图片点不动
+            imgOffsetX += (mouseX - after.x);
+            imgOffsetY += (mouseY - after.y);
             drawAll();
         });
 
         // 禁止右键菜单弹出
         $('#canvas').on('contextmenu', function(e) { e.preventDefault(); });
 
-        // 鼠标移出canvas时隐藏坐标
+        // 鼠标移出canvas时隐藏坐标提示
         $('#canvas').on('mouseleave', function() {
             $('#coordTip').hide();
         });
+
+        // 按钮切换上一张/下一张图片
+        $('#prevImgBtn').on('click', function() {
+            if (currentImgIndex > 0) {
+                currentImgIndex--;
+                renderImgList();
+                loadCurrentImage();
+            }
+        });
+        $('#nextImgBtn').on('click', function() {
+            if (currentImgIndex < imgUrlArr.length - 1) {
+                currentImgIndex++;
+                renderImgList();
+                loadCurrentImage();
+            }
+        });
     });
 </script>
 </body>