index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. <template>
  2. <div class="canvas-container" ref="canvasContainer">
  3. <div class="canvas float-left" ref="yfcontainer" v-loading="loading">
  4. <!-- 画布 -->
  5. <canvas
  6. :style="{
  7. width: canvasSize.width + 'px',
  8. height: canvasSize.height + 'px',
  9. }"
  10. canvas-id="yfCanvas"
  11. id="yfCanvas"
  12. ></canvas>
  13. <!-- 文本标签 -->
  14. <!-- <div class="element-layer">
  15. <span
  16. class="label-link cursor"
  17. v-for="tag in rectangles"
  18. @click="deleteSpan($event, tag.id)"
  19. :key="tag.id"
  20. :style="linkStyle(tag)"
  21. >
  22. <span class="delete" @click.stop="deleteSpan($event, tag.id)">✖</span>
  23. </span>
  24. </div> -->
  25. </div>
  26. </div>
  27. </template>
  28. <script>
  29. import Panel from './comps/panel.vue';
  30. import Tools from './comps/tools.vue';
  31. import Annotation from './comps/Annotation.vue';
  32. let myRatio = 1;
  33. export default {
  34. props: {
  35. markItem: {
  36. type: Object,
  37. default: () => ({
  38. addr: 'https://bigdata-image.oss-cn-hangzhou.aliyuncs.com/Basics/cbd/666000000000001/2025/7/11/001.jpg',
  39. mark: [],
  40. is_mark: 0,
  41. label:
  42. "[{'71': [480, 47, 520, 91, 1.0]}, {'71': [513, 102, 545, 155, 1.0]}, {'71': [690, 238, 740, 272, 1.0]}, {'71': [298, 471, 335, 514, 1.0]}, {'71': [250, 379, 300, 407, 1.0]}, {'71': [106, 40, 143, 89, 1.0]}, {'71': [46, 12, 124, 58, 0.99]}, {'71': [2, 90, 41, 133, 0.98]}, {'71': [767, 189, 799, 237, 0.98]}, {'71': [721, 175, 758, 216, 0.92]}, {'71': [685, 132, 727, 166, 0.35]}]",
  43. }),
  44. },
  45. isInitFullScreen: {
  46. type: Boolean,
  47. default: false,
  48. },
  49. reIdentify: {
  50. type: Boolean,
  51. default: true,
  52. },
  53. title: {
  54. type: String,
  55. default: '目录',
  56. },
  57. typeName: {
  58. type: Number,
  59. default: 1, // 1: 测报灯 2:病虫害可视监测 3:吸虫塔 4:孢子仪
  60. },
  61. },
  62. watch: {
  63. markItem: {
  64. handler(newVal) {
  65. console.log('markItem变化:', newVal, this.ctx);
  66. // 当markItem变化时,重新加载图片和标注
  67. if (!newVal.addr && this.ctx) {
  68. this.clearCanvas();
  69. return;
  70. }
  71. if (this.ctx) {
  72. this.loadImage();
  73. }
  74. },
  75. deep: true,
  76. },
  77. },
  78. components: {
  79. Panel,
  80. Tools,
  81. Annotation,
  82. },
  83. name: 'Draw',
  84. computed: {
  85. inputStyle() {
  86. return {
  87. left: `${this.inputPosition.x}px`,
  88. top: `${this.inputPosition.y}px`,
  89. };
  90. },
  91. canvasStyle() {
  92. return {
  93. cursor: this.mode === 'draw' ? 'crosshair' : 'grab',
  94. };
  95. },
  96. fullScreenHtml() {
  97. return this.$refs.canvasContainer;
  98. },
  99. },
  100. data() {
  101. return {
  102. imgTimer: false, // 图片时间戳
  103. ctx: null,
  104. scale: 1, // 画布的缩放
  105. imageScale: 1, // 图片的原始缩放
  106. markScale: 1, // 绘制的缩放
  107. baseScale: 0.31,
  108. offsetX: 0,
  109. offsetY: 0,
  110. mode: 'drag', // 'draw' / 'drag'
  111. modeLabels: {
  112. draw: 'D绘制',
  113. drag: 'S拖拽',
  114. select: '空格选择',
  115. },
  116. isDragging: false,
  117. isDrawing: false,
  118. lastX: 0,
  119. lastY: 0,
  120. startX: 0,
  121. startY: 0,
  122. rectangles: [],
  123. currentRect: null,
  124. selectedRect: null,
  125. backgroundImage: null,
  126. imageLoaded: false,
  127. originalImageSize: { width: 0, height: 0 },
  128. isFullScreen: false,
  129. imageRect: { x: 0, y: 0, width: 0, height: 0 }, // 存储图片显示区域
  130. showInput: false,
  131. currentLabel: '',
  132. inputPosition: { x: 0, y: 0 },
  133. currentRectId: null,
  134. lastImageScale: 1,
  135. loading: true,
  136. isMoving: false,
  137. colorMap: {}, // 用于存储文本和颜色的映射关系
  138. tempColor: '#FFFF00', // 绘制时黄色半透明
  139. finalColor: '#FF0000', // 完成后红色半透明
  140. changeLeftActive: false,
  141. changeRightActive: false,
  142. colorIndex: 0,
  143. originDataLength: 0, // 用于记录原始数据长度
  144. pestLibrary: {},
  145. canvasSize: { width: 310, height: 279 },
  146. };
  147. },
  148. created() {
  149. this.getAllPestList();
  150. },
  151. mounted() {
  152. // console.log('初始化markItem:', this.markItem)
  153. uni.getSystemInfo({
  154. success: (res) => {
  155. this.canvasSize.width = res.windowWidth - (24 * 2 + 16 * 2);
  156. },
  157. });
  158. this.$nextTick(() => {});
  159. },
  160. beforeDestroy() {},
  161. methods: {
  162. // 切换上一个下一个
  163. changeItem(type) {
  164. this.$emit('changeItem', { type: type, isFullScreen: this.isFullScreen });
  165. },
  166. changeMode(modeType) {
  167. this.mode = modeType;
  168. },
  169. resetPest() {
  170. // 通知业务上层处理逻辑
  171. this.exitFullScreen();
  172. this.$emit('resetPest');
  173. },
  174. // 查看原图
  175. checkImagePreview() {
  176. this.$refs.imgPreview.click();
  177. },
  178. linkStyle(tag) {
  179. let x = Math.min(tag.startX, tag.endX);
  180. let y = Math.min(tag.startY, tag.endY);
  181. const top = y * this.scale + this.offsetY - 20 + 'px';
  182. const left = x * this.scale + this.offsetX + 'px';
  183. return {
  184. top,
  185. left,
  186. color: tag.color,
  187. 'pointer-events': this.isMoving ? 'none' : 'auto',
  188. };
  189. },
  190. initCanvas() {
  191. this.ctx = uni.createCanvasContext('yfCanvas', this);
  192. console.log(this.ctx);
  193. this.loadImage();
  194. },
  195. loadImage() {
  196. if (!this.markItem.addr) {
  197. this.loading = false;
  198. return;
  199. }
  200. uni.getImageInfo({
  201. src: this.markItem.addr,
  202. success: (res) => {
  203. this.imageLoaded = true;
  204. this.backgroundImage = res;
  205. this.originalImageSize = {
  206. width: this.backgroundImage.width,
  207. height: this.backgroundImage.height,
  208. };
  209. this.rectangles = []; // 清除之前的标注
  210. this.selectedRect = null;
  211. this.adjustImagePosition();
  212. this.initMarkData();
  213. this.draw();
  214. this.loading = false;
  215. },
  216. fail: (err) => {
  217. this.loading = false;
  218. this.$message.error('图片加载失败');
  219. },
  220. });
  221. },
  222. adjustImagePosition() {
  223. if (!this.imageLoaded) return;
  224. const canvas = this.canvasSize;
  225. const canvasAspect = canvas.width / canvas.height;
  226. const imageAspect =
  227. this.originalImageSize.width / this.originalImageSize.height;
  228. // 计算等比缩放的尺寸
  229. if (imageAspect > canvasAspect) {
  230. this.imageScale = canvas.width / this.originalImageSize.width;
  231. } else {
  232. this.imageScale = canvas.height / this.originalImageSize.height;
  233. }
  234. // this.imageScale = 0.8
  235. // 根据图片的宽度来定义缩放比例
  236. if (this.originalImageSize.width >= 5000) {
  237. this.baseScale = 0.25;
  238. } else if (this.originalImageSize.width >= 4000) {
  239. this.baseScale = 0.31;
  240. } else if (this.originalImageSize.width < 4000) {
  241. this.baseScale = 0.4;
  242. }
  243. // 用来处理图片等比例显示
  244. this.markScale = 1 - (this.baseScale - this.imageScale) / this.baseScale;
  245. // 窗口大小改变的时候使用
  246. myRatio = this.imageScale / this.lastImageScale;
  247. this.lastImageScale = this.imageScale;
  248. // 计算图片显示区域
  249. this.imageRect = {
  250. x: 0,
  251. y: 0,
  252. width: this.originalImageSize.width * this.imageScale,
  253. height: this.originalImageSize.height * this.imageScale,
  254. };
  255. // 初始位置居中
  256. this.scale = 1;
  257. this.offsetX = (canvas.width - this.imageRect.width) / 2;
  258. this.offsetY = (canvas.height - this.imageRect.height) / 2;
  259. },
  260. isPointInImage(x, y) {
  261. return (
  262. x >= 0 &&
  263. x <= this.imageRect.width &&
  264. y >= 0 &&
  265. y <= this.imageRect.height
  266. );
  267. },
  268. constrainRect(rect) {
  269. // 确保矩形完全在图片区域内
  270. const startX = Math.max(0, Math.min(this.imageRect.width, rect.startX));
  271. const startY = Math.max(0, Math.min(this.imageRect.height, rect.startY));
  272. const endX = Math.max(0, Math.min(this.imageRect.width, rect.endX));
  273. const endY = Math.max(0, Math.min(this.imageRect.height, rect.endY));
  274. return {
  275. ...rect,
  276. startX,
  277. startY,
  278. endX,
  279. endY,
  280. };
  281. },
  282. handleLabelChange() {
  283. this.draw();
  284. },
  285. getRectAt(x, y) {
  286. // 从后往前检查,这样最后绘制的矩形会优先被选中
  287. for (let i = this.rectangles.length - 1; i >= 0; i--) {
  288. const rect = this.rectangles[i];
  289. const left = Math.min(rect.startX, rect.endX);
  290. const right = Math.max(rect.startX, rect.endX);
  291. const top = Math.min(rect.startY, rect.endY);
  292. const bottom = Math.max(rect.startY, rect.endY);
  293. if (x >= left && x <= right && y >= top && y <= bottom) {
  294. return rect;
  295. }
  296. }
  297. return null;
  298. },
  299. deleteSelected() {
  300. if (this.selectedRect) {
  301. this.deleteRect(this.selectedRect.id);
  302. this.selectedRect = null;
  303. }
  304. },
  305. deleteRect(id) {
  306. this.rectangles = this.rectangles.filter((rect) => rect.id !== id);
  307. if (this.selectedRect && this.selectedRect.id === id) {
  308. this.selectedRect = null;
  309. }
  310. this.showInput = false;
  311. this.currentRectId = null;
  312. this.draw();
  313. },
  314. deleteSpan(e, id) {
  315. e.preventDefault();
  316. e.stopPropagation();
  317. this.isDrawing = false;
  318. this.deleteRect(id);
  319. },
  320. clearCanvas() {
  321. this.loading = false;
  322. this.imageLoaded = false;
  323. this.rectangles = [];
  324. this.selectedRect = null;
  325. this.showInput = false;
  326. this.currentRectId = null;
  327. const canvas = this.canvasSize;
  328. const ctx = this.ctx;
  329. // 清除画布
  330. ctx.clearRect(0, 0, canvas.width, canvas.height);
  331. },
  332. // 修改后的 draw 方法
  333. draw() {
  334. const canvas = this.canvasSize;
  335. const ctx = this.ctx;
  336. // 清除画布
  337. ctx.clearRect(0, 0, canvas.width, canvas.height);
  338. console.log(this.offsetX, this.offsetY);
  339. // 保存当前状态
  340. ctx.save();
  341. // 应用变换(缩放和平移)
  342. ctx.translate(this.offsetX, this.offsetY);
  343. ctx.scale(this.scale, this.scale);
  344. // 绘制背景图片(不再除以this.scale)
  345. if (this.imageLoaded && this.backgroundImage) {
  346. const displayWidth = this.originalImageSize.width * this.imageScale;
  347. const displayHeight = this.originalImageSize.height * this.imageScale;
  348. // ctx.imageSmoothingEnabled = true
  349. // ctx.imageSmoothingQuality = 'high'
  350. ctx.drawImage(
  351. this.backgroundImage.path,
  352. 0,
  353. 0,
  354. displayWidth,
  355. displayHeight
  356. );
  357. }
  358. console.log('绘制::', this.rectangles);
  359. // 绘制所有已保存的矩形(添加约束)
  360. this.rectangles.forEach((rect) => {
  361. const constrainedRect = this.constrainRect(rect);
  362. this.drawRectangle(constrainedRect, rect === this.selectedRect);
  363. });
  364. // 绘制当前正在绘制的矩形(添加约束)
  365. if (this.currentRect) {
  366. const constrainedRect = this.constrainRect(this.currentRect);
  367. this.drawRectangle(constrainedRect, false);
  368. }
  369. // 恢复状态
  370. ctx.restore();
  371. ctx.draw();
  372. },
  373. drawRectangle(rect, isSelected) {
  374. const ctx = this.ctx;
  375. const x = Math.min(rect.startX, rect.endX);
  376. const y = Math.min(rect.startY, rect.endY);
  377. const width = Math.abs(rect.endX - rect.startX);
  378. const height = Math.abs(rect.endY - rect.startY);
  379. // 绘制矩形填充
  380. ctx.strokeStyle = rect.color;
  381. ctx.lineWidth = 2 / this.scale;
  382. ctx.strokeRect(x, y, width, height);
  383. // 绘制标签文本
  384. if (rect.text) {
  385. ctx.fillStyle = rect.color;
  386. ctx.font = `${14 / this.scale}px Arial`;
  387. ctx.fillText(rect.text, x + 12 / this.scale, y - 6 / this.scale);
  388. }
  389. },
  390. // 获取数据
  391. getData() {
  392. const data = this.rectangles.map((rect) => {
  393. return {
  394. startX: parseFloat((rect.startX / this.markScale).toFixed(2)),
  395. startY: parseFloat((rect.startY / this.markScale).toFixed(2)),
  396. width: parseFloat(
  397. (Math.abs(rect.startX - rect.endX) / this.markScale).toFixed(2)
  398. ),
  399. height: parseFloat(
  400. (Math.abs(rect.startY - rect.endY) / this.markScale).toFixed(2)
  401. ),
  402. text: rect.text,
  403. };
  404. });
  405. this.originDataLength = data.length;
  406. return data;
  407. // return JSON.stringify(data)
  408. },
  409. // 处理渲染数据
  410. async initMarkData() {
  411. const colorList = [
  412. '#FF0000',
  413. '#00FFC2',
  414. '#FF00A8',
  415. '#120080',
  416. '#BDFF00',
  417. '#FFB800',
  418. '#5E8000',
  419. '#FF5C00',
  420. '#00FF75',
  421. '#00F0FF',
  422. '#0094FF',
  423. '#FFFFFF',
  424. '#007880',
  425. '#00C2FF',
  426. '#C74C4C',
  427. '#EB00FF',
  428. ];
  429. if (this.markItem.is_mark === 0) {
  430. // 机器标注,取label
  431. let aiLabel = [];
  432. if (this.markItem.label) {
  433. aiLabel = JSON.parse(this.markItem.label.replace(/'/g, '"'));
  434. }
  435. this.rectangles = [];
  436. console.log('00000', aiLabel);
  437. aiLabel.forEach((item, index) => {
  438. for (let key in item) {
  439. const [startX, startY, endX, endY] = item[key];
  440. const text = this.pestLibrary[key];
  441. if (!this.colorMap[text]) {
  442. this.colorMap[text] =
  443. colorList[this.colorIndex % colorList.length];
  444. this.colorIndex += 1;
  445. if (this.colorIndex >= colorList.length) {
  446. this.colorIndex = 0;
  447. }
  448. }
  449. this.rectangles.push({
  450. id: Date.now() + index,
  451. text: text,
  452. startX: startX * this.imageScale,
  453. startY: startY * this.imageScale,
  454. endX: endX * this.imageScale,
  455. endY: endY * this.imageScale,
  456. color: this.colorMap[text],
  457. imageScale: this.imageScale,
  458. });
  459. }
  460. });
  461. this.originDataLength = this.rectangles.length;
  462. } else {
  463. // 人工标注,取mark
  464. console.log('this.colorIndex', this.colorIndex);
  465. this.rectangles = [];
  466. this.markItem.mark.map((item, index) => {
  467. const { startX, startY, width, height, text } = item;
  468. if (!this.colorMap[text]) {
  469. this.colorMap[text] = colorList[this.colorIndex % colorList.length];
  470. this.colorIndex += 1;
  471. if (this.colorIndex >= colorList.length) {
  472. this.colorIndex = 0;
  473. }
  474. }
  475. this.rectangles.push({
  476. id: Date.now() + index,
  477. text: text,
  478. startX: Number(startX) * this.markScale,
  479. startY: Number(startY) * this.markScale,
  480. endX:
  481. Number(startX) * this.markScale + Number(width) * this.markScale,
  482. endY:
  483. Number(startY) * this.markScale + Number(height) * this.markScale,
  484. color: this.colorMap[text],
  485. imageScale: this.imageScale,
  486. });
  487. });
  488. this.originDataLength = this.rectangles.length;
  489. }
  490. console.log('原始rectangles', this.rectangles);
  491. },
  492. // 保存标注
  493. saveLabel() {
  494. this.$emit('saveLabel', this.getData());
  495. },
  496. async getAllPestList() {
  497. const res = await this.$myRequest({
  498. url: '/api/api_gateway?method=forecast.pest_info.pest_dict',
  499. data: {
  500. type_name: this.typeName,
  501. },
  502. });
  503. // console.log('获取到的病虫害数据', res)
  504. this.pestLibrary = res;
  505. this.initCanvas();
  506. },
  507. },
  508. };
  509. </script>
  510. <style lang="less" scoped>
  511. @primary-color: #018b3f !important;
  512. ::v-deep .el-button--primary {
  513. background-color: @primary-color !important;
  514. }
  515. .canvas-container {
  516. width: 100%;
  517. height: 100%;
  518. position: relative;
  519. background: #ffffff;
  520. padding-top: 10px;
  521. .virtual-canvas {
  522. /* 2025安全隐藏方案 */
  523. position: absolute !important;
  524. clip: rect(0 0 0 0) !important;
  525. pointer-events: none !important;
  526. opacity: 0 !important;
  527. z-index: -9999 !important;
  528. }
  529. .canvas {
  530. width: 100%;
  531. height: 100%;
  532. overflow: hidden;
  533. background-color: #ffffff;
  534. border: 1px solid #e4e7ed;
  535. box-sizing: border-box;
  536. position: relative;
  537. }
  538. .canvas-timer {
  539. position: absolute;
  540. left: 16px;
  541. top: 16px;
  542. z-index: 2;
  543. color: #ffffff;
  544. font-size: 23px;
  545. }
  546. .panel {
  547. width: 30%;
  548. height: calc(100% - 60px);
  549. background: #ffffff;
  550. padding-left: 10px;
  551. box-sizing: border-box;
  552. position: relative;
  553. z-index: 3;
  554. .save {
  555. height: 60px;
  556. button {
  557. margin: 14px;
  558. }
  559. }
  560. }
  561. .controls {
  562. // padding: 3px 15px;
  563. width: 70%;
  564. height: 64px;
  565. box-sizing: border-box;
  566. position: relative;
  567. z-index: 3;
  568. // background: rgba(0, 0, 0, 0.3);
  569. }
  570. .img-timer {
  571. position: absolute;
  572. left: 0;
  573. top: 22px;
  574. }
  575. .info {
  576. // width: 100%;
  577. // color: #ffffff;
  578. // overflow: hidden;
  579. // text-align: center;
  580. .tools {
  581. // width: 365px;
  582. height: 64px;
  583. // padding: 0 10%;
  584. display: flex;
  585. align-items: center;
  586. justify-content: center;
  587. // background: rgba(0, 0, 0, 0.6);
  588. border-radius: 4px;
  589. gap: 10px;
  590. // margin: 0 auto;
  591. color: rgba(0, 0, 0, 0.6);
  592. .active {
  593. background: @primary-color;
  594. color: #ffffff;
  595. }
  596. .split {
  597. color: #b9b9b9;
  598. margin: 0 5px;
  599. font-size: 14px;
  600. }
  601. }
  602. .float-right {
  603. width: 30%;
  604. }
  605. .img-preview {
  606. display: none;
  607. width: 20px;
  608. position: absolute;
  609. }
  610. }
  611. .info span {
  612. white-space: nowrap;
  613. .active {
  614. color: @primary-color;
  615. margin-right: 5px;
  616. }
  617. }
  618. .info button {
  619. font-size: 26px;
  620. padding: 2px 4px;
  621. background: none;
  622. border: none;
  623. border-radius: 3px;
  624. cursor: pointer;
  625. white-space: nowrap;
  626. color: #434343;
  627. }
  628. .info button:disabled {
  629. cursor: not-allowed;
  630. }
  631. .element-layer {
  632. .label-link {
  633. position: absolute;
  634. z-index: 2;
  635. user-select: none;
  636. pointer-events: auto;
  637. }
  638. }
  639. }
  640. </style>