point.html 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>YOLO图片标注工具</title>
  6. <style>
  7. body { font-family: Arial, sans-serif; margin: 0; background: #f7f7f7; }
  8. #main-container {
  9. display: flex;
  10. height: 100vh;
  11. }
  12. #left-panel {
  13. width: 220px;
  14. background: #fff;
  15. border-right: 1px solid #eee;
  16. padding: 20px 10px 10px 10px;
  17. box-sizing: border-box;
  18. display: flex;
  19. flex-direction: column;
  20. }
  21. #imgList {
  22. flex: 1;
  23. overflow-y: auto;
  24. margin-top: 10px;
  25. }
  26. .img-url-item {
  27. padding: 6px 8px;
  28. border-radius: 4px;
  29. cursor: pointer;
  30. margin-bottom: 4px;
  31. transition: background 0.2s;
  32. }
  33. .img-url-item.active, .img-url-item:hover {
  34. background: #e6f7ff;
  35. }
  36. #center-panel {
  37. flex: 1;
  38. display: flex;
  39. flex-direction: column;
  40. align-items: center;
  41. justify-content: flex-start;
  42. padding: 20px;
  43. max-width: 900px;
  44. max-height: 600px;
  45. box-sizing: border-box;
  46. }
  47. #canvas {
  48. border: 1px solid #ccc;
  49. cursor: crosshair;
  50. background: #fff;
  51. box-shadow: 0 2px 8px #eee;
  52. max-width: 900px;
  53. max-height: 600px;
  54. width: 100%;
  55. height: auto;
  56. }
  57. #controls {
  58. margin-bottom: 10px;
  59. width: 100%;
  60. display: flex;
  61. gap: 8px;
  62. justify-content: center;
  63. }
  64. #right-panel {
  65. width: 260px;
  66. background: #fff;
  67. border-left: 1px solid #eee;
  68. padding: 20px 10px 10px 10px;
  69. box-sizing: border-box;
  70. display: flex;
  71. flex-direction: column;
  72. }
  73. #boxes-list {
  74. margin-top: 10px;
  75. flex: 1;
  76. overflow-y: auto;
  77. }
  78. .box-item {
  79. font-size: 14px;
  80. background: #f5f5f5;
  81. margin-bottom: 6px;
  82. padding: 6px 8px;
  83. border-radius: 4px;
  84. display: flex;
  85. justify-content: space-between;
  86. align-items: center;
  87. }
  88. .del-btn {
  89. background: #ff7875;
  90. color: #fff;
  91. border: none;
  92. border-radius: 3px;
  93. padding: 2px 8px;
  94. cursor: pointer;
  95. font-size: 12px;
  96. }
  97. .del-btn:hover {
  98. background: #d9363e;
  99. }
  100. #exportBtn {
  101. margin-top: 10px;
  102. width: 100%;
  103. background: #1890ff;
  104. color: #fff;
  105. border: none;
  106. border-radius: 4px;
  107. padding: 8px 0;
  108. font-size: 15px;
  109. cursor: pointer;
  110. }
  111. #exportBtn:disabled {
  112. background: #ccc;
  113. color: #fff;
  114. cursor: not-allowed;
  115. }
  116. h2 { margin: 0 0 18px 0; text-align: center; }
  117. #imgUrlInput { width: 100%; box-sizing: border-box; }
  118. #loadImgBtn { width: 100%; }
  119. .img-thumb {
  120. width: 100%;
  121. max-width: 180px;
  122. height: 90px;
  123. object-fit: cover;
  124. border-radius: 6px;
  125. border: 2px solid transparent;
  126. margin-bottom: 8px;
  127. cursor: pointer;
  128. transition: border 0.2s;
  129. background: #f0f0f0;
  130. }
  131. .img-thumb.active {
  132. border: 2px solid #1890ff;
  133. }
  134. </style>
  135. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  136. </head>
  137. <body>
  138. <div id="main-container">
  139. <!-- 左侧图片列表 -->
  140. <div id="left-panel">
  141. <h2>图片列表</h2>
  142. <div id="imgList"></div>
  143. </div>
  144. <!-- 中间标注区 -->
  145. <div id="center-panel">
  146. <h2>图片标注区</h2>
  147. <div id="zoom-controls" style="margin-bottom:10px;display:flex;gap:8px;justify-content:center;">
  148. <button id="zoomInBtn" style="padding:4px 14px;">放大</button>
  149. <button id="zoomOutBtn" style="padding:4px 14px;">缩小</button>
  150. <button id="zoomResetBtn" style="padding:4px 14px;">重置</button>
  151. </div>
  152. <canvas id="canvas"></canvas>
  153. <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>
  154. </div>
  155. <!-- 右侧标注结果 -->
  156. <div id="right-panel">
  157. <h2>标注结果</h2>
  158. <div id="boxes-list"></div>
  159. <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>
  160. <button id="exportBtn" disabled style="margin-top:0;">导出YOLO标注</button>
  161. </div>
  162. </div>
  163. <script>
  164. let img = new window.Image();
  165. let boxes = [];
  166. let drawing = false;
  167. let startX = 0, startY = 0, endX = 0, endY = 0;
  168. let imgWidth = 0, imgHeight = 0;
  169. let selectedBoxIndex = -1; // 当前选中的框索引
  170. let dragMode = null; // 拖动模式:null/move/resize
  171. let dragOffsetX = 0, dragOffsetY = 0; // 拖动时鼠标与框左上角的偏移
  172. const HANDLE_SIZE = 10; // 手柄的像素大小
  173. // 8个手柄类型,分别对应四个角和四条边中点
  174. const HANDLE_TYPES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
  175. // 缩放相关
  176. let scale = 1.0;
  177. let minScale = 0.2, maxScale = 5.0;
  178. let offsetX = 0, offsetY = 0; // 平移偏移(可扩展拖动画布)
  179. let isPanning = false; // 是否正在拖动画布
  180. let panStart = {x: 0, y: 0}; // 拖拽起点
  181. let panOffsetStart = {x: 0, y: 0}; // 拖拽时的初始偏移
  182. let spacePressed = false; // 是否按下空格
  183. // 判断点(x, y)是否在框box内部
  184. function isInBox(x, y, box) {
  185. return x >= box.x && x <= box.x + box.w && y >= box.y && y <= box.y + box.h;
  186. }
  187. // 获取8个手柄的中心坐标
  188. function getHandles(box, size) {
  189. const x = box.x, y = box.y, w = box.w, h = box.h;
  190. const hs = (size || HANDLE_SIZE) / 2;
  191. return {
  192. nw: { x: x - hs, y: y - hs }, // 左上角
  193. n: { x: x + w/2 - hs, y: y - hs }, // 上中
  194. ne: { x: x + w - hs, y: y - hs }, // 右上角
  195. e: { x: x + w - hs, y: y + h/2 - hs }, // 右中
  196. se: { x: x + w - hs, y: y + h - hs }, // 右下角
  197. s: { x: x + w/2 - hs, y: y + h - hs }, // 下中
  198. sw: { x: x - hs, y: y + h - hs }, // 左下角
  199. w: { x: x - hs, y: y + h/2 - hs } // 左中
  200. };
  201. }
  202. // 判断点(x, y)是否在某个手柄上,返回手柄类型
  203. function getHandleAt(x, y, box) {
  204. let domW = $('#canvas').width();
  205. let pixelRatio = $('#canvas')[0].width / domW;
  206. let handleSize = 10 * pixelRatio;
  207. const handles = getHandles(box, handleSize);
  208. for (let type of HANDLE_TYPES) {
  209. const hx = handles[type].x, hy = handles[type].y;
  210. if (x >= hx && x <= hx + handleSize && y >= hy && y <= hy + handleSize) {
  211. return type;
  212. }
  213. }
  214. return null;
  215. }
  216. // 根据手柄类型设置鼠标指针样式
  217. function setCursorByHandle(type) {
  218. const map = {
  219. nw: 'nwse-resize',
  220. se: 'nwse-resize',
  221. ne: 'nesw-resize',
  222. sw: 'nesw-resize',
  223. n: 'ns-resize',
  224. s: 'ns-resize',
  225. e: 'ew-resize',
  226. w: 'ew-resize'
  227. };
  228. $('#canvas').css('cursor', map[type] || 'default');
  229. }
  230. // 绘制函数,支持缩放和平移
  231. function drawAll() {
  232. const ctx = $('#canvas')[0].getContext('2d');
  233. ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  234. ctx.save();
  235. ctx.scale(scale, scale);
  236. ctx.translate(offsetX, offsetY);
  237. ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
  238. // 计算canvas显示缩放比例
  239. let domW = $('#canvas').width();
  240. let pixelRatio = $('#canvas')[0].width / domW;
  241. let lineWidth = 2 * pixelRatio / scale; // 屏幕上2px
  242. let handleSize = 10 * pixelRatio / scale; // 屏幕上10px
  243. boxes.forEach((box, idx) => {
  244. ctx.save();
  245. if (idx === selectedBoxIndex) {
  246. ctx.strokeStyle = 'orange'; // 选中框高亮
  247. ctx.lineWidth = lineWidth;
  248. } else {
  249. ctx.strokeStyle = 'red';
  250. ctx.lineWidth = lineWidth;
  251. }
  252. ctx.strokeRect(box.x, box.y, box.w, box.h);
  253. // 绘制8个缩放手柄
  254. if (idx === selectedBoxIndex) {
  255. ctx.fillStyle = 'blue';
  256. const handles = getHandles(box, handleSize);
  257. for (let type of HANDLE_TYPES) {
  258. ctx.fillRect(handles[type].x, handles[type].y, handleSize, handleSize);
  259. }
  260. }
  261. ctx.restore();
  262. });
  263. // 绘制新建时的临时框
  264. if (drawing) {
  265. ctx.strokeStyle = 'blue';
  266. ctx.lineWidth = lineWidth;
  267. ctx.strokeRect(
  268. Math.min(startX, endX),
  269. Math.min(startY, endY),
  270. Math.abs(endX - startX),
  271. Math.abs(endY - startY)
  272. );
  273. }
  274. ctx.restore();
  275. }
  276. function updateBoxesList() {
  277. let html = '';
  278. boxes.forEach((box, idx) => {
  279. 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>`;
  280. });
  281. $('#boxes-list').html(html);
  282. $('#exportBtn').prop('disabled', boxes.length === 0);
  283. }
  284. // 工具函数:canvas坐标转原图坐标
  285. function toImageCoord(canvasX, canvasY) {
  286. // 需要考虑canvas缩放后的显示尺寸
  287. let domW = $('#canvas').width();
  288. let domH = $('#canvas').height();
  289. let ratioX = imgWidth / domW;
  290. let ratioY = imgHeight / domH;
  291. return {
  292. x: (canvasX * ratioX / scale - offsetX),
  293. y: (canvasY * ratioY / scale - offsetY)
  294. };
  295. }
  296. $(function() {
  297. // 图片URL缩略图列表,默认一张图片
  298. let imgUrlArr = [
  299. 'https://image.cszcyl.cn/2022/image/YfJA8eON3exqppNSQrW5EhE9rGdNS1qwou5dgj3L3651WcDDZEy89Hn1A296TXIx.png',
  300. 'https://image.cszcyl.cn/coze/%E9%AB%98%E8%80%83%E5%BF%85%E8%83%9C-%E8%B6%85%E6%B8%85.png',
  301. 'https://image.cszcyl.cn/2022/image/1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3.png',
  302. 'https://image.cszcyl.cn/2022/image/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567abc890def123.png',
  303. 'https://image.cszcyl.cn/2022/image/sample2.png'
  304. ];
  305. let currentImgIndex = 0;
  306. // mock 标注数据,key为图片url
  307. let mockBoxes = {
  308. 'https://image.cszcyl.cn/2022/image/YfJA8eON3exqppNSQrW5EhE9rGdNS1qwou5dgj3L3651WcDDZEy89Hn1A296TXIx.png': [
  309. { x: 100, y: 120, w: 80, h: 60, classId: 0 },
  310. { x: 300, y: 200, w: 120, h: 90, classId: 0 }
  311. ],
  312. 'https://image.cszcyl.cn/coze/%E9%AB%98%E8%80%83%E5%BF%85%E8%83%9C-%E8%B6%85%E6%B8%85.png': [
  313. { x: 50, y: 50, w: 60, h: 60, classId: 0 }
  314. ]
  315. // 其它图片可继续添加
  316. };
  317. // 渲染左侧图片缩略图列表
  318. function renderImgList() {
  319. let html = '';
  320. imgUrlArr.forEach((url, idx) => {
  321. html += `<img src="${url}" class="img-thumb${idx===currentImgIndex?' active':''}" data-idx="${idx}" title="${url}">`;
  322. });
  323. $('#imgList').html(html);
  324. }
  325. renderImgList();
  326. // 点击图片缩略图切换图片
  327. $('#imgList').on('click', '.img-thumb', function() {
  328. const idx = $(this).data('idx');
  329. if (currentImgIndex === idx) return;
  330. currentImgIndex = idx;
  331. renderImgList();
  332. loadCurrentImage();
  333. });
  334. // 加载当前选中图片到canvas
  335. function loadCurrentImage() {
  336. const url = imgUrlArr[currentImgIndex];
  337. img.onload = function() {
  338. imgWidth = img.width;
  339. imgHeight = img.height;
  340. // 计算缩放比例,canvas最大900x600
  341. let maxW = 900, maxH = 600;
  342. let ratio = Math.min(1, maxW / imgWidth, maxH / imgHeight);
  343. let showW = Math.round(imgWidth * ratio);
  344. let showH = Math.round(imgHeight * ratio);
  345. $('#canvas').attr({ width: imgWidth, height: imgHeight }); // 逻辑像素
  346. $('#canvas').css({ width: showW + 'px', height: showH + 'px' }); // 显示像素
  347. scale = 1.0;
  348. offsetX = 0;
  349. offsetY = 0;
  350. // 根据mock数据初始化boxes
  351. boxes = mockBoxes[url] ? JSON.parse(JSON.stringify(mockBoxes[url])) : [];
  352. drawAll();
  353. updateBoxesList();
  354. };
  355. img.onerror = function() {
  356. alert('图片加载失败,请检查URL是否正确');
  357. };
  358. img.src = url;
  359. }
  360. // 页面加载时自动加载第一张图片
  361. loadCurrentImage();
  362. let resizeHandle = null; // 当前正在拖动的手柄类型
  363. // 监听键盘空格按下/松开
  364. $(document).on('keydown', function(e) {
  365. if (e.code === 'Space') spacePressed = true;
  366. });
  367. $(document).on('keyup', function(e) {
  368. if (e.code === 'Space') spacePressed = false;
  369. });
  370. // 鼠标按下事件
  371. $('#canvas').on('mousedown', function(e) {
  372. if (!imgWidth) return;
  373. const rect = this.getBoundingClientRect();
  374. const canvasX = e.clientX - rect.left;
  375. const canvasY = e.clientY - rect.top;
  376. const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
  377. // 新增:空格+左键 或 右键拖动画布
  378. if ((spacePressed && e.button === 0) || e.button === 2) {
  379. isPanning = true;
  380. panStart = {x: e.clientX, y: e.clientY};
  381. panOffsetStart = {x: offsetX, y: offsetY};
  382. $('#canvas').css('cursor', 'grab');
  383. return;
  384. }
  385. let found = false;
  386. resizeHandle = null;
  387. // 优先判断是否点中手柄
  388. for (let i = boxes.length - 1; i >= 0; i--) {
  389. const handleType = getHandleAt(mouseX, mouseY, boxes[i]);
  390. if (handleType) {
  391. selectedBoxIndex = i;
  392. dragMode = 'resize';
  393. resizeHandle = handleType;
  394. found = true;
  395. break;
  396. } else if (isInBox(mouseX, mouseY, boxes[i])) {
  397. // 点中框内部,准备移动
  398. selectedBoxIndex = i;
  399. dragMode = 'move';
  400. dragOffsetX = mouseX - boxes[i].x;
  401. dragOffsetY = mouseY - boxes[i].y;
  402. found = true;
  403. break;
  404. }
  405. }
  406. if (found) {
  407. drawing = false;
  408. drawAll();
  409. return;
  410. }
  411. // 没点中任何框,开始新建
  412. selectedBoxIndex = -1;
  413. drawing = true;
  414. startX = endX = mouseX;
  415. startY = endY = mouseY;
  416. drawAll();
  417. });
  418. // 鼠标移动事件
  419. $('#canvas').on('mousemove', function(e) {
  420. const rect = this.getBoundingClientRect();
  421. const canvasX = e.clientX - rect.left;
  422. const canvasY = e.clientY - rect.top;
  423. const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
  424. // 实时显示坐标
  425. if (imgWidth) {
  426. let showX = Math.round(mouseX);
  427. let showY = Math.round(mouseY);
  428. if (showX >= 0 && showX <= imgWidth && showY >= 0 && showY <= imgHeight) {
  429. $('#coordTip').text(`${showX}, ${showY}`).css({
  430. left: (e.clientX - 10) + 'px',
  431. top: (e.clientY - 28) + 'px',
  432. display: 'block'
  433. });
  434. } else {
  435. $('#coordTip').hide();
  436. }
  437. }
  438. // 新增:画布拖拽
  439. if (isPanning) {
  440. let domW = $('#canvas').width();
  441. let domH = $('#canvas').height();
  442. let canvasW = $('#canvas')[0].width;
  443. let canvasH = $('#canvas')[0].height;
  444. let ratioX = canvasW / domW;
  445. let ratioY = canvasH / domH;
  446. let dx = (e.clientX - panStart.x) * ratioX / scale;
  447. let dy = (e.clientY - panStart.y) * ratioY / scale;
  448. offsetX = panOffsetStart.x + dx;
  449. offsetY = panOffsetStart.y + dy;
  450. drawAll();
  451. return;
  452. }
  453. if (drawing) {
  454. // 新建框时,动态绘制,限制不出界
  455. endX = Math.max(0, Math.min(mouseX, imgWidth));
  456. endY = Math.max(0, Math.min(mouseY, imgHeight));
  457. drawAll();
  458. } else if (selectedBoxIndex !== -1 && dragMode) {
  459. let box = boxes[selectedBoxIndex];
  460. let minW = 10, minH = 10;
  461. let x = box.x, y = box.y, w = box.w, h = box.h;
  462. if (dragMode === 'move') {
  463. // 拖动移动框
  464. let newX = mouseX - dragOffsetX;
  465. let newY = mouseY - dragOffsetY;
  466. newX = Math.max(0, Math.min(newX, imgWidth - w));
  467. newY = Math.max(0, Math.min(newY, imgHeight - h));
  468. box.x = newX;
  469. box.y = newY;
  470. } else if (dragMode === 'resize' && resizeHandle) {
  471. // 8方向缩放
  472. switch (resizeHandle) {
  473. case 'nw': // 左上角
  474. box.x = Math.min(mouseX, x + w - minW);
  475. box.y = Math.min(mouseY, y + h - minH);
  476. box.w = w + (x - box.x);
  477. box.h = h + (y - box.y);
  478. break;
  479. case 'n': // 上中
  480. box.y = Math.min(mouseY, y + h - minH);
  481. box.h = h + (y - box.y);
  482. break;
  483. case 'ne': // 右上角
  484. box.y = Math.min(mouseY, y + h - minH);
  485. box.w = Math.max(minW, mouseX - x);
  486. box.h = h + (y - box.y);
  487. break;
  488. case 'e': // 右中
  489. box.w = Math.max(minW, mouseX - x);
  490. break;
  491. case 'se': // 右下角
  492. box.w = Math.max(minW, mouseX - x);
  493. box.h = Math.max(minH, mouseY - y);
  494. break;
  495. case 's': // 下中
  496. box.h = Math.max(minH, mouseY - y);
  497. break;
  498. case 'sw': // 左下角
  499. box.x = Math.min(mouseX, x + w - minW);
  500. box.w = w + (x - box.x);
  501. box.h = Math.max(minH, mouseY - y);
  502. break;
  503. case 'w': // 左中
  504. box.x = Math.min(mouseX, x + w - minW);
  505. box.w = w + (x - box.x);
  506. break;
  507. }
  508. // 限制不出界
  509. if (box.x < 0) { box.w += box.x; box.x = 0; }
  510. if (box.y < 0) { box.h += box.y; box.y = 0; }
  511. if (box.x + box.w > imgWidth) box.w = imgWidth - box.x;
  512. if (box.y + box.h > imgHeight) box.h = imgHeight - box.y;
  513. }
  514. drawAll();
  515. updateBoxesList();
  516. } else if (selectedBoxIndex !== -1) {
  517. // 悬停手柄时改变鼠标样式
  518. const box = boxes[selectedBoxIndex];
  519. const handleType = getHandleAt(mouseX, mouseY, box);
  520. setCursorByHandle(handleType);
  521. } else {
  522. $('#canvas').css('cursor', 'crosshair');
  523. }
  524. });
  525. // 鼠标松开事件
  526. $(document).on('mouseup', function(e) {
  527. if (isPanning) {
  528. isPanning = false;
  529. $('#canvas').css('cursor', spacePressed ? 'grab' : 'crosshair');
  530. return;
  531. }
  532. const rect = $('#canvas')[0].getBoundingClientRect();
  533. const canvasX = e.clientX - rect.left;
  534. const canvasY = e.clientY - rect.top;
  535. const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
  536. if (drawing) {
  537. drawing = false;
  538. // 限制endX、endY不出界
  539. endX = Math.max(0, Math.min(mouseX, imgWidth));
  540. endY = Math.max(0, Math.min(mouseY, imgHeight));
  541. const x = Math.max(0, Math.min(startX, endX));
  542. const y = Math.max(0, Math.min(startY, endY));
  543. let w = Math.abs(endX - startX);
  544. let h = Math.abs(endY - startY);
  545. // 再次限制宽高不超界
  546. if (x + w > imgWidth) w = imgWidth - x;
  547. if (y + h > imgHeight) h = imgHeight - y;
  548. if (w > 5 && h > 5) {
  549. boxes.push({ x, y, w, h, classId: 0 });
  550. }
  551. drawAll();
  552. updateBoxesList();
  553. } else if (selectedBoxIndex !== -1 && dragMode) {
  554. dragMode = null;
  555. resizeHandle = null;
  556. }
  557. });
  558. // 删除按钮事件
  559. $('#boxes-list').on('click', '.del-btn', function() {
  560. const idx = $(this).data('idx');
  561. boxes.splice(idx, 1);
  562. if (selectedBoxIndex === idx) selectedBoxIndex = -1;
  563. drawAll();
  564. updateBoxesList();
  565. });
  566. // 导出YOLO格式标注
  567. $('#exportBtn').on('click', function() {
  568. // YOLO格式: class x_center y_center width height (均为归一化)
  569. let lines = boxes.map(box => {
  570. const x_center = (box.x + box.w / 2) / imgWidth;
  571. const y_center = (box.y + box.h / 2) / imgHeight;
  572. const w = box.w / imgWidth;
  573. const h = box.h / imgHeight;
  574. return `${box.classId} ${x_center} ${y_center} ${w} ${h}`;
  575. });
  576. const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
  577. const a = document.createElement('a');
  578. a.href = URL.createObjectURL(blob);
  579. a.download = 'labels.txt';
  580. a.click();
  581. });
  582. // 清空所有标记按钮
  583. $('#clearBoxesBtn').on('click', function() {
  584. boxes = [];
  585. drawAll();
  586. updateBoxesList();
  587. });
  588. // 缩放按钮事件
  589. $('#zoomInBtn').on('click', function() {
  590. scale = Math.min(maxScale, scale * 1.2);
  591. drawAll();
  592. });
  593. $('#zoomOutBtn').on('click', function() {
  594. scale = Math.max(minScale, scale / 1.2);
  595. drawAll();
  596. });
  597. $('#zoomResetBtn').on('click', function() {
  598. scale = 1.0;
  599. offsetX = 0;
  600. offsetY = 0;
  601. drawAll();
  602. });
  603. // 鼠标滚轮缩放
  604. $('#canvas').on('wheel', function(e) {
  605. e.preventDefault();
  606. let mouseX = e.offsetX, mouseY = e.offsetY;
  607. // 以鼠标为中心缩放
  608. let {x: imgX, y: imgY} = toImageCoord(mouseX, mouseY);
  609. let oldScale = scale;
  610. if (e.originalEvent.deltaY < 0) {
  611. scale = Math.min(maxScale, scale * 1.1);
  612. } else {
  613. scale = Math.max(minScale, scale / 1.1);
  614. }
  615. // 修正:缩放后让imgX、imgY依然在(mouseX, mouseY)位置
  616. let domW = $('#canvas').width();
  617. let domH = $('#canvas').height();
  618. let ratioX = imgWidth / domW;
  619. let ratioY = imgHeight / domH;
  620. offsetX = (mouseX * ratioX) / scale - imgX;
  621. offsetY = (mouseY * ratioY) / scale - imgY;
  622. drawAll();
  623. });
  624. // 禁止右键菜单弹出
  625. $('#canvas').on('contextmenu', function(e) { e.preventDefault(); });
  626. // 鼠标移出canvas时隐藏坐标
  627. $('#canvas').on('mouseleave', function() {
  628. $('#coordTip').hide();
  629. });
  630. });
  631. </script>
  632. </body>
  633. </html>