index.vue 17 KB

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