| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 |
- <template>
- <div class="canvas-container" ref="canvasContainer">
- <div
- class="canvas float-left"
- ref="yfcontainer"
- v-loading="loading"
- >
- <!-- 画布 -->
- <canvas
- :style="{width: canvasSize.width + 'px', height: canvasSize.height + 'px'}"
- canvas-id="yfCanvas"
- id="yfCanvas"
- ></canvas>
- <!-- 文本标签 -->
- <!-- <div class="element-layer">
- <span
- class="label-link cursor"
- v-for="tag in rectangles"
- @click="deleteSpan($event, tag.id)"
- :key="tag.id"
- :style="linkStyle(tag)"
- >
- <span class="delete" @click.stop="deleteSpan($event, tag.id)">✖</span>
- </span>
- </div> -->
- </div>
- </div>
- </template>
- <script>
- import Panel from './comps/panel.vue'
- import Tools from './comps/tools.vue'
- import Annotation from './comps/Annotation.vue'
- let myRatio = 1
- export default {
- props: {
- markItem: {
- type: Object,
- default: () => ({
- addr: 'https://bigdata-image.oss-cn-hangzhou.aliyuncs.com/Basics/cbd/666000000000001/2025/7/11/001.jpg',
- mark:[],
- is_mark:0,
- 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]}]",
- })
- },
- isInitFullScreen: {
- type: Boolean,
- default: false
- },
- reIdentify: {
- type: Boolean,
- default: true
- },
- title: {
- type: String,
- default: '目录'
- },
- typeName: {
- type: Number,
- default: 1 // 1: 测报灯 2:病虫害可视监测 3:吸虫塔 4:孢子仪
- }
- },
- watch: {
- markItem: {
- handler(newVal) {
- console.log('markItem变化:', newVal, this.ctx)
- // 当markItem变化时,重新加载图片和标注
- if (!newVal.addr && this.ctx) {
- this.clearCanvas()
- return
- }
- if (this.ctx) {
- this.loadImage()
- }
- },
- deep: true
- }
- },
- components: {
- Panel,
- Tools,
- Annotation
- },
- name: 'Draw',
- computed: {
- inputStyle() {
- return {
- left: `${this.inputPosition.x}px`,
- top: `${this.inputPosition.y}px`
- }
- },
- canvasStyle() {
- return {
- cursor: this.mode === 'draw' ? 'crosshair' : 'grab'
- }
- },
- fullScreenHtml() {
- return this.$refs.canvasContainer
- }
-
- },
- data() {
- return {
- imgTimer: false, // 图片时间戳
- ctx: null,
- scale: 1, // 画布的缩放
- imageScale: 1, // 图片的原始缩放
- markScale: 1, // 绘制的缩放
- baseScale: 0.31,
- offsetX: 0,
- offsetY: 0,
- mode: 'drag', // 'draw' / 'drag'
- modeLabels: {
- draw: 'D绘制',
- drag: 'S拖拽',
- select: '空格选择'
- },
- isDragging: false,
- isDrawing: false,
- lastX: 0,
- lastY: 0,
- startX: 0,
- startY: 0,
- rectangles: [],
- currentRect: null,
- selectedRect: null,
- backgroundImage: null,
- imageLoaded: false,
- originalImageSize: { width: 0, height: 0 },
- isFullScreen: false,
- imageRect: { x: 0, y: 0, width: 0, height: 0 }, // 存储图片显示区域
- showInput: false,
- currentLabel: '',
- inputPosition: { x: 0, y: 0 },
- currentRectId: null,
- lastImageScale: 1,
- loading: true,
- isMoving: false,
- colorMap: {}, // 用于存储文本和颜色的映射关系
- tempColor: '#FFFF00', // 绘制时黄色半透明
- finalColor: '#FF0000', // 完成后红色半透明
- changeLeftActive: false,
- changeRightActive: false,
- colorIndex: 0,
- originDataLength: 0, // 用于记录原始数据长度
- pestLibrary: {},
- canvasSize:{width:310,height:279}
- }
- },
- created() {
- this.getAllPestList()
- },
- mounted() {
- // console.log('初始化markItem:', this.markItem)
-
- uni.getSystemInfo({
- success: (res) => {
- this.canvasSize.width = res.windowWidth-(24*2+16*2)
- }
- })
- this.$nextTick(()=>{
-
- })
- },
- beforeDestroy() {
-
- },
- methods: {
- // 切换上一个下一个
- changeItem(type) {
-
- this.$emit('changeItem', { type: type, isFullScreen: this.isFullScreen })
- },
- changeMode(modeType) {
- this.mode = modeType
- },
- resetPest() {
- // 通知业务上层处理逻辑
- this.exitFullScreen()
- this.$emit('resetPest')
- },
- // 查看原图
- checkImagePreview() {
- this.$refs.imgPreview.click()
- },
- linkStyle(tag) {
- let x = Math.min(tag.startX, tag.endX)
- let y = Math.min(tag.startY, tag.endY)
- const top = y * this.scale + this.offsetY - 20 + 'px'
- const left = x * this.scale + this.offsetX + 'px'
- return {
- top,
- left,
- color: tag.color,
- 'pointer-events': this.isMoving ? 'none' : 'auto'
- }
- },
- initCanvas() {
- this.ctx = uni.createCanvasContext('yfCanvas', this)
- console.log(this.ctx)
- this.loadImage()
-
- },
- loadImage() {
- if (!this.markItem.addr) {
- this.loading = false
- return
- }
- uni.getImageInfo({
- src: this.markItem.addr,
- success: (res) => {
- this.imageLoaded = true
- this.backgroundImage = res
- this.originalImageSize = {
- width: this.backgroundImage.width,
- height: this.backgroundImage.height
- }
- this.rectangles = [] // 清除之前的标注
- this.selectedRect = null
- this.adjustImagePosition()
- this.initMarkData()
- this.draw()
- this.loading = false
- },
- fail: (err) => {
- this.loading = false
- this.$message.error('图片加载失败')
- }
- })
- },
- adjustImagePosition() {
- if (!this.imageLoaded) return
- const canvas = this.canvasSize
- const canvasAspect = canvas.width / canvas.height
- const imageAspect = this.originalImageSize.width / this.originalImageSize.height
- // 计算等比缩放的尺寸
- if (imageAspect > canvasAspect) {
- this.imageScale = canvas.width / this.originalImageSize.width
- } else {
- this.imageScale = canvas.height / this.originalImageSize.height
- }
- // this.imageScale = 0.8
- // 根据图片的宽度来定义缩放比例
- if (this.originalImageSize.width >= 5000) {
- this.baseScale = 0.25
- } else if (this.originalImageSize.width >= 4000) {
- this.baseScale = 0.31
- } else if (this.originalImageSize.width < 4000) {
- this.baseScale = 0.4
- }
- // 用来处理图片等比例显示
- this.markScale = 1 - (this.baseScale - this.imageScale) / this.baseScale
- // 窗口大小改变的时候使用
- myRatio = this.imageScale / this.lastImageScale
- this.lastImageScale = this.imageScale
- // 计算图片显示区域
- this.imageRect = {
- x: 0,
- y: 0,
- width: this.originalImageSize.width * this.imageScale,
- height: this.originalImageSize.height * this.imageScale
- }
- // 初始位置居中
- this.scale = 1
- this.offsetX = (canvas.width - this.imageRect.width) / 2
- this.offsetY = (canvas.height - this.imageRect.height) / 2
- },
- isPointInImage(x, y) {
- return x >= 0 && x <= this.imageRect.width && y >= 0 && y <= this.imageRect.height
- },
- constrainRect(rect) {
- // 确保矩形完全在图片区域内
- const startX = Math.max(0, Math.min(this.imageRect.width, rect.startX))
- const startY = Math.max(0, Math.min(this.imageRect.height, rect.startY))
- const endX = Math.max(0, Math.min(this.imageRect.width, rect.endX))
- const endY = Math.max(0, Math.min(this.imageRect.height, rect.endY))
- return {
- ...rect,
- startX,
- startY,
- endX,
- endY
- }
- },
- handleLabelChange() {
- this.draw()
- },
- getRectAt(x, y) {
- // 从后往前检查,这样最后绘制的矩形会优先被选中
- for (let i = this.rectangles.length - 1; i >= 0; i--) {
- const rect = this.rectangles[i]
- const left = Math.min(rect.startX, rect.endX)
- const right = Math.max(rect.startX, rect.endX)
- const top = Math.min(rect.startY, rect.endY)
- const bottom = Math.max(rect.startY, rect.endY)
- if (x >= left && x <= right && y >= top && y <= bottom) {
- return rect
- }
- }
- return null
- },
- deleteSelected() {
- if (this.selectedRect) {
- this.deleteRect(this.selectedRect.id)
- this.selectedRect = null
- }
- },
- deleteRect(id) {
- this.rectangles = this.rectangles.filter(rect => rect.id !== id)
- if (this.selectedRect && this.selectedRect.id === id) {
- this.selectedRect = null
- }
- this.showInput = false
- this.currentRectId = null
- this.draw()
- },
- deleteSpan(e, id) {
- e.preventDefault()
- e.stopPropagation()
- this.isDrawing = false
- this.deleteRect(id)
- },
- clearCanvas() {
- this.loading = false
- this.imageLoaded = false
- this.rectangles = []
- this.selectedRect = null
- this.showInput = false
- this.currentRectId = null
- const canvas = this.canvasSize
- const ctx = this.ctx
- // 清除画布
- ctx.clearRect(0, 0, canvas.width, canvas.height)
- },
- // 修改后的 draw 方法
- draw() {
- const canvas = this.canvasSize
- const ctx = this.ctx
- // 清除画布
- ctx.clearRect(0, 0, canvas.width, canvas.height)
- console.log(this.offsetX, this.offsetY)
- // 保存当前状态
- ctx.save()
-
- // 应用变换(缩放和平移)
- ctx.translate(this.offsetX, this.offsetY)
- ctx.scale(this.scale, this.scale)
-
- // 绘制背景图片(不再除以this.scale)
- if (this.imageLoaded && this.backgroundImage) {
- const displayWidth = this.originalImageSize.width * this.imageScale
- const displayHeight = this.originalImageSize.height * this.imageScale
- // ctx.imageSmoothingEnabled = true
- // ctx.imageSmoothingQuality = 'high'
- ctx.drawImage(this.backgroundImage.path, 0, 0, displayWidth, displayHeight)
-
- }
- console.log('绘制::',this.rectangles)
- // 绘制所有已保存的矩形(添加约束)
- this.rectangles.forEach(rect => {
- const constrainedRect = this.constrainRect(rect)
- this.drawRectangle(constrainedRect, rect === this.selectedRect)
- })
- // 绘制当前正在绘制的矩形(添加约束)
- if (this.currentRect) {
- const constrainedRect = this.constrainRect(this.currentRect)
- this.drawRectangle(constrainedRect, false)
- }
-
- // 恢复状态
- ctx.restore()
- ctx.draw()
- },
- drawRectangle(rect, isSelected) {
- const ctx = this.ctx
- const x = Math.min(rect.startX, rect.endX)
- const y = Math.min(rect.startY, rect.endY)
- const width = Math.abs(rect.endX - rect.startX)
- const height = Math.abs(rect.endY - rect.startY)
- // 绘制矩形填充
- ctx.strokeStyle = rect.color
- ctx.lineWidth = 2 / this.scale
- ctx.strokeRect(x, y, width, height)
- // 绘制标签文本
- if (rect.text) {
- ctx.fillStyle = rect.color
- ctx.font = `${14 / this.scale}px Arial`
- ctx.fillText(rect.text, x + 12 / this.scale, y - 6 / this.scale)
- }
-
-
- },
- // 获取数据
- getData() {
- const data = this.rectangles.map(rect => {
- return {
- startX: parseFloat((rect.startX / this.markScale).toFixed(2)),
- startY: parseFloat((rect.startY / this.markScale).toFixed(2)),
- width: parseFloat((Math.abs(rect.startX - rect.endX) / this.markScale).toFixed(2)),
- height: parseFloat((Math.abs(rect.startY - rect.endY) / this.markScale).toFixed(2)),
- text: rect.text
- }
- })
- this.originDataLength = data.length
- return data
- // return JSON.stringify(data)
- },
- // 处理渲染数据
- async initMarkData() {
-
- const colorList = [
- '#FF0000',
- '#00FFC2',
- '#FF00A8',
- '#120080',
- '#BDFF00',
- '#FFB800',
- '#5E8000',
- '#FF5C00',
- '#00FF75',
- '#00F0FF',
- '#0094FF',
- '#FFFFFF',
- '#007880',
- '#00C2FF',
- '#C74C4C',
- '#EB00FF'
- ]
- if (this.markItem.is_mark === 0) {
- // 机器标注,取label
- let aiLabel = []
- if (this.markItem.label) {
- aiLabel = JSON.parse(this.markItem.label.replace(/'/g, '"'))
- }
- this.rectangles = []
- console.log('00000',aiLabel)
- aiLabel.forEach((item, index) => {
- for (let key in item) {
- const [startX, startY, endX, endY] = item[key]
- const text = this.pestLibrary[key]
- if (!this.colorMap[text]) {
- this.colorMap[text] = colorList[this.colorIndex % colorList.length]
- this.colorIndex += 1
- if (this.colorIndex >= colorList.length) {
- this.colorIndex = 0
- }
- }
- this.rectangles.push({
- id: Date.now() + index,
- text: text,
- startX: startX * this.imageScale,
- startY: startY * this.imageScale,
- endX: endX * this.imageScale,
- endY: endY * this.imageScale,
- color: this.colorMap[text],
- imageScale: this.imageScale
- })
- }
- })
- this.originDataLength = this.rectangles.length
- } else {
- // 人工标注,取mark
- console.log('this.colorIndex', this.colorIndex)
- this.rectangles = []
- this.markItem.mark.map((item, index) => {
- const { startX, startY, width, height, text } = item
- if (!this.colorMap[text]) {
- this.colorMap[text] = colorList[this.colorIndex % colorList.length]
- this.colorIndex += 1
- if (this.colorIndex >= colorList.length) {
- this.colorIndex = 0
- }
- }
- this.rectangles.push({
- id: Date.now() + index,
- text: text,
- startX: Number(startX) * this.markScale,
- startY: Number(startY) * this.markScale,
- endX: Number(startX) * this.markScale + Number(width) * this.markScale,
- endY: Number(startY) * this.markScale + Number(height) * this.markScale,
- color: this.colorMap[text],
- imageScale: this.imageScale
- })
- })
- this.originDataLength = this.rectangles.length
- }
- console.log('原始rectangles', this.rectangles)
- },
- // 保存标注
- saveLabel() {
- this.$emit('saveLabel', this.getData())
- },
- async getAllPestList() {
- const res = await this.$myRequest({
- url: '/api/api_gateway?method=forecast.pest_info.pest_dict',
- data: {
- type_name: this.typeName
- }
- })
- // console.log('获取到的病虫害数据', res)
- this.pestLibrary = res
- this.initCanvas()
- },
-
- }
- }
- </script>
- <style lang="less" scoped>
- @primary-color: #018b3f !important;
- /deep/ .el-button--primary {
- background-color: @primary-color !important;
- }
- .canvas-container {
- width: 100%;
- height: 100%;
- position: relative;
- background: #ffffff;
- padding-top: 10px;
- .virtual-canvas {
- /* 2025安全隐藏方案 */
- position: absolute !important;
- clip: rect(0 0 0 0) !important;
- pointer-events: none !important;
- opacity: 0 !important;
- z-index: -9999 !important;
- }
- .canvas {
- width: 100%;
- height: 100%;
- overflow: hidden;
- background-color: #ffffff;
- border: 1px solid #e4e7ed;
- box-sizing: border-box;
- position: relative;
- }
- .canvas-timer {
- position: absolute;
- left: 16px;
- top: 16px;
- z-index: 2;
- color: #ffffff;
- font-size: 23px;
- }
- .panel {
- width: 30%;
- height: calc(100% - 60px);
- background: #ffffff;
- padding-left: 10px;
- box-sizing: border-box;
- position: relative;
- z-index: 3;
- .save {
- height: 60px;
- button {
- margin: 14px;
- }
- }
- }
- .controls {
- // padding: 3px 15px;
- width: 70%;
- height: 64px;
- box-sizing: border-box;
- position: relative;
- z-index: 3;
- // background: rgba(0, 0, 0, 0.3);
- }
- .img-timer {
- position: absolute;
- left: 0;
- top: 22px;
- }
- .info {
- // width: 100%;
- // color: #ffffff;
- // overflow: hidden;
- // text-align: center;
- .tools {
- // width: 365px;
- height: 64px;
- // padding: 0 10%;
- display: flex;
- align-items: center;
- justify-content: center;
- // background: rgba(0, 0, 0, 0.6);
- border-radius: 4px;
- gap: 10px;
- // margin: 0 auto;
- color: rgba(0, 0, 0, 0.6);
- .active {
- background: @primary-color;
- color: #ffffff;
- }
- .split {
- color: #b9b9b9;
- margin: 0 5px;
- font-size: 14px;
- }
- }
- .float-right {
- width: 30%;
- }
- .img-preview {
- display: none;
- width: 20px;
- position: absolute;
- }
- }
- .info span {
- white-space: nowrap;
- .active {
- color: @primary-color;
- margin-right: 5px;
- }
- }
- .info button {
- font-size: 26px;
- padding: 2px 4px;
- background: none;
- border: none;
- border-radius: 3px;
- cursor: pointer;
- white-space: nowrap;
- color: #434343;
- }
- .info button:disabled {
- cursor: not-allowed;
- }
- .element-layer {
- .label-link {
- position: absolute;
- z-index: 2;
- user-select: none;
- pointer-events: auto;
- }
- }
- }
- </style>
|