一.实现思路
现在需求是:后端会返回在这张图片上的相对位置,然后前端这边需要在图片上进行标注,就是画个框框圈起来,返回的数据里包括当前框的x,y坐标和图片大小,大体思路就是使用canvas绘制,使用鸿蒙的stack将图片和canvas进行重合,在canvas上进行标注,使他看起来和在图片上是一样的
1.先通过axios进行图片上传
2.传完以后会返回当前需要标注的数据
3.使用canvas进行绘制,绘制的内容包括(框,两行文字)
实现效果:
二.代码
1.进行图片选择
(这里因为支持多张上传,所以有多张绘制,那么canvas的实例就不能是一个,所以这里在上传的时候,每一张图片就创建一次实例,canvas不支持一个实例多次绘制)
Button('选择并上传图片') .position({ x: 100, y: 685 }).onClick(async () => {// 创建 图片选择对象const photoViewPicker = new picker.PhotoViewPicker();// 调用 select 方法,传入选项对象photoViewPicker.select(photoSelectOptions).then(async res => {const context = getContext(this)res.photoUris.forEach((item)=>{// this.str= itemlet settings: RenderingContextSettings = new RenderingContextSettings(true)let context1: CanvasRenderingContext2D = new CanvasRenderingContext2D(settings)let offCanvas: OffscreenCanvas = new OffscreenCanvas(600, 600)this.arr.push({ url:item,context:context1 })// 三、拷贝文件到缓存目录// 将文件保存到缓存目录(只能上传在缓存目录中的文件)const fileType = 'jpg'// 生成一个新的文件名const fileName = Date.now() + '.' + fileType// 通过缓存路径+文件名 拼接出完整的路径const copyFilePath = context.cacheDir + '/' + fileName// 将文件 拷贝到 临时目录const file = fs.openSync(item, fs.OpenMode.READ_ONLY)fs.copyFileSync(file.fd, copyFilePath)// 发送请求this.uploadImg(fileName,context1,settings,offCanvas)})})})
2.上传图片并处理数据
async uploadImg (fileName:string,context:CanvasRenderingContext2D,settings:RenderingContextSettings,offCanvas:OffscreenCanvas){let formData = new FormData()formData.append('file', `internal://cache/${fileName}`)const res:ESObject = awaitaxios.post<string, AxiosResponse<string>, FormData>('你的url', formData, {headers: { 'Content-Type': 'multipart/form-data' ,'X-Auth-Token': token},context: getContext(this),// 上传进度// onUploadProgress: (progressEvent: AxiosProgressEvent): void => {// console.info(fileName,progressEvent && progressEvent.loaded && progressEvent.total ? Math.ceil(progressEvent.loaded / progressEvent.total * 100) + '%' : '0%');// },})// .then((res:AxiosResponse<string>)=>{// console.log(JSON.stringify(res))// }).catch((err:Error)=>{// console.log(JSON.stringify(err))// })const res2:type1 = res.datathis.imgSize = res.data.img_sizeconsole.log(JSON.stringify(res2))// 数据处理res2.boxes_xywh_Relative.forEach((item,index) => {const x = Number(item[0].toFixed(3)) * 3;const y = Number(item[1].toFixed(3)) * 3;const w = Number(item[2].toFixed(3)) * 3;const h = Number(item[3].toFixed(3)) * 3;res2.detection_scores.forEach((score,index1) => {if(index==index1){const formattedScore = Number(score.toFixed(3));res2.detection_classes.forEach((cls) => {// 绘制canvasthis.draw(x, y, w, h, context, settings, offCanvas, formattedScore, cls);});}});});}
3.进行绘制
// 绘制draw(x:number,y:number,w:number,h:number,context:CanvasRenderingContext2D,settings:RenderingContextSettings,offCanvas:OffscreenCanvas,item1?:number,item2?:string) {// context.clearRect(x*100, y*100, w*100, h*100); // 清理画布内容let offContext = offCanvas.getContext("2d", settings)// 框颜色offContext.strokeStyle ='#FF0000'//框宽offContext.lineWidth = 1context.fillStyle = '#FF0000'//字体大小offContext.font = '16vp sans-serif'// 绘制置信度offContext.fillText(item1?.toString(), x*100, (y-0.2)*100)// 绘制detection_classesoffContext.fillText(item2, x*100, y*100)// 绘制标注offContext.strokeRect(x*100, y*100, w*100, h*100)// offContext.strokeRect(40, 40, 200, 150)let image = offCanvas.transferToImageBitmap()context.transferFromImageBitmap(image)this.toDataURL = context.toDataURL("image/png", 0.92)}
4.布局代码
Scroll(){Column(){ForEach(this.arr,(item:type2)=>{Stack(){Image(item.url).width('100%').height('50%').objectFit(ImageFit.Contain)Canvas(item.context)// .margin({left:20,top:20}).width('100%').height('50%').onReady(() => {})}})}}
三.完整代码
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import axios, { AxiosError, AxiosProgressEvent, AxiosResponse, FormData } from '@ohos/axios';
import { componentSnapshot, promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { image } from '@kit.ImageKit';interface type1{detection_classes:Array<string>boxes_xywh_Relative:Array<Array<number>>boxes_xywh_Absolute:Array<Array<number>>detection_scores:Array<number>img_size:Array<number>
}interface type2{context:CanvasRenderingContext2Durl:string
}
const token = '你的token'
// 实例化 选项对象
const photoSelectOptions = new picker.PhotoSelectOptions();// 过滤选择媒体文件类型为IMAGE
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
// 选择媒体文件的最大数目
photoSelectOptions.maxSelectNumber = 3;
@Entry
@Component
struct Page03_uploadImg {@State arr:Array<type2>=[]@State @Watch('draw')content: string = '';@State imgSize:Array<number>=[]@State toDataURL: string = ""// 图片上传 axiosasync uploadImg (fileName:string,context:CanvasRenderingContext2D,settings:RenderingContextSettings,offCanvas:OffscreenCanvas){let formData = new FormData()formData.append('file', `internal://cache/${fileName}`)const res:ESObject = awaitaxios.post<string, AxiosResponse<string>, FormData>('你的url', formData, {headers: { 'Content-Type': 'multipart/form-data' ,'X-Auth-Token': token},context: getContext(this),// 上传进度// onUploadProgress: (progressEvent: AxiosProgressEvent): void => {// console.info(fileName,progressEvent && progressEvent.loaded && progressEvent.total ? Math.ceil(progressEvent.loaded / progressEvent.total * 100) + '%' : '0%');// },})// .then((res:AxiosResponse<string>)=>{// console.log(JSON.stringify(res))// }).catch((err:Error)=>{// console.log(JSON.stringify(err))// })const res2:type1 = res.datathis.imgSize = res.data.img_sizeconsole.log(JSON.stringify(res2))// 数据处理res2.boxes_xywh_Relative.forEach((item,index) => {const x = Number(item[0].toFixed(3)) * 3;const y = Number(item[1].toFixed(3)) * 3;const w = Number(item[2].toFixed(3)) * 3;const h = Number(item[3].toFixed(3)) * 3;res2.detection_scores.forEach((score,index1) => {if(index==index1){const formattedScore = Number(score.toFixed(3));res2.detection_classes.forEach((cls) => {// 绘制canvasthis.draw(x, y, w, h, context, settings, offCanvas, formattedScore, cls);});}});});}// 绘制draw(x:number,y:number,w:number,h:number,context:CanvasRenderingContext2D,settings:RenderingContextSettings,offCanvas:OffscreenCanvas,item1?:number,item2?:string) {// context.clearRect(x*100, y*100, w*100, h*100); // 清理画布内容let offContext = offCanvas.getContext("2d", settings)// 框颜色offContext.strokeStyle ='#FF0000'//框宽offContext.lineWidth = 1context.fillStyle = '#FF0000'//字体大小offContext.font = '16vp sans-serif'// 绘制置信度offContext.fillText(item1?.toString(), x*100, (y-0.2)*100)// 绘制detection_classesoffContext.fillText(item2, x*100, y*100)// 绘制标注offContext.strokeRect(x*100, y*100, w*100, h*100)// offContext.strokeRect(40, 40, 200, 150)let image = offCanvas.transferToImageBitmap()context.transferFromImageBitmap(image)this.toDataURL = context.toDataURL("image/png", 0.92)}build() {Column() {Scroll(){Column(){ForEach(this.arr,(item:type2)=>{Stack(){Image(item.url).width('100%').height('50%').objectFit(ImageFit.Contain)Canvas(item.context)// .margin({left:20,top:20}).width('100%').height('50%').onReady(() => {})}})}}Button('选择并上传图片') .position({ x: 100, y: 685 }).onClick(async () => {// 创建 图片选择对象const photoViewPicker = new picker.PhotoViewPicker();// 调用 select 方法,传入选项对象photoViewPicker.select(photoSelectOptions).then(async res => {const context = getContext(this)res.photoUris.forEach((item)=>{// this.str= itemlet settings: RenderingContextSettings = new RenderingContextSettings(true)let context1: CanvasRenderingContext2D = new CanvasRenderingContext2D(settings)let offCanvas: OffscreenCanvas = new OffscreenCanvas(600, 600)this.arr.push({ url:item,context:context1 })// 三、拷贝文件到缓存目录// 将文件保存到缓存目录(只能上传在缓存目录中的文件)const fileType = 'jpg'// 生成一个新的文件名const fileName = Date.now() + '.' + fileType// 通过缓存路径+文件名 拼接出完整的路径const copyFilePath = context.cacheDir + '/' + fileName// 将文件 拷贝到 临时目录const file = fs.openSync(item, fs.OpenMode.READ_ONLY)fs.copyFileSync(file.fd, copyFilePath)// 发送请求this.uploadImg(fileName,context1,settings,offCanvas)})})})}.padding(15).height('90%')}
}