当前位置: 首页 > news >正文

SegmentAnything官网demo使用vue+python实现

一、效果&准备工作

1.效果

没啥好说的,低质量复刻SAM官网 https://segment-anything.com/

需要提一点:所有生成embedding和mask的操作都是python后端做的,计算mask不是onnxruntime-web实现的,前端只负责了把rle编码的mask解码后画到canvas上,会有几十毫秒的网络传输延迟。我不会react和typescript,官网F12里的源代码太难懂了,生成的svg总是与期望的不一样

主页

在这里插入图片描述

鼠标移动动态分割(Hover)

throttle了一下,修改代码里的throttle delay,反应更快些,我觉得没必要已经够了,设置的150ms

在这里插入图片描述

点选前景背景(Click)

蓝色前景,红色背景,对应clickType分别为1和0

在这里插入图片描述

分割(Cut out object)

同官网,分割出该区域需要的最小矩形框部分
在这里插入图片描述

分割所有(Everything)

随便做了下,实在做不出官网的效果,可能模型也有问题 ,我用的vit_b,懒得试了,这功能对我来说没卵用

在这里插入图片描述

2.准备工作

安装依赖

前端使用了Vue3+ElementPlus(https://element-plus.org/zh-CN/#/zh-CN)+axios+lz-string,npm安装一下。

后端是fastapi(https://fastapi.tiangolo.com/),FastAPI 依赖 Python 3.8 及更高版本。

安装 FastAPI

pip install fastapi

另外我们还需要一个 ASGI 服务器,生产环境可以使用 Uvicorn 或者 Hypercorn:

pip install "uvicorn[standard]"
要用的js文件

@/util/request.js

import axios from "axios";
import { ElMessage } from "element-plus";axios.interceptors.request.use(config => {return config;},error => {return Promise.reject(error);}
);axios.interceptors.response.use(response => {if (response.data.success != null && !response.data.success) {return Promise.reject(response.data)}return response.data;},error => {console.log('error: ', error)ElMessage.error(' ');return Promise.reject(error);}
);export default axios;

然后在main.js中绑定

import axios from './util/request.js'
axios.defaults.baseURL = 'http://localhost:9000'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
app.config.globalProperties.$http = axios

@/util/throttle.js

function throttle(func, delay) {let timer = null; // 定时器变量return function() {const context = this; // 保存this指向const args = arguments; // 保存参数列表if (!timer) {timer = setTimeout(() => {func.apply(context, args); // 调用原始函数并传入上下文和参数clearTimeout(timer); // 清除计时器timer = null; // 重置计时器为null}, delay);}};
}
export default throttle

@/util/mask_utils.js

/*** Parses RLE from compressed string* @param {Array<number>} input* @returns array of integers*/
export const rleFrString = (input) => {let result = [];let charIndex = 0;while (charIndex < input.length) {let value = 0,k = 0,more = 1;while (more) {let c = input.charCodeAt(charIndex) - 48;value |= (c & 0x1f) << (5 * k);more = c & 0x20;charIndex++;k++;if (!more && c & 0x10) value |= -1 << (5 * k);}if (result.length > 2) value += result[result.length - 2];result.push(value);}return result;
};/*** Parse RLE to mask array* @param rows* @param cols* @param counts* @returns {Uint8Array}*/
export const decodeRleCounts = ([rows, cols], counts) => {let arr = new Uint8Array(rows * cols)let i = 0let flag = 0for (let k of counts) {while (k-- > 0) {arr[i++] = flag}flag = (flag + 1) % 2}return arr
};/*** Parse Everything mode counts array to mask array* @param rows* @param cols* @param counts* @returns {Uint8Array}*/
export const decodeEverythingMask = ([rows, cols], counts) => {let arr = new Uint8Array(rows * cols)let k = 0;for (let i = 0; i < counts.length; i += 2) {for (let j = 0; j < counts[i]; j++) {arr[k++] = counts[i + 1]}}return arr;
};/*** Get globally unique color in the mask* @param category* @param colorMap* @returns {*}*/
export const getUniqueColor = (category, colorMap) => {// 该种类没有颜色if (!colorMap.hasOwnProperty(category)) {// 生成唯一的颜色while (true) {const color = {r: Math.floor(Math.random() * 256),g: Math.floor(Math.random() * 256),b: Math.floor(Math.random() * 256)}// 检查颜色映射中是否已存在相同的颜色const existingColors = Object.values(colorMap);const isDuplicateColor = existingColors.some((existingColor) => {return color.r === existingColor.r && color.g === existingColor.g && color.b === existingColor.b;});// 如果不存在相同颜色,结束循环if (!isDuplicateColor) {colorMap[category] = color;break}}console.log("生成唯一颜色", category, colorMap[category])return colorMap[category]} else {return colorMap[category]}
}/*** Cut out specific area of image uncovered by mask* @param w image's natural width* @param h image's natural height* @param image source image* @param canvas mask canvas* @param callback function to solve the image blob*/
export const cutOutImage = ({w, h}, image, canvas, callback) => {const resultCanvas = document.createElement('canvas'),resultCtx = resultCanvas.getContext('2d', {willReadFrequently: true}),originalCtx = canvas.getContext('2d', {willReadFrequently: true});resultCanvas.width = w;resultCanvas.height = h;resultCtx.drawImage(image, 0, 0, w, h)const maskDataArray = originalCtx.getImageData(0, 0, w, h).data;const imageData = resultCtx.getImageData(0, 0, w, h);const imageDataArray = imageData.data// 将mask的部分去掉for (let i = 0; i < maskDataArray.length; i += 4) {const alpha = maskDataArray[i + 3];if (alpha !== 0) { // 不等于0,是mask区域imageDataArray[i + 3] = 0;}}// 计算被分割出来的部分的矩形框let minX = w;let minY = h;let maxX = 0;let maxY = 0;for (let y = 0; y < h; y++) {for (let x = 0; x < w; x++) {const alpha = imageDataArray[(y * w + x) * 4 + 3];if (alpha !== 0) {minX = Math.min(minX, x);minY = Math.min(minY, y);maxX = Math.max(maxX, x);maxY = Math.max(maxY, y);}}}const width = maxX - minX + 1;const height = maxY - minY + 1;const startX = minX;const startY = minY;resultCtx.putImageData(imageData, 0, 0)// 创建一个新的canvas来存储特定区域的图像const croppedCanvas = document.createElement("canvas");const croppedContext = croppedCanvas.getContext("2d");croppedCanvas.width = width;croppedCanvas.height = height;// 将特定区域绘制到新canvas上croppedContext.drawImage(resultCanvas, startX, startY, width, height, 0, 0, width, height);croppedCanvas.toBlob(blob => {if (callback) {callback(blob)}}, "image/png");
}/*** Cut out specific area of image covered by target color mask* PS: 我写的这代码有问题,比较color的时候tmd明明mask canvas中有这个颜色,* 就是说不存在这颜色,所以不用这个函数,改成下面的了* @param w image's natural width* @param h image's natural height* @param image source image* @param canvas mask canvas* @param color target color* @param callback function to solve the image blob*/
export const cutOutImageWithMaskColor = ({w, h}, image, canvas, color, callback) => {const resultCanvas = document.createElement('canvas'),resultCtx = resultCanvas.getContext('2d', {willReadFrequently: true}),originalCtx = canvas.getContext('2d', {willReadFrequently: true});resultCanvas.width = w;resultCanvas.height = h;resultCtx.drawImage(image, 0, 0, w, h)const maskDataArray = originalCtx.getImageData(0, 0, w, h).data;const imageData = resultCtx.getImageData(0, 0, w, h);const imageDataArray = imageData.datalet find = false// 比较mask的color和目标colorfor (let i = 0; i < maskDataArray.length; i += 4) {const r = maskDataArray[i],g = maskDataArray[i + 1],b = maskDataArray[i + 2];if (r != color.r || g != color.g || b != color.b) { // 颜色与目标颜色不相同,是mask区域// 设置alpha为0imageDataArray[i + 3] = 0;} else {find = true}}// 计算被分割出来的部分的矩形框let minX = w;let minY = h;let maxX = 0;let maxY = 0;for (let y = 0; y < h; y++) {for (let x = 0; x < w; x++) {const alpha = imageDataArray[(y * w + x) * 4 + 3];if (alpha !== 0) {minX = Math.min(minX, x);minY = Math.min(minY, y);maxX = Math.max(maxX, x);maxY = Math.max(maxY, y);}}}const width = maxX - minX + 1;const height = maxY - minY + 1;const startX = minX;const startY = minY;// console.log(`矩形宽度:${width}`);// console.log(`矩形高度:${height}`);// console.log(`起点坐标:(${startX}, ${startY})`);resultCtx.putImageData(imageData, 0, 0)// 创建一个新的canvas来存储特定区域的图像const croppedCanvas = document.createElement("canvas");const croppedContext = croppedCanvas.getContext("2d");croppedCanvas.width = width;croppedCanvas.height = height;// 将特定区域绘制到新canvas上croppedContext.drawImage(resultCanvas, startX, startY, width, height, 0, 0, width, height);croppedCanvas.toBlob(blob => {if (callback) {callback(blob)}}, "image/png");
}/*** Cut out specific area whose category is target category* @param w image's natural width* @param h image's natural height* @param image source image* @param arr original mask array that stores all pixel's category* @param category target category* @param callback function to solve the image blob*/
export const cutOutImageWithCategory = ({w, h}, image, arr, category, callback) => {const resultCanvas = document.createElement('canvas'),resultCtx = resultCanvas.getContext('2d', {willReadFrequently: true});resultCanvas.width = w;resultCanvas.height = h;resultCtx.drawImage(image, 0, 0, w, h)const imageData = resultCtx.getImageData(0, 0, w, h);const imageDataArray = imageData.data// 比较mask的类别和目标类别let i = 0for(let y = 0; y < h; y++){for(let x = 0; x < w; x++){if (category != arr[i++]) { // 类别不相同,是mask区域// 设置alpha为0imageDataArray[3 + (w * y + x) * 4] = 0;}}}// 计算被分割出来的部分的矩形框let minX = w;let minY = h;let maxX = 0;let maxY = 0;for (let y = 0; y < h; y++) {for (let x = 0; x < w; x++) {const alpha = imageDataArray[(y * w + x) * 4 + 3];if (alpha !== 0) {minX = Math.min(minX, x);minY = Math.min(minY, y);maxX = Math.max(maxX, x);maxY = Math.max(maxY, y);}}}const width = maxX - minX + 1;const height = maxY - minY + 1;const startX = minX;const startY = minY;resultCtx.putImageData(imageData, 0, 0)// 创建一个新的canvas来存储特定区域的图像const croppedCanvas = document.createElement("canvas");const croppedContext = croppedCanvas.getContext("2d");croppedCanvas.width = width;croppedCanvas.height = height;// 将特定区域绘制到新canvas上croppedContext.drawImage(resultCanvas, startX, startY, width, height, 0, 0, width, height);croppedCanvas.toBlob(blob => {if (callback) {callback(blob)}}, "image/png");
}

二、后端代码

1.SAM下载

首先从github上下载SAM的代码https://github.com/facebookresearch/segment-anything

然后下载模型文件,保存到项目根目录/checkpoints中,

  • default or vit_h: ViT-H SAM model.
  • vit_l: ViT-L SAM model.
  • vit_b: ViT-B SAM model.

2.后端代码

在项目根目录下创建main.py

main.py

import os
import timefrom PIL import Image
import numpy as np
import io
import base64
from segment_anything import SamPredictor, SamAutomaticMaskGenerator, sam_model_registry
from pycocotools import mask as mask_utils
import lzstringdef init():# your model pathcheckpoint = "checkpoints/sam_vit_b_01ec64.pth"model_type = "vit_b"sam = sam_model_registry[model_type](checkpoint=checkpoint)sam.to(device='cuda')predictor = SamPredictor(sam)mask_generator = SamAutomaticMaskGenerator(sam)return predictor, mask_generatorpredictor, mask_generator = init()from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddlewareapp = FastAPI()
app.add_middleware(CORSMiddleware,allow_origins="*",allow_credentials=True,allow_methods=["*"],allow_headers=["*"],
)last_image = ""
last_logit = None@app.post("/segment")
def process_image(body: dict):global last_image, last_logitprint("start processing image", time.time())path = body["path"]is_first_segment = False# 看上次分割的图片是不是该图片if path != last_image:  # 不是该图片,重新生成图像embeddingpil_image = Image.open(path)np_image = np.array(pil_image)predictor.set_image(np_image)last_image = pathis_first_segment = Trueprint("第一次识别该图片,获取embedding")# 获取maskclicks = body["clicks"]input_points = []input_labels = []for click in clicks:input_points.append([click["x"], click["y"]])input_labels.append(click["clickType"])print("input_points:{}, input_labels:{}".format(input_points, input_labels))input_points = np.array(input_points)input_labels = np.array(input_labels)masks, scores, logits = predictor.predict(point_coords=input_points,point_labels=input_labels,mask_input=last_logit[None, :, :] if not is_first_segment else None,multimask_output=is_first_segment  # 第一次产生3个结果,选择最优的)# 设置mask_input,为下一次做准备best = np.argmax(scores)last_logit = logits[best, :, :]masks = masks[best, :, :]# print(mask_utils.encode(np.asfortranarray(masks))["counts"])# numpy_array = np.frombuffer(mask_utils.encode(np.asfortranarray(masks))["counts"], dtype=np.uint8)# print("Uint8Array([" + ", ".join(map(str, numpy_array)) + "])")source_mask = mask_utils.encode(np.asfortranarray(masks))["counts"].decode("utf-8")# print(source_mask)lzs = lzstring.LZString()encoded = lzs.compressToEncodedURIComponent(source_mask)print("process finished", time.time())return {"shape": masks.shape, "mask": encoded}@app.get("/everything")
def segment_everything(path: str):start_time = time.time()print("start segment_everything", start_time)pil_image = Image.open(path)np_image = np.array(pil_image)masks = mask_generator.generate(np_image)sorted_anns = sorted(masks, key=(lambda x: x['area']), reverse=True)img = np.zeros((sorted_anns[0]['segmentation'].shape[0], sorted_anns[0]['segmentation'].shape[1]), dtype=np.uint8)for idx, ann in enumerate(sorted_anns, 0):img[ann['segmentation']] = idx#看一下mask是什么样#plt.figure(figsize=(10,10))#plt.imshow(img) #plt.show()# 压缩数组result = my_compress(img)end_time = time.time()print("finished segment_everything", end_time)print("time cost", end_time - start_time)return {"shape": img.shape, "mask": result}@app.get('/automatic_masks')
def automatic_masks(path: str):pil_image = Image.open(path)np_image = np.array(pil_image)mask = mask_generator.generate(np_image)sorted_anns = sorted(mask, key=(lambda x: x['area']), reverse=True)lzs = lzstring.LZString()res = []for ann in sorted_anns:m = ann['segmentation']source_mask = mask_utils.encode(m)['counts'].decode("utf-8")encoded = lzs.compressToEncodedURIComponent(source_mask)r = {"encodedMask": encoded,"point_coord": ann['point_coords'][0],}res.append(r)return res# 就是将连续的数字统计个数,然后把[个数,数字]放到result中,类似rle算法
# 比如[[1,1,1,2,3,2,2,4,4],[3,3,4...]]
# result是[3,1,  1,2,  1,3,  2,2,  2,4,  2,3,...]
def my_compress(img):result = []last_pixel = img[0][0]count = 0for line in img:for pixel in line:if pixel == last_pixel:count += 1else:result.append(count)result.append(int(last_pixel))last_pixel = pixelcount = 1result.append(count)result.append(int(last_pixel))return result

3.原神启动

在cmd或者pycharm终端,cd到项目根目录下,输入uvicorn main:app --port 8006,启动服务器

三、前端代码

1.页面代码

template
<template><div class="segment-container"><ElScrollbar class="tool-box"><div class="image-section"><div class="title"><div style="padding-left:15px"><el-icon><Picture /></el-icon><span style="font-size: 18px;font-weight: 550;">展示图像</span><el-icon class="header-icon"></el-icon></div></div><ElScrollbar height="350px"><div v-if="cutOuts.length === 0"><p>未进行抠图</p><p>左键设置区域为前景</p><p>右键设置区域为背景</p></div><img v-for="src in cutOuts" :src="src" alt="加载中"@click="openInNewTab(src)"/></ElScrollbar></div><div class="options-section"><span class="option" @click="reset">重置</span><span :class="'option'+(clicks.length===0?' disabled':'')" @click="undo">撤销</span><span :class="'option'+(clickHistory.length===0?' disabled':'')" @click="redo">恢复</span></div><button :class="'segmentation-button'+(lock||clicks.length===0?' disabled':'')"@click="cutImage">分割</button><button :class="'segmentation-button'+(lock||isEverything?' disabled':'')"@click="segmentEverything">分割所有</button></ElScrollbar><div class="segment-box"><div class="segment-wrapper" :style="{'left': left + 'px'}"><img v-show="path" id="segment-image" :src="url" :style="{width:w, height:h}" alt="加载失败" crossorigin="anonymous"@mousedown="handleMouseDown" @mouseenter="canvasVisible = true"@mouseout="() => {if (!this.clicks.length&&!this.isEverything) this.canvasVisible = false}"/><canvas v-show="path && canvasVisible" id="segment-canvas" :width="originalSize.w" :height="originalSize.h"></canvas><div id="point-box" :style="{width:w, height:h}"></div></div></div></div>
</template>
script
<script>
import throttle from "@/util/throttle";
import LZString from "lz-string";
import {rleFrString,decodeRleCounts,decodeEverythingMask,getUniqueColor,cutOutImage,cutOutImageWithMaskColor, cutOutImageWithCategory
} from "@/util/mask_utils";
import {ElCollapse, ElCollapseItem, ElScrollbar} from "element-plus";
import {Picture} from '@element-plus/icons-vue'
export default {name: "segment",components: {ElCollapse, ElCollapseItem, ElScrollbar, Picture},data() {return {image: null,clicks: [],clickHistory: [],originalSize: {w: 0, h: 0},w: 0,h: 0,left: 0,scale: 1,url: null, // url用来设置成img的src展示path: null, // path是该图片在文件系统中的绝对路径loading: false,lock: false,canvasVisible: true,// cutOuts: ['http://localhost:9000/p/2024/01/19/112ce48bd76e47c7900863a3a0147853.jpg', 'http://localhost:9000/p/2024/01/19/112ce48bd76e47c7900863a3a0147853.jpg'],cutOuts: [],isEverything: false}},mounted() {this.init()},methods: {async init() {this.loading = true// 从路由获取idlet id = this.$route.params.idif (!id) {this.$message.error('未选择图片')return}this.id = id// 获取图片信息try {const { path, url } = await this.getPathAndUrl()this.loadImage(path, url)} catch (e) {console.error(e)this.$message.error(e)}},async getPathAndUrl() {let res = await this.$http.get("/photo/path/" + this.id)console.log(res)return res.data},loadImage(path, url) {let image = new Image();image.src = this.$photo_base + url;image.onload = () => {let w = image.width, h = image.heightlet nw, nhlet body = document.querySelector('.segment-box')let mw = body.clientWidth, mh = body.clientHeightlet ratio = w / hif (ratio * mh > mw) {nw = mwnh = mw / ratio} else {nh = mhnw = ratio * mh}this.originalSize = {w, h}nw = parseInt(nw)nh = parseInt(nh)this.w = nw + 'px'this.h = nh + 'px'this.left = (mw - nw) / 2this.scale = nw / wthis.url = this.$photo_base + urlthis.path = pathconsole.log((this.scale > 1 ? '放大' : '缩小') + w + ' --> ' + nw)const img = document.getElementById('segment-image')img.addEventListener('contextmenu', e => e.preventDefault())img.addEventListener('mousemove', throttle(this.handleMouseMove, 150))const canvas = document.getElementById('segment-canvas')canvas.style.transform = `scale(${this.scale})`}},getClick(e) {let click = {x: e.offsetX,y: e.offsetY,}const imageScale = this.scaleclick.x /= imageScale;click.y /= imageScale;if(e.which === 3){ // 右键click.clickType = 0} else if(e.which === 1 || e.which === 0) { // 左键click.clickType = 1}return click},handleMouseMove(e) {if (this.isEverything) { // 分割所有模式,返回return;}if (this.clicks.length !== 0) { // 选择了点return;}if (this.lock) {return;}this.lock = true;let click = this.getClick(e);requestIdleCallback(() => {this.getMask([click])})},handleMouseDown(e) {e.preventDefault();e.stopPropagation();if (e.button === 1) {return;}// 如果是“分割所有”模式,返回if (this.isEverything) {return;}if (this.lock) {return;}this.lock = truelet click = this.getClick(e);this.placePoint(e.offsetX, e.offsetY, click.clickType)this.clicks.push(click);requestIdleCallback(() => {this.getMask()})},placePoint(x, y, clickType) {let box = document.getElementById('point-box')let point = document.createElement('div')point.className = 'segment-point' + (clickType ? '' : ' negative')point.style = `position: absolute;width: 10px;height: 10px;border-radius: 50%;background-color: ${clickType?'#409EFF':'#F56C6C '};left: ${x-5}px;top: ${y-5}px`// 点的id是在clicks数组中的下标索引point.id = 'point-' + this.clicks.lengthbox.appendChild(point)},removePoint(i) {const selector = 'point-' + ilet point = document.getElementById(selector)if (point != null) {point.remove()}},getMask(clicks) {// 如果clicks为空,则是mouse move产生的clickif (clicks == null) {clicks = this.clicks}const data = {path: this.path,clicks: clicks}console.log(data)this.$http.post('http://localhost:8006/segment', data, {headers: {"Content-Type": "application/json"}}).then(res => {const shape = res.shapeconst maskenc = LZString.decompressFromEncodedURIComponent(res.mask);const decoded = rleFrString(maskenc)this.drawCanvas(shape, decodeRleCounts(shape, decoded))this.lock = false}).catch(err => {console.error(err)this.$message.error("生成失败")this.lock = false})},segmentEverything() {if (this.isEverything) { // 上一次刚点过了return;}if (this.lock) {return;}this.lock = truethis.reset()this.isEverything = truethis.canvasVisible = truethis.$http.get("http://localhost:8006/everything?path=" + this.path).then(res => {const shape = res.shapeconst counts = res.maskthis.drawEverythingCanvas(shape, decodeEverythingMask(shape, counts))}).catch(err => {console.error(err)this.$message.error("生成失败")})},drawCanvas(shape, arr) {let height = shape[0],width = shape[1]console.log("height: ", height, " width: ", width)let canvas = document.getElementById('segment-canvas'),canvasCtx = canvas.getContext("2d"),imgData = canvasCtx.getImageData(0, 0, width, height),pixelData = imgData.datalet i = 0for(let x = 0; x < width; x++){for(let y = 0; y < height; y++){if (arr[i++] === 0) { // 如果是0,是背景,遮住pixelData[0 + (width * y + x) * 4] = 40;pixelData[1 + (width * y + x) * 4] = 40;pixelData[2 + (width * y + x) * 4] = 40;pixelData[3 + (width * y + x) * 4] = 190;} else {pixelData[3 + (width * y + x) * 4] = 0;}}}canvasCtx.putImageData(imgData, 0, 0)},drawEverythingCanvas(shape, arr) {const height = shape[0],width = shape[1]console.log("height: ", height, " width: ", width)let canvas = document.getElementById('segment-canvas'),canvasCtx = canvas.getContext("2d"),imgData = canvasCtx.getImageData(0, 0, width, height),pixelData = imgData.data;const colorMap = {}let i = 0for(let y = 0; y < height; y++){for(let x = 0; x < width; x++){const category = arr[i++]const color = getUniqueColor(category, colorMap)pixelData[0 + (width * y + x) * 4] = color.r;pixelData[1 + (width * y + x) * 4] = color.g;pixelData[2 + (width * y + x) * 4] = color.b;pixelData[3 + (width * y + x) * 4] = 150;}}// 显示在图片上canvasCtx.putImageData(imgData, 0, 0)// 开始分割每一个mask的图片const image = document.getElementById('segment-image')Object.keys(colorMap).forEach(category => {cutOutImageWithCategory(this.originalSize, image, arr, category, blob => {const url = URL.createObjectURL(blob);this.cutOuts = [url, ...this.cutOuts]})})},reset() {for (let i = 0; i < this.clicks.length; i++) {this.removePoint(i)}this.clicks = []this.clickHistory = []this.isEverything = falsethis.clearCanvas()},undo() {if (this.clicks.length === 0)returnconst idx = this.clicks.length - 1const click = this.clicks[idx]this.clickHistory.push(click)this.clicks.splice(idx, 1)this.removePoint(idx)if (this.clicks.length) {this.getMask()} else {this.clearCanvas()}},redo() {if (this.clickHistory.length === 0)returnconst idx = this.clickHistory.length - 1const click = this.clickHistory[idx]console.log(this.clicks, this.clickHistory, click)this.placePoint(click.x * this.scale, click.y * this.scale, click.clickType)this.clicks.push(click)this.clickHistory.splice(idx, 1)this.getMask()},clearCanvas() {let canvas = document.getElementById('segment-canvas')canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)},cutImage() {if (this.lock || this.clicks.length === 0) {return;}const canvas = document.getElementById('segment-canvas'),image = document.getElementById('segment-image')const {w, h} = this.originalSizecutOutImage(this.originalSize, image, canvas, blob => {const url = URL.createObjectURL(blob);this.cutOuts = [url, ...this.cutOuts]// 不需要之后用下面的清除文件// URL.revokeObjectURL(url);})},openInNewTab(src) {window.open(src, '_blank')}}
}
</script>
style
<style scoped lang="scss">
.segment-container {position: relative;
}.tool-box {position: absolute;left: 20px;top: 20px;width: 200px;height: 600px;border-radius: 20px;//background: pink;overflow: auto;box-shadow: 0 0 5px rgb(150, 150, 150);box-sizing: border-box;padding: 10px;.image-section {height: fit-content;width: 100%;.title {height: 48px;line-height: 48px;border-bottom: 1px solid lightgray;margin-bottom: 15px;}}.image-section img {max-width: 85%;max-height: 140px;margin: 10px auto;padding: 10px;box-sizing: border-box;object-fit: contain;display: block;transition: .3s;cursor: pointer;}.image-section img:hover {background: rgba(0, 30, 160, 0.3);}.image-section p {text-align: center;}.options-section {margin-top: 5px;display: flex;justify-content: space-between;align-items: center;padding: 10px;box-sizing: border-box;border: 3px solid lightgray;border-radius: 20px;}.options-section:hover {border: 3px solid #59ACFF;}.option {font-size: 15px;padding: 5px 10px;cursor: pointer;}.option:hover {color: #59ACFF;}.option.disabled {color: gray;cursor: not-allowed;}.segmentation-button {margin-top: 5px;width: 100%;height: 40px;background-color: white;color: rgb(40, 40, 40);font-size: 17px;cursor: pointer;border: 3px solid lightgray;border-radius: 20px;}.segmentation-button:hover {border: 3px solid #59ACFF;}.segmentation-button.disabled {color: lightgray;cursor: not-allowed;}
}.segment-box {position: relative;margin-left: calc(220px);width: calc(100% - 220px);height: calc(100vh - 80px);//background: #42b983;.segment-wrapper {position: absolute;left: 0;top: 0;}#segment-canvas {position: absolute;left: 0;top: 0;pointer-events: none;transform-origin: left top;z-index: 1;}#point-box {position: absolute;left: 0;top: 0;z-index: 2;pointer-events: none;}.segment-point {position: absolute;width: 10px;height: 10px;border-radius: 50%;background-color: #409EFF;}.segment-point.negative {background-color: #F56C6C;}
}
</style>

2.代码说明

  • 本项目没做上传图片分割,就是简单的选择本地图片分割,data中url是img的src,path是绝对路径用来传给python后端进行分割,我是从我项目的系统获取的,请自行修改代码成你的图片路径,如src: “/assets/test.jpg”, path:“D:/project/segment/assets/test.jpg”
  • 由于pycocotools的rle encode是从上到下进行统计连续的0和1,为了方便,我在【@/util/mask_utils.js:decodeRleCounts】解码Click点选产生的mask时将(H,W)的矩阵转成了(W,H)顺序存储的Uint8array;而在Everything分割所有时,我没有使用pycocotools的encode,而是main.py中的my_compress函数编码的,是从左到右进行压缩,因此矩阵解码后仍然是(H,W)的矩阵,所以在drawCanvasdrawEverythingCanvas中的二层循环xy的顺序不一样,我实在懒得改了,就这样就可以了。

关于上面所提rle,可以在项目根目录/notebooks/predictor_example.ipynb中产生mask的位置添加代码自行观察他编码的rle,他只支持矩阵元素为0或1,result的第一个位置是0的个数,不管矩阵是不是0开头。

  • [0,0,1,1,0,1,0],rle counts是[2(两个0), 2(两个1), 1(一个0), 1(一个1), 1(一个0)];

  • [1,1,1,1,1,0],rle counts是[0(零个0),5(五个1),1(一个0)]

def decode_rle(rle_string): # 这是将pycocotools的counts编码的字符串转成counts数组,而非转成原矩阵result = []char_index = 0while char_index < len(rle_string):value = 0k = 0more = 1while more:c = ord(rle_string[char_index]) - 48value |= (c & 0x1f) << (5 * k)more = c & 0x20char_index += 1k += 1if not more and c & 0x10:value |= -1 << (5 * k)if len(result) > 2:value += result[-2]result.append(value)return resultfrom pycocotools import mask as mask_utils
import numpy as np
mask = np.array([[1,1,0,1,1,0],[1,1,1,1,1,1],[0,1,1,1,0,0],[1,1,1,1,1,1]])
mask = np.asfortranarray(mask, dtype=np.uint8)
print("原mask:\n{}".format(mask))
res = mask_utils.encode(mask)
print("encode:{}".format(res))
print("rle counts:{}".format(decode_rle(res["counts"].decode("utf-8"))))
# 转置后好看
print("转置:{}".format(mask.transpose()))
# flatten后更好看
print("flatten:{}".format(mask.transpose().flatten()))
#numpy_array = np.frombuffer(res["counts"], dtype=np.uint8)
# 打印numpy数组作为uint8array的格式
#print("Uint8Array([" + ", ".join(map(str, numpy_array)) + "])")

输出:

在这里插入图片描述

相关文章:

SegmentAnything官网demo使用vue+python实现

一、效果&准备工作 1.效果 没啥好说的&#xff0c;低质量复刻SAM官网 https://segment-anything.com/ 需要提一点&#xff1a;所有生成embedding和mask的操作都是python后端做的&#xff0c;计算mask不是onnxruntime-web实现的&#xff0c;前端只负责了把rle编码的mask解…...

Java:字符集、IO流 --黑马笔记

一、字符集 1.1 字符集的来历 我们知道计算机是美国人发明的&#xff0c;由于计算机能够处理的数据只能是0和1组成的二进制数据&#xff0c;为了让计算机能够处理字符&#xff0c;于是美国人就把他们会用到的每一个字符进行了编码&#xff08;所谓编码&#xff0c;就是为一个…...

RabbitMQ之五种消息模型

1、 环境准备 创建Virtual Hosts 虚拟主机&#xff1a;类似于mysql中的database。他们都是以“/”开头 设置权限 2. 五种消息模型 RabbitMQ提供了6种消息模型&#xff0c;但是第6种其实是RPC&#xff0c;并不是MQ&#xff0c;因此不予学习。那么也就剩下5种。 但是其实3、4…...

项目02《游戏-14-开发》Unity3D

基于 项目02《游戏-13-开发》Unity3D &#xff0c; 任务&#xff1a;战斗系统之击败怪物与怪物UI血条信息 using UnityEngine; public abstract class Living : MonoBehaviour{ protected float hp; protected float attack; protected float define; …...

【Java数据结构】单向 不带头 非循环 链表实现

模拟实现LinkedList&#xff1a;下一篇文章 LinkedList底层是双向、不带头结点、非循环的链表 /*** LinkedList的模拟实现*单向 不带头 非循环链表实现*/ class SingleLinkedList {class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val …...

【ES6】模块化

nodejs遵循了CommonJs的模块化规范 导入 require() 导出 module.exports 模块化的好处&#xff1a; 模块化可以避免命名冲突的问题大家都遵循同样的模块化写代码&#xff0c;降低了沟通的成本&#xff0c;极大方便了各个模块之间的相互调用需要啥模块&#xff0c;调用就行 …...

腾讯云4核8G服务器可以用来干嘛?怎么收费?

腾讯云4核8G服务器适合做什么&#xff1f;搭建网站博客、企业官网、小程序、小游戏后端服务器、电商应用、云盘和图床等均可以&#xff0c;腾讯云4核8G服务器可以选择轻量应用服务器4核8G12M或云服务器CVM&#xff0c;轻量服务器和标准型CVM服务器性能是差不多的&#xff0c;轻…...

怎么在bash shell中操作复杂json对象

怎么在bash shell中操作复杂json对象 在bash shell中操作复杂JSON对象&#xff0c;jq可以帮助我们在bash环境下轻松地处理这类数据&#xff0c;本文将详细介绍如何使用jq在bash中操作复杂的JSON对象。 jq是一个轻量级且灵活的命令行JSON处理器&#xff0c;它允许你以非常高效的…...

11.div函数

文章目录 函数简介1.函数原型2.div_t结构体3.引用头文件 代码运行 函数简介 1.函数原型 div_t div(int numerator, int denominator);div函数把numerator除以denominator&#xff0c;产生商和余数&#xff0c;用一个div_t的结构体返回。 2.div_t结构体 typedef struct _div…...

windows11 MSYS2下载安装教程

MSYS2 可以理解为在windows平台上模拟linux编程环境的开源工具集 当前环境&#xff1a;windows11 1. 下载 官网地址可下载最新版本&#xff0c;需要科学上网 https://www.msys2.org/ 2. 安装 按照正常安装软件流程一路next就可以 打开 3. 配置环境 网上很多教程提到需…...

Excel+VBA处理高斯光束

文章目录 1 图片导入与裁剪2 获取图片数据3 数据拟合 1 图片导入与裁剪 插入图片没什么好说的&#xff0c;新建Excel&#xff0c;【插入】->【图片】。 由于图像比较大&#xff0c;所以要对数据进行截取&#xff0c;选中图片之后&#xff0c;点击选项卡右端的【图片格式】…...

如何启动若依框架

Mysql安装 一、下载 链接&#xff1a;https://pan.baidu.com/s/1s8-Y1ooaRtwP9KnmP3rxlQ?pwd1234 提取码&#xff1a;1234 二、安装(解压) 下载完成后我们得到的是一个压缩包&#xff0c;将其解压&#xff0c;我们就可以得到MySQL 5.7.24的软件本体了(就是一个文件夹)&…...

案例:CentOS8 在 MySQL8.0 实现半同步复制

异步复制 MySQL 默认的复制即是异步的&#xff0c;主库在执行完客户端提交的事务后会立即将结果返给给客户端&#xff0c;并不关心从库是否已经接收并处理&#xff0c;这样就会有一个问题&#xff0c;主节点如果 crash 掉了&#xff0c;此时主节点上已经提交的事务可能并没有传…...

阿里云带宽计费模式怎么选?如何收费的?

阿里云服务器带宽计费模式分为“按固定带宽”和“按使用流量”&#xff0c;有什么区别&#xff1f;按固定带宽是指直接购买多少M带宽&#xff0c;比如1M、5M、10M、100M等&#xff0c;阿里云直接分配用户所购买的带宽值&#xff0c;根据带宽大小先付费再使用&#xff1b;按使用…...

c#记录几个问题

最近在看c#&#xff0c;有几个问题记录下 1&#xff09;全局变量&#xff0c;其实是声明一个public static类&#xff0c;里面包含一些public static变量和函数&#xff0c;也就是在程序开始运行后就创生了一个对应的存储空间&#xff0c;调用时就要写明是谁的什么变量。针对的…...

第69讲后端登录逻辑实现

Admin实体&#xff1a; TableName("t_admin") Data public class Admin {TableId(type IdType.AUTO)private Integer id; // 编号private String userName; // 用户名private String password; // 密码TableField(select false)private String newPassword; // 新…...

Qt 字符串类应用与常用基本数据类型

目录 操作字符串 查询字符串 Qt 常见数据类型 操作字符串 创建一个控制台项目 &#xff08;1&#xff09;QString提供一个二元的 “” 操作符&#xff0c;主要用于组合两个字符串。QString str1 "Hello World 传递给QString一个 const char* 类型的ASCII字符串 “He…...

JAVA面试题15

当然&#xff0c;我可以提供给您一些常见的Java面试题及其答案。以下是一些示例&#xff1a; 什么是Java的四种基本数据类型&#xff1f; 答案&#xff1a;Java的四种基本数据类型是整型&#xff08;byte、short、int、long&#xff09;、浮点型&#xff08;float、double&…...

git安装及使用

1、下载git 官网 Windows系统Git安装教程&#xff08;详解Git安装过程&#xff09; 官网打不开的话&#xff0c;可以使用镜像地址 镜像地址 2、使用git Git的下载、安装与使用(Windows) 30分钟带你精通git使用 3、注册github https://github.com/ 4、github文档 h…...

电力负荷预测 | Matlab实现基于LSTM长短期记忆神经网络的电力负荷预测模型(结合时间序列)

文章目录 效果一览文章概述源码设计参考资料效果一览 文章概述 电力负荷预测 | Matlab实现基于LSTM长短期记忆神经网络的电力负荷预测模型(结合时间序列) 所谓预测,就是指通过对事物进行分析及研究,并运用合理的方法探索事物的发展变化规律,对其未来发展做出预先估计和判断…...

力扣:455. 分发饼干

贪心解法思路&#xff1a; 1.先把两个数组按顺序遍历好&#xff0c;之后用最大的饼干来喂最大的胃口&#xff0c;如果最大的饼干不能喂饱最大的胃口&#xff0c;就除去这个最大的胃口&#xff0c;在剩下的为胃口中找最大的胃口来进行比对。这题主要历用了通过局部的优解&#…...

SpringCloud-项目引入Nacos

一、安装Nacos服务 首先&#xff0c;我们需要从 Nacos 的官方网站下载发布版本。下载地址&#xff1a;Releases alibaba/nacos GitHub 选择合适的版本并下载&#xff0c;解压缩得到 Nacos 的安装包。 在解压后的 Nacos 目录中&#xff0c;找到 bin 文件夹。 用写字板编辑…...

如何在 Windows 10/11 上恢复回收站永久删除的文件夹?

经验丰富的 Windows 用户将使用 Windows 备份和还原或文件历史记录来恢复不在回收站中的已删除文件夹。这些工具确实有助于 Windows 文件夹恢复&#xff0c;但并不总是有效。现在有许多专用的 Windows 数据恢复软件和免费解决方案可以替代它们&#xff0c;为 Windows 用户提供了…...

七、滚动条操作——调整图像对比度

对比度调整&#xff1a;是在原来图像基础上进行相应的公式调整&#xff0c;是类似乘法操作&#xff0c;本身像数值越大&#xff0c;对比度增加之后其与低像素点值差距越大&#xff0c;导致对比增强 项目最终效果&#xff1a;通过滚动条trackbar来实现调整图片亮度的功能 我这里…...

免费生成ios证书的方法(无需mac电脑)

使用hbuilderx的uniapp框架开发移动端程序很方便&#xff0c;可以很方便地开发出移动端的小程序和app。但是打包ios版本的app的时候却很麻烦&#xff0c;官方提供的教程需要使用mac电脑来生成证书&#xff0c;但是mac电脑却不便宜&#xff0c;一般的型号都差不多上万。 因此&a…...

gtkmm4 应用程序使用 CSS 样式

文章目录 前言css选择器css文件示例源代码效果动态设置css-classes 前言 程序样式和代码逻辑分离开 使代码逻辑更可观 css选择器 Cambalache提供了两种css-classes 相当于css里的类名:class“类名”css-name 相当于css里的标签名:spin div p 啥的 如上我设置了这个按钮控件的…...

科研绘图-半小提琴图-

文章目录 前言1.软件安装-Origin 20222.绘制半小提琴图3.绘制径向条形图 前言 本文叙述记录的是一些科研绘图的实现方法&#xff0c;具体介绍从软件安装到实现图表绘制的详细过程。 1.软件安装-Origin 2022 Origin是一款具有丰富绘图功能的科研绘图软件&#xff0c;安装过程…...

机器学习 | 深入集成学习的精髓及实战技巧挑战

目录 xgboost算法简介 泰坦尼克号乘客生存预测(实操) lightGBM算法简介 《绝地求生》玩家排名预测(实操) xgboost算法简介 XGBoost全名叫极端梯度提升树&#xff0c;XGBoost是集成学习方法的王牌&#xff0c;在Kaggle数据挖掘比赛中&#xff0c;大部分获胜者用了XGBoost。…...

SNMP(简单网络管理协议)介绍

简介 作为系统管理员的重要工作之一是收集关于服务器和基础设施的准确信息。有许多工具和选项可用于收集和处理这种类型的信息。其中许多工具都是建立在一种称为SNMP的技术之上。 SNMP代表简单网络管理协议。这是服务器可以共享有关其当前状态的信息的一种方式&#xff0c;也…...

Spring中常见的设计模式

使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性、更具有灵活、优雅&#xff0c;而Spring中共有九种常见的设计模式 工厂模式 工厂模式&#xff08;Factory Pattern&#xff09;是 Java 中最常用的设计模式之一。这种类型的设计模式属于…...

【MySQL】——数值函数的学习

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-Z1fAnfrxGD7I5gqp {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…...

LLMs模型选择,LLMs复读机问题,LLMs长文本处理方案

为什么会出现 LLMs 复读机问题&#xff1f; LLMs 复读机问题&#xff08;LLMs Parroting Problem&#xff09;是指大型语言模型&#xff08;LLMs&#xff09;在生成文本时可能出现的重复或重复先前输入内容的现象。出现LLMs复读机问题可能有以下几个原因&#xff1a; 数据偏差…...

LeetCode.144. 二叉树的前序遍历

题目 144. 二叉树的前序遍历 分析 这道题目是比较基础的题目&#xff0c;我们首先要知道二叉树的前序遍历是什么&#xff1f; 就是【根 左 右】 的顺序&#xff0c;然后利用递归的思想&#xff0c;就可以得到这道题的答案&#xff0c;任何的递归都可以采用 栈 的结构来实现…...

Redis复制

文章目录 1.Redis复制是什么2.Redis能干嘛3.权限细节4.基本操作命令5.常用三招5.1 一主二仆5.2 薪火相传5.3 反客为主 6.复制原理和工作流程7.复制的缺点 1.Redis复制是什么 就是主从复制&#xff0c;master以写为主&#xff0c;Slave以读为主。当master数据变化的时候&#x…...

C++入门学习(二十七)跳转语句—break语句

1、与switch语句联合使用 C入门学习&#xff08;二十三&#xff09;选择结构-switch语句-CSDN博客 #include <iostream> #include <string> using namespace std;int main() { int number;cout<<"请为《斗萝大路》打星(1~5※)&#xff1a;" &…...

Spark安装(Yarn模式)

一、解压 链接&#xff1a;https://pan.baidu.com/s/1O8u1SEuLOQv2Yietea_Uxg 提取码&#xff1a;mb4h tar -zxvf /opt/software/spark-3.0.3-bin-hadoop3.2.tgz -C /opt/module/spark-yarn mv spark-3.0.3-bin-hadoop3.2/ spark-yarn 二、配置环境变量 vim /etc/profile…...

1.4 Binance_interface API U本位合约行情

Binance_interface API U本位合约行情 Github地址PyTed量化交易研究院 1. API U本位合约行情接口总览 方法解释Pathget_ping测试服务器连通性 PING/fapi/v1/pingget_time获取服务器时间/fapi/v1/timeget_exchangeInfo获取交易规则和交易对/fapi/v1/exchangeInfoget_depth深度…...

单片机学习笔记---AT24C02(I2C总线)

目录 有关储存器的介绍 存储器的简介 存储器简化模型 AT24C02介绍 AT24C02引脚及应用电路 I2C总线介绍 I2C电路规范 开漏输出模式和弱上拉模式 其中一个设备的内部结构 I2C通信是怎么实现的 I2C时序结构 起始条件和终止条件 发送一个字节 接收一个字节 发送应答…...

c++恶魔轮盘制造第1期输赢

小常识&#xff0c;恶魔叫DEALER。 赢了很简单 void sheng() { cout<<"你获胜了&#xff01;";MessageBox(NULL,TEXT("你的钱~~~~~~给你"),TEXT("DEALER"),MB_OK);system("pause");system("cls"); } 输了我用了个选…...

60-JS-Ajax

ajax取数据的一种手段,局部刷新,例如弹幕 1.ajax的使用,创建ajax对象,发起对服务器请求 2.核心对象XMLHttpRequest对象(简称XHR) CSS:Cascading Style Sheets(层叠样式表) HTML:Hypertext Markup Language(超文本标记语言) 3.发起对服务器的请求 浏览器方式请求:打…...

C# Avalonia 折线图

线图开发在C# Avalonia框架中可以通过多种方式实现。由于Avalonia旨在成为跨平台的UI框架&#xff0c;您可以利用多种库和方法来绘制折线图。以下是一个简单的例子&#xff0c;展示了如何在Avalonia应用程序中创建一个基本的折线图。 首先&#xff0c;您需要在Avalonia项目中包…...

Vue3中Setup概述和使用(三)

一、引入Setup 1、Person.Vue 与Vue3编写简单的App组件(二) 中的区别是&#xff1a;取消data、methods等方法,而是将数据和方法定义全部放进setup中。 <template><div class"person"><h1>姓名:{{name}}</h1><h1>年龄:{{age}}</h…...

hexo 博客搭建以及踩雷总结

搭建时的坑 文章置顶 安装一下这个依赖 npm install hexo-generator-topindex --save然后再文章的上面设置 top: number&#xff0c;数字越大&#xff0c;权重越大&#xff0c;也就是越靠顶部 hexo 每次推送 nginx 都访问不到 宝塔自带的 nginx 的 config 里默认的角色是 …...

WordPress后台编辑个人资料页面直接修改用户名插件Change Username

前面跟大家介绍了『如何修改WordPress后台管理员用户名&#xff1f;推荐2种简单方法』一文&#xff0c;但是对于新站长或者有很多用户的站长来说&#xff0c;操作有点复杂&#xff0c;所以今天向大家推荐一款可以直接在WordPress后台编辑个人&#xff08;用户&#xff09;资料页…...

ssm+vue的医药垃圾分类管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的医药垃圾分类管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结…...

LLM大模型基本概念,及其相关问题汇总(1)

什么是涌现&#xff1f;为什么会出现涌现&#xff1f; "大模型的涌现能力"这个概念可能是指大型神经网络模型在某些任务上表现出的出乎意料的能力&#xff0c;超出了人们的预期。出现的原因从结论上来看&#xff0c;是模型不够好&#xff0c;导致的原因主要是&#…...

【已解决】pt文件转onnx后再转rknn时得到推理图片出现大量锚框变花屏

前言 环境介绍&#xff1a; 1.编译环境 Ubuntu 18.04.5 LTS 2.RKNN版本 py3.8-rknn2-1.4.0 3.单板 迅为itop-3568开发板 一、现象 采用yolov5训练并将pt转换为onnx&#xff0c;再将onnx采用py3.8-rknn2-1.4.0推理转换为rknn&#xff0c;rknn模型能正常转换&#xff0c;…...

DevOps文章之 操作手册用户使用说明书

前言 最近主导了几个项目操作手册的编写。有新开发的项目&#xff0c;要重新编写操作手册&#xff1b;有中途接手别的项目&#xff0c;后来功能迭代&#xff0c;需要更新原操作手册&#xff1b;有客户对操作手册有意见&#xff0c;需要调整&#xff1b;零零散散写了数万字的手…...

【RT-DETR进阶实战】利用RT-DETR进行视频划定区域目标统计计数

👑欢迎大家订阅本专栏,一起学习RT-DETR👑 一、本文介绍 Hello,各位读者,最近会给大家发一些进阶实战的讲解,如何利用RT-DETR现有的一些功能进行一些实战, 让我们不仅会改进RT-DETR,也能够利用RT-DETR去做一些简单的小工作,后面我也会将这些功能利用PyQt或者是…...

2.11学习总结

有效点对https://www.acwing.com/problem/content/description/5472/ 给定一个 n&#xfffd; 个节点的无向树&#xff0c;节点编号 1∼n1∼&#xfffd;。 树上有两个不同的特殊点 x,y&#xfffd;,&#xfffd;&#xff0c;对于树中的每一个点对 (u,v)(u≠v)(&#xfffd;,…...