import * as THREE from 'three' import math from './util/math.js' import common from './util/common.js' import BoundingMesh from './util/BoundingMesh.js' import Vectors from './util/Vectors.js' const version = 'output' //----------------------复制以下内容--------------------------------- let player, skyBoxTight, meshGroup, modelBound = new THREE.Box3, ray = new THREE.Raycaster(), groundPlane = new THREE.Plane() let groundY, safeBound, boundConfirmed //安全区域应该在扣除每种类型柜子大概的长宽的一半 let hue = 0 let boxes = [] let standards = { cabinet: { widthNormal: { min: 0.6, max: 0.7 }, //widthNormal是不计宽还是厚度的平均宽度 height:{min: 0.7, max:2.5, standard:2}, }, air: { widthNormal: { min: 0.35, max: 0.7 }, width: { min: 0.6, max: 0.75 }, thick: { min: 0.3, max: 0.4 }, height: {min:1.4, max:1.95, standard: 1.8}, //standard是通常出现的最高高度 }, 'air-hanging': { widthNormal: { min: 0.3, max: 1}, width: { min: 0.8, max: 1.1 }, thick: { min: 0.2, max: 0.3 }, height: { min: 0.3, max: 0.5, standard: 0.4}, bottom: { min: 0.8, max: 2.0 }, // 不绝对,大部分 }, battery: { widthNormal: { min: 0.4, max: 1.2}, width: { min: 0.7, max: 1.4 }, thick: { min: 0.35, max: 0.5 }, height:{min:0.3, max:1.3, standard:1.1 }, //maxHeight //有的电池很小。考虑是否追加battery-little 并且限制大电池的长宽高比例 }, rowBigBox: { widthNormal: { min: 0.5, max: Infinity }, height: {min: 0.7, max:2.4, standard:2}, }, } let addLabel = (pos, text, { bgcolor, a } = {}) => { if(version != 'vision')return bgcolor = bgcolor ? new THREE.Color(bgcolor) : { r: 1, g: 1, b: 1 } let textMesh = new TextSprite({ text: text || 'aaaaaa', textColor: { r: 0, g: 0, b: 0, a: 1 }, backgroundColor: { r: bgcolor.r * 255, g: bgcolor.g * 255, b: bgcolor.b * 250, a: a || 0.8 }, margin: { x: 5, y: 5 }, borderRadius: 0, player: player, }) textMesh.position.copy(pos) textMesh.scale.set(0.3, 0.3, 0.3) meshGroup.add(textMesh) return textMesh } let getBoxPos = info => { return (info.preDealRes && info.preDealRes.position) || info.center || (info.type == 'air-hanging' ? info.posAtWall : info.btmPos) || info.posAtWall } let updateBoxType = info => { let type = info.category || info.box0.category if(info.sid == "pano4-0"){ console.log(3) } if (type == 'air') { let btm = info.btmPos || info.btmPos //btmPosAtWall if(!btm){ btm = getBoxBtm(info ) } if (!btm) return let center = info.posAtWall || (info.preDealRes && info.preDealRes.position) || info.center const s = standards['air-hanging'] if (btm.y - groundY > s.bottom.min){ let h0 = btm.y - groundY let h1 = (modelBound.max.y - center.y) /(modelBound.max.y-modelBound.min.y) let h2 = center.y-btm.y let score = h0 * 2 - h1*3 - h2 * 3 if(score > 0){ type = 'air-hanging' } //console.error( score, h0,h1,h2, info.sid||info.name) } //注意:如果air被遮住底部,露出的部分只有一点,还是有可能被识别成air-hanging。只能希望 //console.error( type, info.sid||info.name) } if (info.box0) { info.boxType = /* info.box0.type = */ type //info.box1 && (info.box1.type = type) //因为box0和box1不一定匹配,所以不能直接赋值 } else { info.type = type } } let preDealBox = matchInfo => { if (matchInfo.preDealRes || !matchInfo.center) return matchInfo.preDealRes = {} matchInfo.boxType || updateBoxType(matchInfo) let minWidth = standards[matchInfo.boxType].widthNormal.min let needGetPose if (matchInfo.str && matchInfo.str.includes('outsideBound')) { const shrink = minWidth * 0.85 //addLabel(matchInfo.center, '原') let finalPos = matchInfo.center.clone().clamp(safeBound.min, safeBound.max) matchInfo.preDealRes.position = finalPos //addLabel(finalPos, 'finalPos') updateBoxType(matchInfo) needGetPose = true } let center = getBoxPos(matchInfo) if (needGetPose || !matchInfo.boxposes) { matchInfo.boxposes = [] ;[matchInfo.box0, matchInfo.box1].forEach(box => { box && matchInfo.boxposes.push(getBoxPoseByPos(box, center)) }) } //----------------- /* if(matchInfo.name == "pano0-4&pano2-1"){ console.log(4) } */ let xProp, yProp if (matchInfo.boxType == 'battery' || matchInfo.boxType == 'air-hanging' || matchInfo.boxType == 'air') { //根据比例判断 /* let r1 = Math.abs((center.x - skyBoxTight.position.x) / (center.z - skyBoxTight.position.z)) let r2 = player.model.size.x / player.model.size.z if(!math.closeTo(r1,r2, 0.05)){ if (r1 { let score = 0 const minDis = 1.5 boxposes.forEach(pose => { pose.lowR = pose.dis < minDis ? pose.dis / minDis : 1 //太近的话误差大 if (pose.projectWidth > pose.maxProjectWidth) { score += Math.pow((pose.projectWidth - pose.maxProjectWidth) * pose.lowR * 5, 2) //超过的话数字较大所以乘的数小一些 } else if (/* isSingle && */pose.projectWidth < pose.minProjectWidth) { score += Math.pow((pose.minProjectWidth - pose.projectWidth) * pose.lowR * 10, 2) } }) score = Math.min(score,500) //压低一点,因为得的宽度可能不准 if(boxposes.length == 2){ //每一个方向对应有四个方向(每个象限一个)看到的projectWidth应该接近。 //先把camTangent转化为第一个象限的 let camTangent0 = new THREE.Vector2(Math.abs(boxposes[0].camTangent.x), Math.abs(boxposes[0].camTangent.y)) let camTangent1 = new THREE.Vector2(Math.abs(boxposes[1].camTangent.x), Math.abs(boxposes[1].camTangent.y)) let a = camTangent0.dot(camTangent1) if(a>0.8){ //0.9: 25度之内. 0.8: 36.8度之内 let diff = Math.abs(boxposes[0].projectWidth - boxposes[1].projectWidth) boxposes.score2 = a * diff * 1000 * boxposes[0].lowR * boxposes[1].lowR score += boxposes.score2 //console.warn('在同一个方向看到的projectWidth应该接近。 ', diff) } boxposes.camTangentCos = a } score = Math.min(score,1000) return -score } let getBoxSize = info => { if (info.size) return //console.warn('开始算 ' + info.name) let exStr = '', warnStr = '' let x, y //求对角线的向量 x>0,y>0 //假设盒子的长宽为x,y (x>0,y>0),视线切线单位向量为(k,m),投影距离:x'k+y'm. //由于盒子的对角线有四个可选方向,(类似四个象限) 则需要能使投影距离最长的一个对角线向量。 //如,当k<0,m>0时,要使xk+ym 最大,必有x<0,y>0. 故 x = -x', y = y', 故 投影距离:x'k+y'm = x(-k)+ym 。 //故无论km的符号如何,只要变为正数,再去联立方程即可得xy。 //注:但是因为无法获取准确的投影距离(角平分线左右两边的端点到角平分线的距离不相等,垂足也无法确定),所以所算的误差非常大。 let center = getBoxPos(info) let oriX, oriY if(info.predictSize){ x = oriX = info.predictSize.x, y = oriY = info.predictSize.y }else{ if (info.box1) { let x1 = info.boxposes[0].camTangent.x, x2 = info.boxposes[1].camTangent.x, y1 = info.boxposes[0].camTangent.y, y2 = info.boxposes[1].camTangent.y, w1 = info.boxposes[0].projectWidth, //Math.max(minProjectWidth1, this.boxposes[0].projectWidth), w2 = info.boxposes[1].projectWidth //Math.max(minProjectWidth2, this.boxposes[1].projectWidth) //如果识别到柜门上,(柜体被遮住了),整体中心就会在柜门上,且厚度小于真实值。 if (x1 == 0) { y = w1 x = (w2 - y2 * y) / x2 } else {//联立方程得: y = (w2 - (x2 / x1) * w1) / (y2 - (x2 / x1) * y1) x = (w1 - y1 * y) / x1 } //console.log('xy', { x, y }) ;(x < 0.3 || x > 1.4) && (exStr += ' x:' + math.toPrecision(x, 2)) ;(y < 0.3 || y > 1.4) && (exStr += ' y:' + math.toPrecision(y, 2)) if (y < 0 || x < 0) { //console.log('<0 ?????????') warnStr = x < 0 ? 'x<0!' : 'y<0!' } ;(oriX = x), (oriY = y) } else { //single pano data x = oriX = info.boxposes[0].maxX //姑且取最大值 y = oriY = info.boxposes[0].maxY } let o = restrictSize(x, y, info) ;(x = o.x), (y = o.y) //按正常来说,得到的x,y都应>0,但是由于箱子会被遮挡,导致投影宽度比真实的小,算出的也不准,可能是负数 //所以手动将过小的宽度矫正 } /* if(info.name == "pano2-6"){ console.log(7) } */ let height if (info.boxType == 'air-hanging') { //挂式空调最好把长宽固定。 不过极少出错 //center.y -= 0.1 //很可能过高 height = standards[info.boxType].height.standard getBoxBtm(info) let d = center.y - info.btmPos.y center.y -= THREE.MathUtils.clamp((d - height/2)/2,-0.1,0.1) //如果中心点到底部的距离和height的一半不同,中心点移动差值的一半 } else { if (!info.topPos) getBoxTop(info) let {min,max} = standards[info.boxType].height height = THREE.MathUtils.clamp(info.topPos.y - groundY, min, max) } info.size = new THREE.Vector3(x, height, y) if (info.name == 'pano2-2&pano4-2') { console.log(6) } info.sizeAdjust = Math.pow(Math.abs(x - oriX), 1.3) + Math.pow(Math.abs(y - oriY), 1.3) //计算得到的值和标准值之间的差距,可以反映该info的匹配分值 if (info.sizeAdjust) info.score = (info.score || 0) - Math.min(info.sizeAdjust * 100, 400) info.size.oriX = oriX, info.size.oriY = oriY ;(info.exStr = exStr), (info.warnStr = warnStr) } let restrictSize = (x, y, info) => { let s if (info.boxType == 'battery' || info.boxType == 'air-hanging' || info.boxType == 'air') { let { xProp, yProp } = info if(xProp != void 0){ var { min, max } = standards[info.boxType][xProp] x = THREE.MathUtils.clamp(x, min, max) var { min, max } = standards[info.boxType][yProp] y = THREE.MathUtils.clamp(y, min, max) s = true } } if(!s){ var { min, max } = standards[info.boxType].widthNormal x = THREE.MathUtils.clamp(x, min, max) y = THREE.MathUtils.clamp(y, min, max) if(info.boxposes){ x = Math.min(x, ...info.boxposes.map(e=>e.maxX)) y = Math.min(y, ...info.boxposes.map(e=>e.maxY)) } } return { x, y } } let getMixBox = (box0, box1) => { //重叠部分 let box = new THREE.Box2() box.min.set(Math.max(box0.min.x, box1.min.x), Math.max(box0.min.y, box1.min.y)) box.max.set(Math.min(box0.max.x, box1.max.x), Math.min(box0.max.y, box1.max.y)) return box } const axises = [new THREE.Vector3(-1,1,-1),new THREE.Vector3(1,1,-1),new THREE.Vector3(1,1,1),new THREE.Vector3(-1,1,1), new THREE.Vector3(-1,-1,-1),new THREE.Vector3(1,-1,-1),new THREE.Vector3(1,-1,1),new THREE.Vector3(-1,-1,1)] class Box {//结果 constructor(info) { //preDealBox(info) this.setFromInfo(info) if(version == 'vision') this.draw() boxes.push(this) } setFromInfo(info) { for (let i in info) { this[i] = info[i] } let h = info.size.y let standardH = standards[info.boxType].height.standard if (h > standardH) { h = standardH + Math.log(1 + (h - standardH) / 2) //Math.log2: 以2为底的对数 ,Math.log:自然对数 info.size.y = h } this.name = this.boxType + '-' + this.name let position let center = getBoxPos(this) if (this.boxType == 'air-hanging') { position = center } else { position = center.clone().setY(groundY + this.size.y / 2) //使着地 } this.position = position let bound = new THREE.Box3() bound.expandByPoint(position) bound.expandByVector(new THREE.Vector3(this.size.x / 2, this.size.y / 2, this.size.z / 2)) this.bound = bound } draw() { hue += 0.06 var color = new THREE.Color().setHSL(hue, 1, 0.75) this.boxHelper = new THREE.Box3Helper(this.bound, color) this.boxHelper.material.depthTest = false this.boxHelper.material.transparent = true this.boxHelper.renderOrder = 30 let { warnStr, exStr } = this warnStr && (exStr += `【${warnStr}】`) this.label = addLabel(this.position, exStr ? [this.name, exStr] : this.name, { bgcolor: color }) meshGroup.add(this.boxHelper) } dispose() { let index = boxes.indexOf(this) if (index > -1) { boxes.splice(index, 1) if(version == 'vision'){ this.label.sprite.material.opacity = 0.2 this.boxHelper.material.opacity = 0.1 } } } toJson(){ let json = { points: axises.map(axis=>new THREE.Vector3().addVectors(this.position,this.size.clone().multiply(axis).multiplyScalar(0.5)).toArray()), category:this.boxType, sid: this.name } return json } } class cabinet extends Box { constructor() { super() } } // 2d坐标转3d坐标 let getDirByUV = (uv, pano) => { // 计算方向向量 let yaw = -uv.x * (Math.PI * 2) let pitch = Math.PI / 2 - uv.y * Math.PI let dir = new THREE.Vector3() dir.copy(Vectors.RIGHT).applyAxisAngle(Vectors.BACK, pitch).applyAxisAngle(Vectors.UP, yaw).applyQuaternion(pano.quaternion) return dir } let getCenterDir = (box) => { if (box.centerDir) return //假设不存在在box中间拍摄的情况,所以y不会横跨两边 let bbox = box.bbox2 let center = { x: getBbox2center(bbox[0], bbox[2]), y: (bbox[1] + bbox[3]) / 2 } box.bbox2CenterX = center.x let dir = getDirByUV(center, box.pano) box.centerDir = dir let centerTop = { x: center.x, y: bbox[1] } box.centerTopDir = getDirByUV(centerTop, box.pano) let centerBtm = { x: center.x, y: bbox[3] } box.centerBtmDir = getDirByUV(centerBtm, box.pano) } let getOtherPos = box=>{ if(!boundConfirmed){ ray.set(box.pano.position, box.centerBtmDir) box.btmPosPredict = ray.ray.intersectPlane(groundPlane, new THREE.Vector3()) //没有的话就在空中 (部分air-hanging也会有) if(box.btmPosPredict){ const depth = box.category == 'cabinet' ? 0.5 : 0.4 let dir2d = new THREE.Vector2(box.centerBtmDir.x, box.centerBtmDir.z).normalize().multiplyScalar(depth) box.btmPosPredict.x += dir2d.x box.btmPosPredict.z += dir2d.y //addLabel(box.btmPosPredict,'b_'+box.category+"_"+box.sid, {bgcolor:'#6ff',a:0.1}) //box.btmPosPredict.clamp(safeBound.min, safeBound.max) } return } if(!box.btmPos) { getBoxBtm(box) } if(!box.topPos){ getBoxTop(box) } if (!box.posAtWall && (box.category == 'battery' || box.category == 'air')) { const shrink = 0.4 ray.set(box.pano.position, box.centerDir) let o = ray.intersectObjects([skyBoxTight]) box.posAtWall = new THREE.Vector3().addVectors(box.pano.position, box.centerDir.clone().multiplyScalar(o[0].distance - shrink)) //因墙壁不准确,所以还是尽量不用墙的位置 /* if(box.btmPos){ let wallRatio = 0.5; if(new THREE.Vector3().subVectors(box.btmPos, box.posAtWall).setY(0).length() > 1 )wallRatio = 0.2 //可能墙壁位置不准,靠后了 box.predictCenter = new THREE.Vector3().addVectors(box.btmPos.clone().multiplyScalar(1-wallRatio), box.posAtWall.clone().multiplyScalar(wallRatio)) //box.predictCenter = new THREE.Vector3().addVectors(box.posAtWall, box.btmPos).multiplyScalar(0.5) //也许能当中心点? 虽然y会低一些 addLabel(box.predictCenter, box.sid+'-preC') } */ } updateBoxType(box) } let getUVs = box => { if (box.bbox2) return let uvs = [] box.bbox2 = box.bbox.map((e, i) => { return i % 2 == 0 ? e / 4096 /* + 0.25 */ : e / 2048 }) } let getBoxBase = box => { getUVs(box) getCenterDir(box) getOtherPos(box) } let getBbox2Diff = (x1, x2) => {//获取x1-x2,如果x1在x2右边则为正 if (Math.abs(x1 - x2) < 0.5) return x1 - x2 else { if (x1 > x2) x1 -= 1 else x2 -= 1 return x1 - x2 } } let getBbox2center = (x1, x2) => { //找中间位置 if (Math.abs(x1 - x2) > 0.5) { //永远找小于180度的那一边 return (x1 + x2 + 1) / 2 //另外半边 } else { return (x1 + x2) / 2 } } let getBoxPoseByPos = (box, centerPos, addDis = 0) => { //当得知box的大概位置时,求box在这个角度上的宽度、朝向 //在这个方向看的box的宽度 let angle = getBbox2Diff(box.bbox2[2], box.bbox2[0]) * Math.PI //角度的一半 let dis = new THREE.Vector3().subVectors(box.pano.position, centerPos).setY(0).length() + addDis let projectWidth = 2 * Math.tan(angle) * dis //投影宽度 (准确的投影宽度无法求得,只能近似) let camDir = /* new THREE.Vector2(centerPos.x-box.pano.position.x, centerPos.z-box.pano.position.z).normalize() */ box.centerDir.clone().setY(0).normalize() let camTangent = math.getNormal({ points: [ { x: 0, y: 0 }, /* camDir */ { x: camDir.x, y: camDir.z }, ], }) //视线切线方向 camTangent.x = Math.abs(camTangent.x) camTangent.y = Math.abs(camTangent.y) if(box.sid == 'pano8-6'){ console.log(7) } let maxWidth = standards[box.type].widthNormal.max let minWidth = standards[box.type].widthNormal.min let maxProjectWidth = (camTangent.x + camTangent.y) * maxWidth //该角度下该类型允许的最大投影距离 let minProjectWidth = (camTangent.x + camTangent.y) * minWidth let maxX = projectWidth / camTangent.x //可得x的最大值(假设y为0) let maxY = projectWidth / camTangent.y //可得y的最大值(假设x为0) return { projectWidth, camTangent, maxProjectWidth, minProjectWidth, maxX, maxY, dis} } let getBoxTop = info => { if(info.box1){ let o2 = getIntersect2(info.box0.pano.position, info.box0.centerTopDir, info.box1.pano.position, info.box1.centerTopDir) info.topPos = o2.pos3d info.diffHeight = o2.mid2 ? o2.mid2.distanceTo(o2.mid1) : 1 }else{ //取btm上方对应的位置 ( 因为和skybox的交点会因离墙远而偏上或偏下) let box = info.box0 || info if(box.sid == 'pano2-2'){ console.log(3) } let btm = box.btmPos if(!btm){ btm = getBoxBtm(info) } box.topPos = btm.clone() //xz同btm,要求y let xDelta = btm.x - box.pano.position.x let yDelta = xDelta * box.centerTopDir.y / box.centerTopDir.x box.topPos.y = yDelta + box.pano.position.y let minHeight = info.boxType ? standards[info.boxType].height.min : box.category == 'air' ? 0.5 : standards[box.category].height.min let diffH = Math.max(box.topPos.y - btm.y, minHeight) box.topPos.y = btm.y + diffH info.topPos = box.topPos } return info.topPos } let getBoxBtm = info => { if (info.box1) { let o2 = getIntersect2(info.box0.pano.position, info.box0.centerBtmDir, info.box1.pano.position, info.box1.centerBtmDir) info.btmPos = o2.pos3d } else { let box = info.box0 || info if(!box.btmPos){ ray.set(box.pano.position, box.centerBtmDir) let o = ray.intersectObjects([skyBoxTight]) //如果skybound有问题,位置就会错 box.btmPos = o[0].point.clone() const depth = Math.abs(o[0].face.normal.y) > 0.9 ? 0.4 : -0.4 let dir2d = new THREE.Vector2(box.centerBtmDir.x, box.centerBtmDir.z).normalize().multiplyScalar(depth) box.btmPos.x += dir2d.x box.btmPos.z += dir2d.y //addLabel(box.btmPos,'b_'+box.sid,{bgcolor:'#ff4399'}) } info.btmPos = box.btmPos } return info.btmPos } let getIntersect = (pano0Pos, dir0, pano1Pos, dir1) => { let pos0 = new THREE.Vector3().addVectors(pano0Pos, dir0) let pos1 = new THREE.Vector3().addVectors(pano1Pos, dir1) /* var {pos3d, mid1, mid2, behind} = math.getLineIntersect({ A: pano0Pos.clone(), B: pano1Pos.clone(), p1: pos0, p2: pos1 }) */ //三维线若接近平行,算出的交点可能很近,不是实际应该无交点才对 var pos2d = math.isLineIntersect( [ { x: pano0Pos.x, y: pano0Pos.z }, { x: pos0.x, y: pos0.z }, ], [ { x: pano1Pos.x, y: pano1Pos.z }, { x: pos1.x, y: pos1.z }, ], true ) //优先考虑水平面方向的交点 if (pos2d) { let y0 = ((pos2d.x - pano0Pos.x) * dir0.y) / dir0.x + pano0Pos.y let y1 = ((pos2d.x - pano1Pos.x) * dir1.y) / dir1.x + pano1Pos.y //console.log(y1-y0) let pos3d = new THREE.Vector3(pos2d.x, (y0 + y1) / 2, pos2d.y) return { pos3d, diffHeight: Math.abs(y0 - y1) } //diffHeight越小越好 } } //究竟哪个比较准 - - 可能两个都判断? let getIntersect2 = (pano0Pos, dir0, pano1Pos, dir1) => { let pos0 = new THREE.Vector3().addVectors(pano0Pos, dir0) let pos1 = new THREE.Vector3().addVectors(pano1Pos, dir1) let o = math.getLineIntersect2({ A: pano0Pos.clone(), B: pano1Pos.clone(), p1: pos0, p2: pos1, dir0, dir1 })//不用getLineIntersect,因为这个针对热点写的,当无交点时选用的点不是想要的 if (!o.pos3d) { console.error('getIntersect2 no result? ?') } return o } let searchPair = (beginItem, group0_, group1_, parentPairs, resultPairs) => { let pair = [] //只能是来自group0的 if (parentPairs) { //元结点裂变出多个,来装新的pair let i = resultPairs.indexOf(parentPairs) resultPairs.splice(i, 1) } for (let j = 0; j < group1_.length; j++) { pair = [beginItem, group1_[j]] let newPairs //用来存放该组pair if (parentPairs) { newPairs = parentPairs.slice(0) //复制 newPairs.push(pair) } else { newPairs = [pair] //新的容器 } resultPairs.push(newPairs) let newGroup0 = group0_.slice(0) let newGroup1 = group1_.slice(0) let index = newGroup0.indexOf(pair[0]) newGroup0.splice(index, 1) index = newGroup1.indexOf(pair[1]) newGroup1.splice(index, 1) if (newGroup0.length > 0 && newGroup1.length > 0) { searchPair(newGroup0[0], newGroup0, newGroup1, newPairs, resultPairs) } } } let getLeftRight = boxArr => {//获取pano的boxes中最左和最右的bbox.x let lefts = boxArr.map(e => e.bbox2[0]) let rights = boxArr.map(e => e.bbox2[2]) lefts.sort((a, b) => getBbox2Diff(a, b)) rights.sort((a, b) => getBbox2Diff(b, a)) let leftX = lefts[0] //最左 let rightX = rights[0] //最右 return { leftX, rightX, } } export default class PanoBoxFrame extends THREE.Group { constructor(player_, ifAnalyze, dataList) { super() player = player_ player.model.add(this) this.ifAnalyze = ifAnalyze this.wireframes = new THREE.Object3D() this.wireframes.name = 'wireframes' this.add(this.wireframes) this.matchScoreMap = {} this.bindEvents() meshGroup = new THREE.Object3D() meshGroup.name = 'testBox' this.add(meshGroup) this.compute(dataList) } async compute(dataList) { this.datas = {} this.datasMixed = {} this.boxes = boxes let compu = 0 let beginCompute = () => { //获取匹配分数 let getMatchScore = (box0, box1, { isSingle, center, onlyGet, dontCheckDis } = {}) => { let name0 = box0.sid + '&' + box1.sid let name1 = box1.sid + '&' + box0.sid let matchInfo0 = this.matchScoreMap[box1.category][name0] let matchInfo1 = this.matchScoreMap[box1.category][name1] let matchInfo = matchInfo0 || matchInfo1 if (onlyGet) return matchInfo let name if (!matchInfo) { name = name0 matchInfo = { name, box0, box1, center } this.matchScoreMap[box1.category][name] = matchInfo }else{ return matchInfo } if (name == 'pano2-1&pano0-0') { console.log(1) } getBoxBase(box0) getBoxBase(box1) let A = box0.pano.position.clone() let B = box1.pano.position.clone() let AB = new THREE.Vector3().subVectors(B, A) let AB2d = new THREE.Vector2(AB.x, AB.z).normalize() let AP12d = center ? new THREE.Vector2(center.x - A.x, center.z - A.z).normalize() : new THREE.Vector2(box0.centerDir.x, box0.centerDir.z).normalize() let BP22d = center ? new THREE.Vector2(center.x - B.x, center.z - B.z).normalize() : new THREE.Vector2(box1.centerDir.x, box1.centerDir.z).normalize() let angleA = Math.acos(AB2d.dot(AP12d)) let angleB = Math.PI - Math.acos(AB2d.dot(BP22d)) let score = 100, str = [] if (angleA + angleB > Math.PI + 0.2) {//无交点(比180大是因为中心角度有误差,所以给一定的容错) //console.log(`${panoId0}的第${box0.index}个与${panoId1}的第${box1.index}个因角度大于180度 不匹配`) return Object.assign(matchInfo, { score: -5000, str: ['angle>180'] }) } if (box0.type != box1.type) { return Object.assign(matchInfo, { score: -5000, str: ['typeNotSame'] }) } if (matchInfo.dirAngleXZ == void 0) { matchInfo.dirAngleXZ = THREE.MathUtils.radToDeg(Math.acos(AP12d.dot(BP22d))) //需要尽量接近90度算出来的交点会比较准 //matchInfo.minAng = Math.min(180 - matchInfo.dirAngleXZ, matchInfo.dirAngleXZ) if (isSingle) { //单个匹配单个,而非多个匹配多个(没有固定到两个漫游点),所以可以直接寻找最优角度 score += Math.sin(THREE.MathUtils.degToRad(matchInfo.dirAngleXZ)) * 300 score += matchInfo.dirAngleXZ //另外角度越大越不容易偏向一边 } } let shinkRatio = 1 if(!dontCheckDis){ let r = box0.type == 'air' ? 1 : box0.type == 'cabinet' ? 0.9 : 0.5 //随着宽度增加而降低 if(box0.type != 'air-hanging' && box0.btmPos && box1.btmPos) { //注:挂空调不应使用btmPosPredict let d = box0.btmPos.distanceToSquared(box1.btmPos) /* if(d > 0.6) { if(box0.type == 'battery' || box0.predictLen > 0.7 || box1.predictLen > 0.7){ //如果是相对着的角度,距离可以比较远 shinkRatio = math.linearClamp(0.8 || (box0.predictLen+box1.predictLen)/2-0.6, 0.8, 4 , 0.7, 0.4) } } */ matchInfo.btmPosPreDis = d score -= d * 1500 * r * shinkRatio let a = box0.topPos.distanceToSquared(box1.topPos) matchInfo.topPosPreDis = a let u = a * 700 * r * shinkRatio let AP0 = new THREE.Vector2(box0.btmPos.x - A.x, box0.btmPos.z - A.z).lengthSq() let AP1 = new THREE.Vector2(box1.btmPos.x - B.x, box1.btmPos.z - B.z).lengthSq() if(AP0<0.4 || AP1<0.4)u *= 0.3 //太近 score -= u } else if (box0.posAtWall && box1.posAtWall) { let d = box0.posAtWall.distanceToSquared(box1.posAtWall) matchInfo.wallPosPreDis = d score -= d * 500 * r //墙面不准所以分低 } } //检查从两处看高度是否一致 /* getBoxTop(matchInfo) score -= Math.pow(matchInfo.diffHeight * 10, 2) // 0.1处是1 */ //检查高度的一半(从 顶部到中心 与 中心到地板 的距离是否差别太大) /* let diffHalfHeight if (box0.type == 'air-hanging') { //diffHalfHeight = 0 //if(matchInfo.topPos.y - center.y > 0.5)score -= Math.pow((matchInfo.topPos.y - center.y )*5, 4) } else { diffHalfHeight = Math.abs(matchInfo.topPos.y - matchInfo.center.y - (matchInfo.center.y - groundY)) score -= Math.pow(diffHalfHeight * 3, 4) } */ if (!matchInfo.center) { let o = getIntersect2(A, box0.centerDir, B, box1.centerDir) matchInfo.center = o.pos3d.clone() /* if (name == 'pano0-1&pano2-0') { console.log(1) var line1 = LineDraw.createLine([A, A.clone().add(box0.centerDir.clone().multiplyScalar(40))]) var line2 = LineDraw.createLine([B, B.clone().add(box1.centerDir.clone().multiplyScalar(40))]) meshGroup.add(line1) meshGroup.add(line2) addLabel(matchInfo.center, matchInfo.name + '-c') } */ let dir0 = new THREE.Vector3().subVectors(o.pos3d, A).normalize() let dir1 = new THREE.Vector3().subVectors(o.pos3d, B).normalize() let sum = dir0.dot(box0.centerDir) + dir1.dot(box1.centerDir) let wrongDir = sum < 1.9 score -= (2 - sum) * 10000 if (wrongDir) { str.push('wrongDir') return Object.assign(matchInfo, { score: score - 5000, str }) } let minAng = Math.min(180 - matchInfo.dirAngleXZ, matchInfo.dirAngleXZ)//角度小的getIntersect2容易算不准 if(!dontCheckDis && box0.type != 'air-hanging' && box0.btmPos && box1.btmPos){ let p0 = new THREE.Vector2(box0.btmPos.x, box0.btmPos.z) let p1 = new THREE.Vector2(box1.btmPos.x, box1.btmPos.z) let p = new THREE.Vector2(matchInfo.center.x,matchInfo.center.z) let dis = p0.distanceToSquared(p) + p1.distanceToSquared(p) let s = math.linearClamp(minAng, 0, 10, 0, 1) score -= dis * 1000 * s //如果距离较远就说明算的center误差大,不可信。可能有一个框不准确 matchInfo.centerDrift = dis } getBoxBtm(matchInfo) //靠墙的有这些属性 可以帮助预测中心点 let predict0 = box0.type != 'air-hanging' && box0.btmPos || matchInfo.topPos let predict1 = box1.type != 'air-hanging' && box1.btmPos || matchInfo.topPos let cr = math.linearClamp(minAng, 0, 10, 0.05, 0.4) //getIntersect2结果的权重 if (predict0 && predict1) { matchInfo.center = new THREE.Vector3().addVectors( matchInfo.center.clone().add(matchInfo.btmPos).multiplyScalar(1/2* cr), predict0.clone().add(predict1).multiplyScalar(1/2 * (1-cr)) ).setY(o.pos3d.y) } //addLabel(matchInfo.center, matchInfo.name + '-c') } updateBoxType(matchInfo) if(matchInfo.boxType != box0.type || matchInfo.boxType != box1.type){ score -= 1000, str.push('typeNotSame2') } if (!safeBound.containsPoint(matchInfo.center)) { let dis = safeBound.distanceToPoint(matchInfo.center) //console.log(`${panoId0}的第${box0.index}个与${panoId1}的第${box1.index}个因中心点在bounding外不匹配`, center, 'dis: ' + dis) ;(score -= 1000 * dis * dis), (str.push('outsideBound')) Object.assign(matchInfo, { score, str, center: matchInfo.center, disToBound: dis }) if (dis > 0.5) return matchInfo } /* if (name == 'pano20-3&pano18-5') { console.log(1) let color = '#fe1' var line1 = LineDraw.createLine([A, A.clone().add(box0.centerBtmDir.clone().multiplyScalar(40))], { color }) var line2 = LineDraw.createLine([B, B.clone().add(box1.centerBtmDir.clone().multiplyScalar(40))], { color }) meshGroup.add(line1) meshGroup.add(line2) addLabel(matchInfo.btmPos, 'btmPos') } */ //检查宽度 let boxposes let checkWidth = () => { boxposes = [] ;[box0, box1].forEach(box => { let pose = getBoxPoseByPos(box, matchInfo.center) boxposes.push(pose) //如果超出标准,基本上这二者不匹配;但过小的话,有可能是被遮挡所以残缺,因此不予过滤 }) } checkWidth() /* let overMaxs = boxposes.filter(e => e.projectWidth > e.maxProjectWidth) if ( // matchInfo.dirAngleXZ > 160 && (overMaxs.length == 1 && Math.abs(boxposes[0].maxProjectWidth - boxposes[0].projectWidth - (boxposes[1].maxProjectWidth - boxposes[1].projectWidth)) > 0.8) || (matchInfo.dirAngleXZ < 20 && overMaxs.length == 2) ) { //一大一小 //中心偏向一点,如果能移回一些就好了,现在暂时先用topPos的xz。 因为向上的话,侧面角度大,水平角度差影响会小。但如果有一边不准也是不准。 ;(matchInfo.center.x = matchInfo.topPos.x), (matchInfo.center.z = matchInfo.topPos.z) //y要变吗 //console.log('修改中心') checkWidth() }*/ score += getPoseScore(boxposes, isSingle) //根据投影信息预测的长度再得匹配分数 compu++ return Object.assign(matchInfo, { score: score, str, /* diffHalfHeight, */ boxposes }) /* preDealBox(matchInfo) getBoxSize(matchInfo) */ } this.rows = {} /* let getchainNext = (left, end, chain, boxes) => { chain.push(left) if (left == end) return boxes.chains.push(chain) let nodes = boxes.relationships.filter(pair => pair.includes(left)) let rights = nodes.map(pair => pair.find(e => e != left)) rights = rights.filter(e => !chain.includes(e) && boxes.indexOf(e) > boxes.indexOf(left)) rights.forEach(right => { getchainNext(right, end, chain.slice(), boxes) }) } */ let getPanoBigRowBox = (panoBoxes, { reason='row' } = {}) => {//将一个pano中的所有boxes分组,识别哪些是一排的。也可用于识别融合 let pano = panoBoxes[0].pano const category = panoBoxes[0].category let type = category+'|'+reason this.rows[type] || (this.rows[type] = {}) if (this.rows[type][pano.id]) return this.rows[type][pano.id] let bigBoxes let bigBox = { sid: 'pano' + pano.id + (reason == 'mix' ? '-mix' : '-row'), pano, category: 'rowBigBox', type: 'rowBigBox', } let rows = [] for (let i = 0; i < panoBoxes.length; i++) { let box0 = panoBoxes[i] getBoxBase(box0) let [left0, right0] = [box0.bbox2[0], box0.bbox2[2]] for (let j = i + 1; j < panoBoxes.length; j++) { let box1 = panoBoxes[j] getBoxBase(box1) if (box0.type != box0.type ) continue //类型不同 let [left1, right1] = [box1.bbox2[0], box1.bbox2[2]] let atEdge = (left1 < 0.00025 && right0 > 0.9997) || (left0 < 0.00025 && right1 > 0.9997) //在全景图的边界必会分两半 if (reason == 'mix' && box0.category == 'cabinet' && !atEdge) continue //柜子容易并排,尽量不融合 let d1 = getBbox2Diff(left1, right0), d2 = getBbox2Diff(left1, left0), d3 = getBbox2Diff(left0, right1) /* if(box0.sid == "pano10-1"){ console.log(9) } */ const min = reason == 'mix' ? 0.004 : 0.003 //mix代表寻找分裂的重新融合到一起 if ((d1 <= min && d2 >= min) || (d3 <= min && d2 <= min)) { //边框交接 const { min, max } = standards[box0.btmPos ? category : 'air-hanging'].widthNormal const w = (min + max) / 2 const tolerate = w * w * (reason == 'mix' ? 0.8 : 2.1) let p0 = getBoxPos(box0) let p1 = getBoxPos(box1) let dis = p0.distanceToSquared(p1) let disY if (reason == 'mix'){ if(atEdge) { dis -= tolerate * 0.8 //给many优势 }else{ disY = (Math.abs(box0.bbox2[3] - box1.bbox2[3]) + Math.abs(box0.bbox2[1] - box1.bbox2[1])) * 5 //边框纵向差异越大越不可能融合(虽然是可能有包含的情况啦,留到最后再combine。因为在侧面看,近处的box很容易嵌套远处的box,但它们不该融合) if (disY > 0.6) continue dis += disY let atEdgeMight = (left1 < 0.001 && right0 > 0.999) ? [left1,right0] : (left0 < 0.001 && right1 > 0.999) ? [left0,right1] : null //有在全景图的边界的可能性 let atEdgePossib = atEdgeMight? 0.0003/ (atEdgeMight[0] + (1-atEdgeMight[1])) : 0 // 两条线越接近越可能融合 dis -= atEdgePossib //给点点优势 } } /* if(box0.sid == "pano2-0"){ console.log('dis',dis,'tolerate',tolerate,[box0, box1],disY) } */ if (dis < tolerate) { //console.log('dis',dis,'tolerate',tolerate,[box0, box1],disY) /* let vec = new THREE.Vector3().subVectors(box0.btmPos,box1.btmPos) let k = Math.abs(vec.x / vec.z) console.log('pushToGroupAuto',k, box0.sid, box1.sid) */ common.pushToGroupAuto([box0, box1], rows) } } } } //一排箱子的角度范围不可超过180度,因为不可能站在箱子上拍,所以超过的话肯定有边缘的不在这一排中。 //可判断边缘箱子的是否角度偏大,一般中间的被遮挡所以偏小 rows.forEach(boxes => { //从左到右排序 boxes.sort((a, b) => { //但因有的box跨越到别的box区域,所以这个顺序不准确 return getBbox2Diff(a.bbox2CenterX, b.bbox2CenterX) }) }) //去除不在一条直线上的连接 rows.slice(0).forEach(boxes => { if (boxes.length >= 2) { let removes = [], bound = new THREE.Box2, size = new THREE.Vector2, maxW = 0.6 for (let i = 0, j = boxes.length; i < j; i++) { let box = boxes[i] let pos2d = new THREE.Vector2(box.btmPos.x, box.btmPos.z) bound.expandByPoint(pos2d) bound.getSize(size) let min = Math.min(size.x, size.y) if(min>maxW){ removes.push([boxes[i], boxes[i - 1]]) bound = new THREE.Box2 bound.expandByPoint(pos2d) console.log('removes',size) } //console.log('removes',k, box1.sid) } if (removes.length) { console.log( '去除错误row连接', removes.map(e => e.map(a => a.sid)) ) let { newGroups } = common.disconnectGroup(removes, rows) //if(newGroups.length>1){//分裂成多组了,重新计算 // console.log(newGroups) //} } } }) rows.forEach(boxes => { //从左到右重新排序 boxes.sort((a, b) => { //但因有的box跨越到别的box区域,所以这个顺序不准确 return getBbox2Diff(a.bbox2CenterX, b.bbox2CenterX) }) }) /* rows.slice(0).forEach(boxes => { if (boxes.length >= 2) { let removes = [], minDiff = 1, minDis = 0.4 let lastFirst = 0 for (let i = 1, j = boxes.length; i < j-1; i++) { let box0 = boxes[lastFirst], box1 = boxes[i] , box2 = boxes[i+1] let vec0 = new THREE.Vector3().subVectors(box0.btmPos,box1.btmPos) let vec1 = new THREE.Vector3().subVectors(box1.btmPos,box2.btmPos) let angle = vec0.angleTo(vec1) if(angle>minDiff){ removes.push([boxes[i], boxes[i + 1]]) lastFirst = i console.log('removes', angle) } } if (removes.length) { console.log( '去除错误row连接', removes.map(e => e.map(a => a.sid)) ) let { newGroups } = common.disconnectGroup(removes, rows) //if(newGroups.length>1){//分裂成多组了,重新计算 // console.log(newGroups) //} } } }) */ /* rows.slice(0).forEach(boxes => { //有的其实是分离的但也在边缘连上了,可以根据底部的差来判断,过大的很可能是分离的 if (boxes.length >= 2) { let diffs = [] for (let i = 0, j = boxes.length; i < j - 1; i++) { let box0 = boxes[i], box1 = boxes[i + 1] let p0 = box0.predictCenter || box0.btmPos || box0.posAtWall let p1 = box1.predictCenter || box1.btmPos || box1.posAtWall diffs.push(p0.distanceToSquared(p1)) } let minW = standards[category].widthNormal.min if(!boxes[0].btmPos)minW = standards['air-hanging'].widthNormal.min let mid = minW * minW let removes = [] diffs.forEach((e, i) => { if (e > mid * 2) { removes.push([boxes[i], boxes[i + 1]]) } }) if (removes.length) { console.log( '去除错误row连接', removes.map(e => e.map(a => a.sid)) ) let { newGroups } = common.disconnectGroup(removes, rows) //if(newGroups.length>1){//分裂成多组了,重新计算 // console.log(newGroups) //} } } }) */ rows.sort((a, b) => { return b.length - a.length }) //箱子数量从大到小排序 bigBoxes = rows.map((boxes, i) => { let { leftX, rightX } = getLeftRight(boxes) //最左 let btmY = boxes.slice().sort((a, b) => a.bbox2[3] - b.bbox2[3])[0].bbox2[3] let topY = boxes.slice().sort((a, b) => b.bbox2[1] - a.bbox2[1])[0].bbox2[1] let rowBigBox = Object.assign({}, bigBox, { boxes, bbox2: [leftX, topY, rightX, btmY], //整排的bbox left: boxes.find(e => e.bbox2[0] == leftX), right: boxes.find(e => e.bbox2[2] == rightX), }) let p0 = getBoxPos(rowBigBox.left) let p1 = getBoxPos(rowBigBox.right) let vec = new THREE.Vector2(p0.x - p1.x, p0.z - p1.z) rowBigBox.k = Math.abs(vec.x / vec.y) rowBigBox.predictLen = (rowBigBox.k > 1 ? Math.abs(vec.x) : Math.abs(vec.y)) + 0.6 //加入一个宽度 /* if(boxes.length <= boxes.relationships.length){//多条链 boxes.chains = [] getchainNext(left,right,[], boxes ) let aveAngle = (getBbox2Diff(left.bbox2[2], left.bbox2[0]) + getBbox2Diff(right.bbox2[2], right.bbox2[0]) ) / 2 -0.01 //首尾的angle平均数。但如果这两个不准那就导致整体出错了 let middleAngle = getBbox2Diff(right.bbox2[0], left.bbox2[2]) let counts = boxes.chains.map(e=>e.length) counts.sort((a,b)=>a-b) let min = counts[0],max = counts[counts.length-1] let r = [], cur = min; while(cur<=max){ r.push({cur, diff:Math.abs((middleAngle / (cur-2) - aveAngle)}) //加 0.01是因为增加边缘 cur++ } r.sort((a,b)=>a.diff-b.diff) rowBigBox.predictBoxCount = r[0].cur //--------- let goodCountChains = boxes.chains.filter(e=>e.length == rowBigBox.predictBoxCount) if(goodCountChains.length == 1) rowBigBox.bestChain = goodCountChains[0] else{ goodCountChains = goodCountChains.map((chain,i)=>{ let j = 1, diff=0 //中间的box的angle的方差 while(ja.diff-b.diff) rowBigBox.bestChain = goodCountChains[0].chain } console.log('getChains',boxes.chains, 'predictBoxCount',rowBigBox.predictBoxCount, r) } */ return rowBigBox }) if(reason != 'mix')panoBoxes.forEach(box => { //加入单个的 if (!rows.some(row => row.includes(box))) { let boxBig = Object.assign({}, bigBox, { bbox2: box.bbox2, boxes: [box], left: box, right: box, }) bigBoxes.push(boxBig) } }) bigBoxes.forEach(bigBox => { bigBox.sid += '-' + bigBox.boxes.map(e => e.index).join(',') /* if (bigBox.sid == 'pano0-rowBigBox-1,0,2,4') { console.log(3) } */ //取平均值 if (bigBox.boxes[0].btmPos) { bigBox.btmPos = bigBox.boxes.reduce((w, c) => w.add(c.btmPos), new THREE.Vector3()).multiplyScalar(1 / bigBox.boxes.length) //addLabel(bigBox.btmPos,'b_'+bigBox.sid, {bgcolor:'#f93',a:0.4}) } if (bigBox.boxes[0].topPos) { bigBox.topPos = bigBox.boxes.reduce((w, c) => w.add(c.topPos), new THREE.Vector3()).multiplyScalar(1 / bigBox.boxes.length) } if (bigBox.boxes[0].posAtWall) { bigBox.posAtWall = bigBox.boxes.reduce((w, c) => w.add(c.posAtWall), new THREE.Vector3()).multiplyScalar(1 / bigBox.boxes.length) } }) this.rows[type][pano.id] = bigBoxes //当前pano的所有row return bigBoxes } /* let getPanoBoxAngleTrend = rowBox => { //顺时针方向该pano的box角度范围是越来越大还是越来越小 let diffs = [] let angles = rowBox.boxes.map(box => getBbox2Diff(box.bbox2[2], box.bbox2[0])) for (let i = 0, j = angles.length; i < j - 1; i++) { //得所有相邻之间的差 let a0 = angles[i], a1 = angles[i + 1] diffs.push(a1 - a0) } diffs.sort((a, b) => a - b) return diffs[Math.floor(diffs.length / 2)] //中位数 } */ /* let getBoxCount = (rowBigBox)=>{ return rowBigBox.predictBoxCount || rowBigBox.boxes.length } */ let getReverseInfo = (rowBigBox0, rowBigBox1) => {//两个row的方向对应 let reversed = false let lefts = [rowBigBox0.left, rowBigBox1.left] let rights = [rowBigBox0.right, rowBigBox1.right] let dis0 = lefts[0].btmPos.distanceToSquared(lefts[1].btmPos) let dis1 = rights[0].btmPos.distanceToSquared(rights[1].btmPos) let dis2 = lefts[0].btmPos.distanceToSquared(rights[1].btmPos) let dis3 = rights[0].btmPos.distanceToSquared(lefts[1].btmPos) let posLeft2, posRight2 if (dis0 + dis1 > dis2 + dis3) {//距离近的代表是同一端 reversed = true posLeft2 = new THREE.Vector3().addVectors(lefts[0].btmPos, rights[1].btmPos).multiplyScalar(0.5) posRight2 = new THREE.Vector3().addVectors(rights[0].btmPos, lefts[1].btmPos).multiplyScalar(0.5) } else { posLeft2 = new THREE.Vector3().addVectors(lefts[0].btmPos, lefts[1].btmPos).multiplyScalar(0.5) posRight2 = new THREE.Vector3().addVectors(rights[0].btmPos, rights[1].btmPos).multiplyScalar(0.5) } let vec = new THREE.Vector2(posLeft2.x - posRight2.x, posLeft2.z - posRight2.z) let k = Math.abs(vec.x / vec.y) //这个算斜率更准,但位置容易偏向一侧(可能用边缘的bbox算会好些?) return { reversed, k } } let searchByRow = (groups, datas) => { //先查找row,匹配row,再slice row的方法 this.matchScoreMap['rowBigBox'] = {} let rowInfos = [] let getCenter = (rowBigBox0, rowBigBox1, ignoreCountMatch) => {//获取row间的匹配信息 //获取bigBox位置。由于一排的盒子比较长,中心方向误差大,所以采用先获取两边位置,再求中点的方法 //if (rowBigBox0.boxes.length != rowBigBox1.boxes.length && !ignoreCountMatch) return //太难了,不算不一样的情况了 if((rowBigBox0.sid + '&' + rowBigBox1.sid) in rowInfos){ return rowInfos[rowBigBox0.sid + '&' + rowBigBox1.sid] } if (rowBigBox0.boxes.length != rowBigBox1.boxes.length && !ignoreCountMatch) return //if (getBoxCount(rowBigBox0) != getBoxCount(rowBigBox1) && getBoxCount(rowBigBox0) != 1 && getBoxCount(rowBigBox1) != 1)return if (rowBigBox0.sid == 'pano0-row-1,2,6' && rowBigBox1.sid == 'pano2-row-0,2,6') { console.log(4) } let rowInfo if (rowBigBox0.boxes.length > 1 && rowBigBox1.boxes.length > 1) {//多对多,可以求两端的位置 let lefts = [rowBigBox0.left, rowBigBox1.left] let rights = [rowBigBox0.right, rowBigBox1.right] let leftInfo let rightInfo let info2 = getReverseInfo(rowBigBox0, rowBigBox1) let len0 = rowBigBox0.predictLen, //长度应该接近 len1 = rowBigBox1.predictLen let overLen = Math.abs(len0 - len1) /* / (rowBigBox0.boxes.length + rowBigBox1.boxes.length) * 5 */ if (overLen > 1){ console.error('overLen> 1', overLen, rowBigBox0.sid, '和', rowBigBox1.sid) return done() } if (info2.reversed) { leftInfo = getMatchScore(lefts[0], rights[1], { isSingle: true }) rightInfo = getMatchScore(rights[0], lefts[1], { isSingle: true }) } else { leftInfo = getMatchScore(lefts[0], lefts[1], { isSingle: true }) rightInfo = getMatchScore(rights[0], rights[1], { isSingle: true }) } let posLeft = getBoxPos(leftInfo) let posRight = getBoxPos(rightInfo) if (!posLeft || !posRight || leftInfo.score < -4000 || rightInfo.score < -4000){ return done()//漫游点重合、>180度会导致此问题 } preDealBox(leftInfo) //getBoxSize(leftInfo) preDealBox(rightInfo) //getBoxSize(rightInfo) posLeft = getBoxPos(leftInfo) posRight = getBoxPos(rightInfo) //验证是否是垂直或水平 let vec = new THREE.Vector2(posLeft.x - posRight.x, posLeft.z - posRight.z) let k = Math.abs(vec.x / vec.y) if ((info2.k > 1 && k < 1) || (info2.k < 1 && k > 1)) { console.error('请检查!info2.k > 1 && k < 1 || info2.k < 1 && k > 1', rowBigBox0.sid, '和', rowBigBox1.sid) //绘制的方向错误,尺寸错误 return done() } let wrongK = 0 if ((rowBigBox0.k > 1 && rowBigBox1.k < 1) || (rowBigBox0.k < 1 && rowBigBox1.k > 1)) { wrongK = rowBigBox0.k / rowBigBox1.k if(wrongK<1)wrongK = 1/wrongK } /* let trend0 = getPanoBoxAngleTrend(rowBigBox0) let trend1 = getPanoBoxAngleTrend(rowBigBox1) let judgeReverse = () => { //这个方法有时不准 let disLeftSquared0 = new THREE.Vector2(posLeft.x - rowBigBox0.pano.position.x, posLeft.z - rowBigBox0.pano.position.z).lengthSq() let disRightSquared0 = new THREE.Vector2(posRight.x - rowBigBox0.pano.position.x, posRight.z - rowBigBox0.pano.position.z).lengthSq() let a = trend0 * (disLeftSquared0 - disRightSquared0) if (a < 0 && Math.abs(a) > 0.1) return true let posLeft2 = reversed ? posRight : posLeft, //反向过的对第二个漫游点来说左右是反的 posRight2 = reversed ? posLeft : posRight let disLeftSquared1 = new THREE.Vector2(posLeft2.x - rowBigBox1.pano.position.x, posLeft2.z - rowBigBox1.pano.position.z).lengthSq() let disRightSquared1 = new THREE.Vector2(posRight2.x - rowBigBox1.pano.position.x, posRight2.z - rowBigBox1.pano.position.z).lengthSq() let b = trend1 * (disLeftSquared1 - disRightSquared1) if (b < 0 && Math.abs(b) > 0.1) return true } if (leftInfo.score < -2000 || rightInfo.score < -2000 || judgeReverse()) { //反向试试 leftInfo = getMatchScore(lefts[0], rights[1], { isSingle: true }) rightInfo = getMatchScore(rights[0], lefts[1], { isSingle: true }) posLeft = getBoxPos(leftInfo) posRight = getBoxPos(rightInfo) reversed = true //rowBigBox1 反向了 } if (leftInfo.score < -2000 || rightInfo.score < -2000 || judgeReverse()) { return console.log('getCenter ;两个方向都不符合', rowBigBox0.sid, rowBigBox1.sid) } */ /*const maxK = Math.max(0.6 / Math.sqrt(rowBigBox0.boxes.length), 0.2) // 最大斜率 if (k < maxK && k > 1 / maxK) { return //console.log('放弃,斜率', k) } */ //横的话,按x从小到大,竖的按z从小到大 if ((k < 1 && posLeft.z > posRight.z) || (k > 1 && posLeft.x > posRight.x)) { let temp = posRight ;(posRight = posLeft), (posLeft = temp) } //addLabel(posLeft, 'left-' + rowBigBox0.pano.id + '&' + rowBigBox1.pano.id, { a: 0.1 }) //addLabel(posRight, 'right-' + rowBigBox0.pano.id + '&' + rowBigBox1.pano.id, { a: 0.1 }) /* if (rowBigBox0.pano.id + '&' + rowBigBox1.pano.id == '22&26') { console.log(777) } */ /* var line1 = LineDraw.createLine([posLeft, posRight]) meshGroup.add(line1) */ //根据btmPos矫正一下中心位置, 否则容易偏漫游点这一侧 let center = new THREE.Vector3().addVectors(posLeft, posRight).multiplyScalar(0.5) center.add(rowBigBox0.btmPos).add(rowBigBox1.btmPos).multiplyScalar(1/3) let axis = k > 1 ? 'z' : 'x' //posLeft[axis] = center[axis], posRight[axis] = center[axis] let match = getMatchScore(rowBigBox0, rowBigBox1, { isSingle: true, center }) //是否预先传送center ? //rowInfo.minAngs = [leftInfo.minAng , rightInfo.minAng] if(match.name == 'pano20-row-8,10&pano16-row-6,0'){ console.log(8) } let sc = match.score - overLen * 1000 - wrongK*100 + leftInfo.score + rightInfo.score if (sc < -4000) { console.log('放弃,匹配分过低,可能不是一组', rowBigBox0.sid, '和', rowBigBox1.sid, sc) return done() } //console.log('getcenter', rowBigBox0.sid, '和', rowBigBox1.sid, overLen, match.score + overLen * 1000 + leftInfo.score + rightInfo.score) rowInfo = { rowBigBox0, rowBigBox1, match, k, posLeft, posRight, score: sc / 3 + 500 , //700 + match.score*0.7 + (leftInfo.score + rightInfo.score)*0.3 , reversed: info2.reversed, } } else { if (rowBigBox0.boxes.length == 1 && rowBigBox1.boxes.length == 1) { rowInfo = getMatchScore(rowBigBox0.boxes[0], rowBigBox1.boxes[0] /* , { isSingle: true } */) //直接匹配box } else { //一对多。getMatchScore计算误差大(长度越长中心误差越大、宽度计算也误差大)所以再写点限制。直接使用btm来预测长度和位置似乎更准 let mulBoxRow = rowBigBox0.boxes.length>1 ? rowBigBox0 : rowBigBox1 let singleBox = rowBigBox0.boxes.length==1 ? rowBigBox0 : rowBigBox1 /* if(rowBigBox0.sid == "pano12-row-3" && rowBigBox1.sid == "pano0-row-3,1,0" ){ console.log(5) } */ rowInfo = getMatchScore(rowBigBox0, rowBigBox1, { dontCheckDis:true }) //一对多 if(rowInfo.name == "pano2-row-6,4&pano4-row-3"){ console.log(5) } rowInfo.k = mulBoxRow.k if(rowInfo.center){ rowInfo.center.add(getBoxPos(mulBoxRow)).multiplyScalar(0.5) } rowInfo.predictSize = rowInfo.k > 1 ? {x : mulBoxRow.predictLen, y: 0.6} : {y : mulBoxRow.predictLen, x: 0.6} //单个的应该和多个的其中一端一样 let dis0 = getBoxPos(mulBoxRow.left).distanceToSquared(getBoxPos(singleBox)) let dis1 = getBoxPos(mulBoxRow.right).distanceToSquared(getBoxPos(singleBox)) let dis = Math.min(dis0, dis1) rowInfo.score -= dis * 1000 } } function done(rowInfo){ rowInfo && rowInfos.push(rowInfo) rowInfos[rowBigBox0.sid + '&' + rowBigBox1.sid] = rowInfo } done(rowInfo) return rowInfo } let matchGroups = [] let allRelations = [] let getK = info => { let k if (info.left) { let vec = new THREE.Vector2(info.left.x - info.right.x, info.left.z - info.right.z) k = Math.abs(vec.x / vec.y) } else { k = Math.abs(Math.max(info.size.x,0.6) / Math.max(info.size.z,0.6)) } return k } let ignoreCountMatch = groups.filter(e => e.length > 1).length == 1 let match = searchType => { if (searchType == 'second') ignoreCountMatch = true for (let i = 0; i < groups.length - 1; i++) { let rowBigBoxes_0 = getPanoBigRowBox(groups[i]) let pano0 = groups[i][0].pano if (searchType == 'second') rowBigBoxes_0 = rowBigBoxes_0.filter(e => !matchGroups.some(u => u.includes(e))) for (let j = i + 1; j < groups.length; j++) { let rowBigBoxes_1 = getPanoBigRowBox(groups[j]) if (searchType == 'second') rowBigBoxes_1 = rowBigBoxes_1.filter(e => !matchGroups.some(u => u.includes(e))) let pano1 = groups[j][0].pano let resultPairs = [] let bigBoxes_0, bigBoxes_1 if (rowBigBoxes_0.length < rowBigBoxes_1.length) { bigBoxes_0 = rowBigBoxes_1.slice() bigBoxes_1 = rowBigBoxes_0.slice() } else { bigBoxes_0 = rowBigBoxes_0.slice() bigBoxes_1 = rowBigBoxes_1.slice() } while (bigBoxes_1.length < bigBoxes_0.length) { bigBoxes_1.push({ sid: 'void' }) //为了使排列正确,补个空,使左右两边个数相等,过后和void匹配的不会计算box } if (!bigBoxes_0[0]) continue searchPair(bigBoxes_0[0], bigBoxes_0, bigBoxes_1, null, resultPairs) resultPairs = resultPairs.map(pairs => { let infos = pairs.map(pair => (pair.some(e => e.sid == 'void') ? null : getCenter(pair[0], pair[1], ignoreCountMatch))) //infos.sort((a,b)=>{return a.score-b.score}); let score = infos.reduce((s, e) => { return s + (e && e.score > -2000 ? e.score : -1000) //只考虑组成功的分数 }, 0) return { pairs, infos, score, name: pairs.map(pair => pair.map(item => item.sid).join(' & ')), } }) resultPairs.sort((a, b) => b.score - a.score) /* if (resultPairs[0].name[0].includes('pano8') && resultPairs[0].name[0].includes('pano0')) { console.log(111) } */ resultPairs[0].pairs.forEach((pair, i) => { let info = resultPairs[0].infos[i] if (info && info.score > -2000) { allRelations.push(info) let items = pair.filter(e => e.sid != 'void') common.pushToGroupAuto(items, matchGroups, null, atGroup => { //需要朝向一致才行 if (!info.k) return true //(box识别的宽高识别不准所以不需要) let onePair = atGroup.relationships[0] let name = onePair[0].sid + '&' + onePair[1].sid if (!rowInfos[name].k) return true //不过不应该有这种情况,否则匹配不到一起才对 if ((rowInfos[name].k < 1 && info.k < 1) || (rowInfos[name].k > 1 && info.k > 1)) { return true } else { console.log('k不一致无法匹配', info, atGroup) } }) //根据目前的规则应该是有端点的和有端点的匹配,box和box匹配 } }) //console.log(resultPairs[0]) } } } match() ignoreCountMatch || match('second') //再次将剩余的匹配一下,这次允许个数不同的row匹配 console.log('matchGroups', matchGroups) //识别出来的多组,可能有重复的,因为box个数不同所以才没到一组 //整理一下,每个组整理出一个info,同时重新检查一下,挑去每组中和其他成员非常不同的 let groupInfo = [] let getGroupInfo = group => { let left = new THREE.Vector3(), right = new THREE.Vector3(), pointsLen = 0 let bigBoxes = [] let info = {} group.relationships.forEach(pair => { let name = pair[0].sid + '&' + pair[1].sid let matchInfo = rowInfos[name] //this.matchScoreMap["rowBigBox"][name] || this.matchScoreMap["cabinet"][name]; if (matchInfo.posLeft) { left.add(matchInfo.posLeft), right.add(matchInfo.posRight), pointsLen++ } else { bigBoxes.push(matchInfo) preDealBox(matchInfo) getBoxSize(matchInfo) } }) let index = groupInfo.length if (pointsLen > 0) { left.multiplyScalar(1 / pointsLen) right.multiplyScalar(1 / pointsLen) //addLabel(left, 'Left' + index, { bgcolor: '#F00', a: 0.2 }) //addLabel(right, 'Right' + index, { bgcolor: '#F00', a: 0.2 }) let center = new THREE.Vector3().addVectors(left, right).multiplyScalar(0.5) //addLabel(center, 'center' + index, { bgcolor: '#F00', a: 0.3 }) ;(info.left = left), (info.right = right), (info.center = center) info.pointsLen = pointsLen } if (bigBoxes.length > 0) { let getAve = bigBoxes => { let center1 = new THREE.Vector3(), size = new THREE.Vector3() bigBoxes.forEach(box => { let center0 = getBoxPos(box) center1.add(center0) size.add(box.size) }) if (pointsLen > 0) { let size0 = new THREE.Vector3(Math.abs(left.x - right.x), size.y, Math.abs(left.z - right.z)) size.add(size0.multiplyScalar(pointsLen)).multiplyScalar(1 / (pointsLen + bigBoxes.length)) center1.add(info.center.clone().multiplyScalar(pointsLen)).multiplyScalar(1 / (pointsLen + bigBoxes.length)) } else { size.multiplyScalar(1 / bigBoxes.length) center1.multiplyScalar(1 / bigBoxes.length) } return { center1, size } } let { center1, size } = getAve(bigBoxes) //console.log(center1, size) let getScores = (center, size) => { //获得相对于center,size的差别分数 bigBoxes.forEach(box => { box.sc = -box.center.distanceToSquared(center1) - size.distanceToSquared(box.size) * 0.5 }) } getScores(center1, size) bigBoxes.sort((a, b) => b.sc - a.sc) let midItem = bigBoxes[Math.floor(bigBoxes.length / 2)] //中位数 getScores(midItem.center, midItem.size) const minScore = -8 let removes = bigBoxes.filter(e => { return e.sc < minScore }) if (removes.length) { let { newGroups } = common.disconnectGroup( removes.map(e => [e.box0, e.box1]), matchGroups ) console.log('去除错误数据', removes) if (newGroups.length > 1) { //分裂成多组了,重新计算 newGroups.forEach(e => { getGroupInfo(e) }) return } bigBoxes = bigBoxes.filter(e => { return e.sc >= minScore }) } if (bigBoxes.length) { let o = getAve(bigBoxes) //again ;(info.center = o.center1), (info.size = o.size) } } info.k = getK(info) info.bigBoxes = bigBoxes info.group = group groupInfo.push(info) } matchGroups.slice(0).forEach(group => { getGroupInfo(group) }) let getLength = c => { //获取bigbox长度 return c.size ? (c.k > 1 ? c.size.x : c.size.z) : (c.k > 1 ? c.right.x - c.left.x : c.right.z - c.left.z + 0.6) } let getLeft = (group, k) => { let dirAxis = (k || group.k) > 1 ? 'x' : 'z' return group.left ? group.left[dirAxis] - 0.3 : group.center[dirAxis] - group.size[dirAxis] / 2 //left和right加减半个宽度 } let getRight = (group, k) => { let dirAxis = (k || group.k) > 1 ? 'x' : 'z' return group.right ? group.right[dirAxis] + 0.3 : group.center[dirAxis] + group.size[dirAxis] / 2 } //识别是否group之间有一样的, 去重 { let realGroups = [] let getAveWidth = (infos, len) => { let boxCounts = [] infos.forEach(e => { boxCounts.push(...e.group.map(bigBox => bigBox.boxes.length)) }) /* infos.forEach(e => { boxCounts.push(...e.group.relationships.map(pair => Math.max(pair[0].predictBoxCount || pair[0].boxes.length, pair[1].predictBoxCount || pair[1].boxes.length))) }) //根据匹配到的来得个数 */ /* infos.forEach(e => { e.group.relationships.forEach(pair => { let counts = pair.map(e => e.boxes.length) if (counts.filter(e => e == 1).length == 1) { //如果是1和其他数字匹配,取数字大的。如1和3的话是3个箱子。 let c = counts.find(e => e != 1) boxCounts.push(c) } else { boxCounts.push(...counts) //否则两个数字都取 } }) }) //根据匹配到的来得个数 */ boxCounts.sort((a, b) => a - b) /*let midCounts = [] //这一排的箱子个数中位数 midCounts.push(boxCounts[Math.floor(boxCounts.length / 2)]) if (boxCounts.length % 2 == 0) { let a = boxCounts[Math.floor(boxCounts.length / 2) - 1] let b = midCounts[0] let c = b - 1 while (c >= a) { midCounts.push(c) c-- } //加入a到b中所有的整数 } */ let midCounts = [] let r0=0.3, r1=0.7 //取中间这部分的算最适合的个数,结果不一定是中位数 boxCounts.slice(Math.floor(boxCounts.length*r0), Math.floor(boxCounts.length*r1)+1).forEach((c)=>{ if(!midCounts.includes(c)) midCounts.push(c) }) let { min, max } = standards.cabinet.widthNormal let standardW = (min+max)/2 let aveWs = midCounts.map(e => { return { aveW: len / e, count: e } }) aveWs.sort((a, b) => Math.abs(a.aveW - standardW) - Math.abs(b.aveW - standardW)) let aveW = aveWs[0].aveW let count = aveWs[0].count if (aveW > max || aveW < min) { let w = THREE.MathUtils.clamp(aveW, min, max) //console.warn(`box aveW宽度不太对,从${aveW}修改到${w}`) aveW = w } return { aveW, count } } let getBox2 = (center, len, thick, k) => { let box2 = new THREE.Box2() box2.expandByPoint(new THREE.Vector2(center.x, center.z)) let sizeVec = k > 1 ? new THREE.Vector2(len / 2, thick / 2) : new THREE.Vector2(thick / 2, len / 2) box2.expandByVector(sizeVec) return box2 } const standardW = 0.6 //两排之间最小距离 /* let getBoxLen = (group)=>{ if( group.bigBoxes.length == 0) return 0 let max = 0 group.bigBoxes.forEach(e=>{ max = Math.max(max, e.box0.boxes ? e.box0.boxes.length : 1 , e.box1.boxes ? e.box1.boxes.length : 1 ) }) return max } */ for (let m = 0; m < groupInfo.length - 1; m++) { let group0 = groupInfo[m] for (let n = m + 1; n < groupInfo.length; n++) { let group1 = groupInfo[n] /* if(group0.k == 5.135329840006164 && group1.k ==4.494597962707433 || group0.k ==4.494597962707433 && group1.k == 5.135329840006164){ console.log(4) } */ if (group0.k == 4.242560016595383 || group1.k == 0.8571428571428572) { console.log(2) } const maxR = 2.3 if ( ((group0.k > 1 && group1.k < 1) || (group0.k < 1 && group1.k > 1)) && //getBoxLen(group0) != 1 && getBoxLen(group1) != 1 )continue //且没有其中一个是单个的box //(group0.k > maxR || group0.k < 1/maxR ) && (group1.k > maxR || group1.k < 1/maxR )) continue //如果是方块状的无视k getLength(group0) > 1.5 && getLength(group1) > 1.5 ) continue //如果是方块状的无视k //间距 let spaceAxis = (group0.k + group1.k) / 2 > 1 ? 'z' : 'x' let spaceDis = Math.abs(group0.center[spaceAxis] - group1.center[spaceAxis]) if (spaceDis > standardW * 1.5) continue let o0 = getAveWidth([group0], getLength(group0)) //因为有可能长度和箱子个数不匹配,所以需要得到限制后的宽度再比较 let o1 = getAveWidth([group1], getLength(group1)) let len0 = (group0.predictLen = o0.aveW * o0.count) let len1 = (group1.predictLen = o1.aveW * o1.count) const minR = 0.5 //不可限制太死,因为有的框个数识别少了,导致len短。但可通过重叠面积来判断 // if( len0 / len1 < minR || len0 / len1 > 1/minR) continue let area0 = (group0.area = len0 * o0.aveW) let area1 = (group1.area = len1 * o1.aveW) let getBoxMixArea = (expandRatio1, expandRatio2) => { let box0 = getBox2(group0.center, len0 + expandRatio1, o0.aveW + expandRatio2, group0.k) let box1 = getBox2(group1.center, len1 + expandRatio1, o1.aveW + expandRatio2, group1.k) let mixBox = getMixBox(box0, box1) //重叠部分 let s = mixBox.getSize(new THREE.Vector2()) return { box0, box1, areaMix: Math.max(0, s.x) * Math.max(0, s.y) } //可能是0 } let areaMixExpand = getBoxMixArea(0.1, 0.4).areaMix if (areaMixExpand / area0 < 0.7 && areaMixExpand / area1 < 0.7) continue //包含的可以通过 let areaMix = getBoxMixArea(0, 0).areaMix //实际重合面积 group0.contains = group0.contains || [] group1.contains = group1.contains || [] group0.contains.push({ group: group1, selfPercent: areaMix / area0, percent2: areaMix / area1, areaMix }) group1.contains.push({ group: group0, selfPercent: areaMix / area1, percent2: areaMix / area0, areaMix }) console.log('两个合并', group0, group1) common.pushToGroupAuto([group0, group1], realGroups) //包含的直接合并吧 - - ,这样会使结果偏移,不过没办法了,多个重叠面积太难算了 } } //但没合并前样本数量少,包含关系可能有错 - - /*for(let m=0; m e.percent2 > 0.8) //所有包含的 contains.reduce } */ groupInfo.forEach(info => { //加入单个的 if (!realGroups.some(groups => groups.includes(info))) { realGroups.push([info]) } }) console.log('realGroups', realGroups) //get boxes realGroups.forEach((infos, i) => { const sampleCount = infos.reduce((w, c) => { return (w += c.pointsLen || c.bigBoxes.length) }, 0) let k { //const k = infos.reduce((w, c) => (w += c.k), 0) / infos.length let ks = infos.map(e=>e.k) ks.sort((a,b)=>a-b) let min = ks[0], max = ks[ks.length-1] if(min<1 && max>1){//比较最小和最大,选取更极端的那个 let min_ = 1/min if(min_ < max) k = max else k = min }else k = (min + max) / 2 } let centerPos = infos .reduce((w, c) => { return w.add(c.center.clone().multiplyScalar(c.pointsLen || c.bigBoxes.length)) }, new THREE.Vector3()) .multiplyScalar(1 / sampleCount) //预得中心点 //获取左右端点(需要排除可能的误差,所以采用最靠近端点的三个点。但无法排除前三个点中万一含有包含box的、或者误差大的端点) let lefts = infos .map(e => getLeft(e, k)) .sort((a, b) => a - b) .filter(a => a < centerPos[k > 1 ? 'x' : 'z']) .slice(0, 3) let rights = infos .map(e => getRight(e, k)) .sort((a, b) => b - a) .filter(a => a > centerPos[k > 1 ? 'x' : 'z']) .slice(0, 3) let left = 0, right = 0 let c0 = ((lefts.length + 1) * lefts.length) / 2 lefts.forEach((e, i) => { //越靠近最外侧权重越高。 left += e * ((lefts.length - i) / c0) }) c0 = ((rights.length + 1) * rights.length) / 2 rights.forEach((e, i) => { right += e * ((rights.length - i) / c0) }) centerPos[k > 1 ? 'x' : 'z'] = (left + right) / 2 let len = right - left //加一点值是因为之前计算长度,用的是最外box的中心点,会少box一半宽度 let infos2 = infos.filter(e => { return !e.predictLen || e.predictLen / len > 0.7 }) if(infos2.length == 0){ infos2 = infos.sort((a,b)=>b.predictLen - a.predictLen).slice(0,1) } let { aveW, count } = getAveWidth(infos2, len) //长宽比中心点的误差更大,尤其是box类型的、或样本少的 //获取高度 let heights = [] { let pairs = [] , heightss infos.forEach(e => { pairs.push(...e.group.relationships.filter(pair => pair[0].boxes.length == count && pair[1].boxes.length == count)) }) if(pairs.length){ heightss = pairs.map(pair => { let boxes = pair.map(e => { /* if (e.boxes.length == count) */ return e.boxes.slice() /* return e.bestChain.slice() */ }) let match = rowInfos[pair[0].sid + '&' + pair[1].sid] let ifReverse = match.reversed if (match.reversed == void 0 && pair[0].boxes.length > 1 && pair[1].boxes.length > 1) { let { reversed } = getReverseInfo(pair[0], pair[1]) ifReverse = reversed } if (ifReverse) { boxes[1].reverse() } let heights1 = [] let topPoss = [] for (let i = 0; i < count; i++) { let match1 = getMatchScore(boxes[0][i], boxes[1][i], { onlyGet: true }) let topPos = match1 && match1.topPos if (!topPos) { topPos = getBoxTop({ box0: boxes[0][i], box1: boxes[1][i] }) } heights1.push(topPos.y - groundY) if(topPos.y - groundY<0){ console.log('?') } topPoss.push(topPos) } if ((k < 1 && topPoss[0].z > topPoss[count - 1].z) || (k > 1 && topPoss[0].x > topPoss[count - 1].x)) { heights1.reverse() } return heights1 }) }else{ let bigBoxes = [] infos.forEach(e => { bigBoxes.push(...e.group.filter(e=>e.boxes.length == count)) }) heightss = bigBoxes.map(bigBox=>{ let topPoss = bigBox.boxes.map(box=>{ let a = {box0:box} getBoxTop(a) return a.topPos }) if ((k < 1 && topPoss[0].z > topPoss[count - 1].z) || (k > 1 && topPoss[0].x > topPoss[count - 1].x)) { topPoss.reverse() } let heights1 = topPoss.map(topPos=>topPos.y - groundY) return heights1 }) } heightss.forEach(arr => { for (let i = 0; i < count; i++) { heights[i] = (heights[i] || 0) + arr[i] } }) heights = heights.map(e => { let h = e / heightss.length return h }) //console.log('heightss',heightss, pairs, heights) } //拆分成小box let size = new THREE.Vector3(aveW, 2, aveW) let c = 0 infos.box = [] while (c < count) { let center if (k > 1) { let startX = centerPos.x - ((count - 1) / 2) * aveW center = new THREE.Vector3(startX + c * aveW, centerPos.y, centerPos.z) } else { let startZ = centerPos.z - ((count - 1) / 2) * aveW center = new THREE.Vector3(centerPos.x, centerPos.y, startZ + c * aveW) } let size1 = heights[c] ? size.clone().setY(heights[c]) : size //如1*3的是得不到height的 let box = new Box({ name: 'row' + i + '-' + c, center, size: size1, boxType: 'cabinet', infos }) c++ infos.box.push(box) } }) return realGroups.length > 0 } } let removeContain = arr => { //去除嵌套 let len = arr.length if (len < 2) return for (let i = 0; i < len - 1; i++) { let box0 = arr[i] getBoxBase(box0) box0.contains = box0.contains || [] for (let j = i + 1; j < len; j++) { let box1 = arr[j] getBoxBase(box1) box1.contains = box1.contains || [] let d3 = Math.abs(box1.bbox2CenterX - box0.bbox2CenterX) //限制d3是因为在相差180度两端可能也符合 //d4 = Math.abs(box1.bbox2[3] - box0.bbox2[3]) if (d3 > 0.4 /* || d4 > 0.01 */) continue let d0 = getBbox2Diff(box1.bbox2[0], box0.bbox2[0]), d1 = getBbox2Diff(box0.bbox2[2], box1.bbox2[2]) let min = 0.005 if ((d1 >= 0 && Math.abs(d0) < min) || (d0 >= 0 && Math.abs(d1) < min) || (d1 >= 0 && d0 >= 0)) { box0.contains.push(box1) } else if ((d0 <= 0 && Math.abs(d1) < min) || (d1 <= 0 && Math.abs(d0) < min) || (d1 <= 0 && d0 <= 0)) { box1.contains.push(box0) } } } let getWidthScore = (box, type) => { const addDis = 0.1 //因为用的是btm的pos,比中心点近了一些,所以加上一段距离 let o = getBoxPoseByPos(box, getBoxPos(box), addDis) let boxPjW = o.projectWidth let standardPjW = (o.maxProjectWidth + o.minProjectWidth) / 2 let s = type == 'out' ? boxPjW - standardPjW : standardPjW - boxPjW return -Math.pow(s, 2) * 10 } arr.slice().forEach(box => { if (box.contains.length > 1) { //假设不存在第二层嵌套, 假设每个只能被一个嵌套 //决定留大还是留小 //先只去掉包含两个以上的,且角度范围一致 //尽量保留内层,除非内层太小 let { leftX, rightX } = getLeftRight(box.contains) if (Math.abs(getBbox2Diff(box.bbox2[0], leftX)) > 0.005 || Math.abs(getBbox2Diff(box.bbox2[2], rightX)) > 0.005) return //范围不一致 let remainChild = true let childrenScores = box.contains.map(e => getWidthScore(e, 'in')) let childAve = childrenScores.reduce((w, c) => w + c, 0) / childrenScores.length if (childAve < -4) { let outScore = getWidthScore(box, 'out') remainChild = childAve > outScore } if (!remainChild) { box.contains.forEach(e => { ;(e.state = '因被嵌套被删除'), (e.containBy = box) arr.splice(arr.indexOf(e), 1) console.log('因被嵌套被删除', ...box.contains) }) } else { box.state = '因嵌套其他被删除' console.log('因嵌套其他被删除', box) arr.splice(arr.indexOf(box), 1) } } }) } let waitFindRest = [] let Search = type => { console.error('开始search', type) let matchScoreMap = (this.matchScoreMap[type] = {}) let datas = {} let panoIds = [] for (let id in this.datas) { datas[id] = this.datas[id].shapes.filter(e => e.category == type) datas[id].length && panoIds.push(id) } for (let id in this.datas) { //对data预处理 //(之后如果还出现不同类型重叠在一起的,需要先识别摘除下。 )4GqaqNdyjGf removeContain(datas[id]) //去除线框中的嵌套,主要是一个嵌套两个的。案例:KK-1Zjm9Rbl47 if (datas[id].length) {//融合。很多box被一分为二了,基本都是在全景图左右边界处。 let bigBoxes = getPanoBigRowBox(datas[id], { reason: 'mix' }) bigBoxes.forEach(bigBox => { if (bigBox.boxes.length > 1) { bigBox.boxes.forEach(box => { ;(box.state = '被删除'), (box.mixTo = bigBox) let i = datas[id].indexOf(box) datas[id].splice(i, 1) if(version == 'vision'){ i = this.datasMixed[id].shapes.findIndex(e=>e.sid == box.sid) this.datasMixed[id].shapes.splice(i, 1) } }) console.log('因融合而删除', bigBox.boxes) datas[id].push(bigBox) if(version == 'vision'){ this.datasMixed[id].shapes.push(bigBox) } bigBox.index = datas[id].length>1 ? datas[id][datas[id].length-2].index+1 : 0 { let a = bigBox.sid.split('mix-') bigBox.sid = a[0]+bigBox.index+'(mix'+a[1]+')' //"pano20-mix-1,2" } bigBox.category = type //更正 } }) } } if (panoIds.length == 0) return panoIds.sort((a, b) => { return datas[b].length - datas[a].length }) let groups = panoIds.map(e => datas[e]) console.log('按box个数排序:', groups.slice()) let group0 = groups[0], len0 = group0.length if (groups.length == 1) { //只有一个全景里有数据 group0.forEach(e => createSinglePano(e)) return combines(type) } if (len0 == 1) {//最多的也只有一个box。此情况大部分是空调 panoIds.forEach(e => getBoxBase(datas[e][0])) let maxAngle //找出centerDir夹角最大的两个pano for (let i = 0; i < panoIds.length; i++) { let box0 = datas[panoIds[i]][0] for (let j = i + 1; j < panoIds.length; j++) { let box1 = datas[panoIds[j]][0] getMatchScore(box0, box1, { isSingle: true }) } } let list = Object.keys(matchScoreMap) list.sort((a, b) => { return matchScoreMap[b].score - matchScoreMap[a].score }) let match = matchScoreMap[list[0]] preDealBox(match) //根据分数重排序,前两个已匹配的pano放在第一第二(之后会被跳过),获得groups2 let panoIds2 = [] list.forEach(e => { let info = matchScoreMap[e] if (!panoIds2.includes(info.box0.pano.id)) panoIds2.push(info.box0.pano.id) if (!panoIds2.includes(info.box1.pano.id)) panoIds2.push(info.box1.pano.id) }) let groups2 = panoIds2.map(e => datas[e]) if (match.score > -100) { getBoxSize(match) if (match.score > 0 && match.sizeAdjust < 0.1) { new Box(matchScoreMap[list[0]]) waitFindRest.push({type, args:[groups2]} ) //等待最后检查遗漏 return } } //继续match reMatchLowScores([match], groups2) waitFindRest.push({type, args:[groups2]} ) //等待最后检查遗漏 return } { //重新根据距离排序,挑选离所有box距离最近的两个pano (远的可能看不到box,或者得到的线框计算的位置不准。不过其实太近也不准-,-) let counts = {} groups.forEach(e => { e.forEach(a => getBoxBase(a)) counts[e.length] || (counts[e.length] = []) counts[e.length].push(e) }) groups = [] let nums = Object.keys(counts) nums.reverse() nums.forEach(count => { let groups_ = counts[count] if (groups_.length > 1) { groups_.forEach(e => { e.disSc = e.reduce((w, c) => { let pos = getBoxPos(c) return w + c.pano.position.distanceToSquared(pos) }, 0) }) groups_.sort((a, b) => a.disSc - b.disSc) } groups.push(...groups_) }) console.log('按距离和个数排序:', groups) group0 = groups[0] } if (type == 'cabinet') { //转化为分组 if (searchByRow(groups, datas)){ waitFindRest.push({type, args:[groups,0]} ) //等待最后检查遗漏 return } } //零散匹配。 let group1 = groups[1], len1 = group1.length for (let i = 0; i < len0; i++) { //复杂度:n的平方次 for (let j = 0; j < len1; j++) { let box1 = group0[i] let box2 = group1[j] let result = getMatchScore(box1, box2) } } //寻找最佳配对 n!种组合(是否要限制个数多的情况?) 超过8个就很恐怖 //仅先查找选中的两个pano配对 let resultPairs = [] let newGroup0 = group0.slice(0) let newGroup1 = group1.slice(0) while (newGroup1.length < newGroup0.length) { newGroup1.push({ sid: 'void' }) //为了使排列正确,补个空,使左右两边个数相等,过后和void匹配的不会计算box } searchPair(group0[0], newGroup0, newGroup1, null, resultPairs) console.log( 'resultPairs', resultPairs.map(pairs => pairs.map(pair => pair.map(item => item.sid).join(' & '))) ) resultPairs = resultPairs.map(pairs => { let infos = pairs.map(pair => matchScoreMap[pair[0].sid + '&' + pair[1].sid]) let score = infos.reduce((s, e) => { return s + (e ? e.score : 0) }, 0) //if(infos[0].score < -1000){ //score -= infos[0].score //} let o = { infos, score, pairs, name: pairs.map(pair => pair.map(item => item.sid).join(' & ')), } return o }) console.log('resultPairs', resultPairs.slice()) console.log( 'resultPairs按分数高低', resultPairs.sort((a, b) => b.score - a.score) ) //console.log('compu',compu) let noMatches = [] //和void匹配的,需要和其他pano的重新匹配 let mayHaventMatched = []; let lowScores = [] if (resultPairs[0]) { resultPairs[0].infos.forEach((info, i) => { if (!info) { noMatches.push(resultPairs[0].pairs[i].find(e => e.sid != 'void')) return //match with void } if (info.score < -2000) { lowScores.push(info) return } preDealBox(info) getBoxSize(info) if (info.sizeAdjust > 0.1) { lowScores.push(info) return } let box = new Box(info) }) } if (noMatches.length) { reMatchLowScores( noMatches.map(e => { return { box0: e } }), groups ) } reMatchLowScores(lowScores, groups) waitFindRest.push({type, args:[groups]} ) //combines(type) } function findRest(groups/* ,startIndex=2 */){ //查找是否有遗漏。 //1 可能有距离较远的box不在头两个pano的附近导致被漏掉。(概率很小) //2 被剩余的 for(let i=0;i{ if(!used(box)){ if(box.sid == 'pano4-1'){ console.log(3) } //如果和现有的box的距离都很远,很可能是漏掉的 let near = boxes.find(solidBox =>{ if(solidBox.name == "air-pano16-mix-5,4&pano18-0"){ console.log(3) } if(solidBox.boxType != box.type && (solidBox.boxType == 'air-hanging' || box.type == 'air-hanging')) return; //挂空调一般不会撞到地面上的 let p1 = getBoxPos(solidBox) let p2 = getBoxPos(box) let p1_ = new THREE.Vector2(p1.x, p1.z); let p2_ = new THREE.Vector2(p2.x, p2.z); let maxWidth = standards[solidBox.boxType].widthNormal.max if(solidBox.boxType != box.type){ maxWidth = Math.min(standards[box.type].widthNormal.max, maxWidth) } let dis = solidBox.bound.distanceToPoint(p2) let r0 = solidBox.boxType == 'air' ? 1.5 : solidBox.boxType == 'battery' ? 1.1 : 1 //空调最不容易扎堆放置,所以范围设置广一些 let r1 = math.linearClamp( box.pano.position.distanceTo(p2), 3, 6, 1, 1.5); //距离远的话识别、计算都会更不准确,给一定的容错 let ra = (solidBox.boxType == box.type ? 1 : 0.5 ) * r0 * r1 //数字越小限制越大 //let rb = (solidBox.boxType == box.type ? 0.5 : 0.2 ) * r0 * r1 let a = maxWidth * maxWidth * ra - p1_.distanceToSquared(p2_) let b = - dis * 0.5 let c = a + b if(c>0){ console.log(1) } return c>0 }) if(!near){ reMatchLowScores([{box0:box, log:"findRest"}], groups,0 ) }else{ //console.log('find near', near.name, box.sid) } } }) } } function used(box){ let has = (e)=>{ return e.box0 == box || e.box1 == box } return /* matched.includes(box) || */ boxes.some(e => /* e.boxType == box.type && */ has(e) || (e.list && e.list.some(a => has(a))) || e.infos && e.infos.some(u=>u.group.some(r=> r.boxes.some(b=>b==box) )) //row ) } function reMatchLowScores(lowScores, groups, startIndex=2 ) { let matched = [], tooLows = [] let isSameMatch = (match0, match1) => { //是否对应同一个box实体(不一定准),通过两两box之间是否都match来判断 //如果是相同pano但不同的box肯定不是对应同一个box实体 let ifWrong = (box0, box1) => { if (box0 == box1) return if (box0.pano == box1.pano) return true let match2 = getMatchScore(box0, box1, { isSingle: true, restMatch: true }) if (match2.score < -2000) return true } if (ifWrong(match0.box0, match1.box0) || ifWrong(match0.box1, match1.box1) || ifWrong(match0.box0, match1.box1) || ifWrong(match0.box1, match1.box0)) return return true } if (lowScores.length) { console.warn(lowScores[0].log || (lowScores[0].box1 ? '低分重新匹配' : '剩余匹配'), lowScores) if (lowScores[0].box1) { lowScores.sort((a, b) => { //低分优先 return a.score - b.score }) } lowScores.forEach(info => {//info中的box0和box1分别向后寻找其他的配对。选择分数高的配对。 但box0和box1可能是错误配对,会导致找到了替代的也可能遗漏。 /* if (info.name == 'pano0-3&pano2-3') { console.log(1) } */ let box01 = info.box0 let box02 = info.box1 let bigGroup = [] box02 && bigGroup.push(info) let got for (let cur = startIndex; cur < groups.length; cur++) { let thirdGroup = groups[cur] let scores0 = [], scores1 = [] thirdGroup.forEach(box1 => { //if (matched.includes(box1.sid)) return if (used(box1)) return //会不会太严格? if(box1.pano != box01.pano){ let r1 = getMatchScore(box01, box1, { isSingle: true, restMatch: true }) r1.score > -4000 && scores0.push(r1) } if (box02 && box1.pano != box02.pano) { let r2 = getMatchScore(box02, box1, { isSingle: true, restMatch: true }) r2.score > -4000 && scores1.push(r2) } }) scores0.sort((a, b) => { return b.score - a.score }) scores1.sort((a, b) => { return b.score - a.score }) scores0[0] && bigGroup.push(scores0[0]) scores1[0] && bigGroup.push(scores1[0]) } bigGroup.sort((a, b) => { return b.score - a.score }) let goodList = bigGroup.slice(0, 10).map(e => { if (!getBoxPos(e)) return e preDealBox(e) getBoxSize(e) return e }) let goodList2 = goodList .sort((a, b) => { return b.score - a.score }) .slice(0, 3) if (goodList2.length == 0) { return createSinglePano(box01) } if (goodList2[0].score > -1000) { goodList2 = goodList2.filter(e => e.score > -1000) } else { goodList2 = [goodList2[0]] // 最高分已经过小 if (goodList2[0].score < -1200) { if (!box02) return createSinglePano(box01) console.warn('分数过低,是否有匹配错误?', goodList2[0]) return tooLows.push(goodList2[0]) } } if (goodList2.length) { //需要确认两两之间是配对的,也就是都对应同一个box let subGroups = [], boxes = [] for (let i = 0, len = goodList2.length; i < len; i++) { //向后选择队友 let match0 = goodList2[i] if (subGroups.some(e => e.includes(match0))) continue //被挑选了的没有选择权 let gr = [match0] for (let j = i + 1; j < len; j++) { let match1 = goodList2[j] if (isSameMatch(match0, match1)) {//可能不是同一个,所以需要检验 gr.push(match1) } } //if(gr.length>1){ subGroups.push(gr) //} } console.log('lowScores subGroups',subGroups) subGroups.forEach(pair => { boxes.push(mixMatchBox(pair, lowScores[0].log)) }) combineBoxes(boxes) //很可能其实还是同一个,需要检验是否要融合 } }) //改为之后 findRest, 因为两者都single的可能性低 /* let judge = box => { if (!used(box)) { matched.push(box) createSinglePano(box) } } tooLows.forEach(e => { judge(e.box0) judge(e.box1) }) */ } } function mixMatchBox(list, log) { let center = new THREE.Vector3(), size = new THREE.Vector3(), name list.forEach(e => { center.add(getBoxPos(e)) size.add(e.size) //matched.push(e.box1) }) center.multiplyScalar(1 / list.length) size.multiplyScalar(1 / list.length) let prefix = log == 'findRest' ? 'rest:' : 'low:' let object = { name: prefix + list.map(e => e.name), boxType: list[0].boxType, center, size, list, xProp: list.find(e=>e.xProp) && list.find(e=>e.xProp).xProp, yProp: list.find(e=>e.yProp) && list.find(e=>e.xProp).yProp, } let o = restrictSize(size.x, size.z, object) size.x = o.x size.z = o.y let box = new Box(object) console.log('mixMatchBox',box) return box } function combineBoxes(boxes){ const group = [] if (boxes.length > 1) { const boxType = boxes[0].boxType let { min, max } = standards[boxType].widthNormal for (let i = 0, len = boxes.length; i < len - 1; i++) { let box0 = boxes[i] for (let j = i + 1; j < len; j++) { let box1 = boxes[j] let bound = box0.bound.clone().union(box1.bound) let size = bound.getSize(new THREE.Vector3()) let intersect = box0.bound.intersectsBox(box1.bound) let minX = min, minZ = min, maxX = max, maxZ = max let xProp = box1.xProp || box0.xProp let yProp = box1.yProp || box0.yProp if (xProp) { if (box0.xProp && box1.xProp && box0.xProp != box1.xProp) continue maxX = standards[boxType][xProp].max maxZ = standards[boxType][yProp].max } maxX = Math.max(maxX, box0.size.x, box1.size.x) //必须大于各自的size,否则无法去除本身就oversize的box中包含的 maxZ = Math.max(maxZ, box0.size.z, box1.size.z) let r = intersect ? 1.4 : 1.1 //如果是没有交集,限制更大些 let maxDiff = 0.3 if ((size.x < maxX * r || size.x-maxX { let bound = new THREE.Box3() pair.forEach(e => { bound.union(e.bound) e.dispose() }) let size = bound.getSize(new THREE.Vector3()) let center = bound.getCenter(new THREE.Vector3()) let info = { name: 'mix:' + pair.map(e => e.name + ','), mixedFrom: pair, boxType, center, size, xProp: pair.find(e=>e.xProp) && pair.find(e=>e.xProp).xProp, yProp: pair.find(e=>e.yProp) && pair.find(e=>e.xProp).yProp, } let o = restrictSize(size.x, size.z, info) size.x = o.x size.z = o.y let box = new Box(info) console.error('混合', boxType, pair, box) }) } } } function combines(type) { //合并boxSolids . battery经常嵌套 if (type == 'battery') { /* let airs = boxes.filter(e => e.boxType == 'air') let hangings = boxes.filter(e => e.boxType == 'air-hanging') */ let batterys = boxes.filter(e => e.boxType == 'battery') ;[batterys].forEach(boxes => { combineBoxes(boxes) }) } } let createSinglePano = box => { //仅用一个pano中的data来创建 getBoxBase(box) let center = getBoxPos(box) let info = { name: box.sid, box0: box, center, topPos: box.topPos, btmPos: box.btmPos, } preDealBox(info) let a = getPoseScore(info.boxposes,true) let failed = a < -8 console.log('createSinglePano', failed?'失败':'成功' , 'pose score:',a, box ) if(failed)return addLabel(center, 'center', { a: 0.3 }) info.topPos && addLabel(info.topPos, 'topPos', { a: 0.3 }) getBoxSize(info) new Box(info) } this.expandModelBound() if(version == 'vision')this.datasMixed = common.CloneObject(this.datas,null, [player.model.panos.list[0].constructor]) Search('cabinet') Search('air') Search('battery') console.log('----FindRest----') waitFindRest.forEach((e)=>{ findRest(...e.args) combines(e.type) }) } /* let getSid = (function(){ let sid = 0 return function(){ return sid++ } })() */ let done = ()=>{ for(let panoId in this.datas){ this.datas[panoId].shapes = this.datas[panoId].shapes.map((shape, i) => { return Object.assign( { sid: 'pano' + panoId + '-' + i, category: shape.category, //提前 便于调试 pano: player.model.panos.index[panoId], index: i, }, shape ) }) } player.model.chunks.forEach(e=>{ modelBound.union(e.geometry.boundingBox) //注:不用model.boundingBox是 因为union了pano的position的 }) //针对部分模型错误,只有底面的,union一下pano.position player.model.panos.list.forEach(e=>{ modelBound.expandByPoint(e.position) }) groundY = modelBound.min.y//部分模型底部高度错误,不管了 groundPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, groundY, 0)) /* const w = 0 //这一版的模型bound常常太小,把箱子当墙面,所以扩展一个箱子宽度 safeBound = player.model.boundingBox.clone().expandByVector(new THREE.Vector3(w, w, w)) */ this.ifAnalyze && beginCompute() if(version == 'vision')this.load(player.currentPano.id) } async function load(panoId) { let data = await http.post('/service/scene/sceneMarkShape/getInfo', { num: player.$app.config.num, imagePath: panoId + '.jpg' }) // console.error(data) if (!data.data || !data.success) return (this.datas[panoId] = null) this.datas[panoId] = data.data if (Object.keys(this.datas).length == panosCount && !player.model.panos.list.some(e => e.isAligned() && !(panoId in this.datas))) { done() } } let panosCount = 0 if(!dataList){ player.model.panos.list.forEach(e => { if (!e.isAligned()) return panosCount++ load.bind(this)(e.id) }) }else{ //when version == 'output' dataList.forEach(e=>{ let panoId = e.imagePath.split('.jpg')[0] this.datas[panoId] = e }) done() } } expandModelBound(){ const maxDis0 = 1, maxDis1 = 3 //不可扩展太宽,否则不准确的框会飘很远,甚至多画多个box,如R7xZsmm9FsG let newBound = modelBound.clone() let list = [] for(let panoId in this.datas ){ this.datas[panoId].shapes.forEach(box=>{ getBoxBase(box) if(box.btmPosPredict){ let far = box.pano.position.distanceToSquared(box.btmPosPredict) if(far > 12)return //太远不准 let dis = modelBound.distanceToPoint(box.btmPosPredict) if(dis>0 && disa.dis-b.dis) //let mid = list[Math.floor(list.length/2)] let mid = list.length// /2 for(let i=0;i maxDis0){ let p1 = pos.clone().clamp(modelBound.min, modelBound.max) let vec = new THREE.Vector3().subVectors(pos,p1).normalize().multiplyScalar(maxDis0) pos = new THREE.Vector3().addVectors(p1, vec) } marginBound.expandByPoint(pos); //console.log(box.btmPosPredict, box) marginBound.expandByVector(new THREE.Vector3(0.2,0,0.2)); newBound.union(marginBound); } console.log(newBound) skyBoxTight = new BoundingMesh(newBound, undefined, 0) skyBoxTight.material.side = 2 skyBoxTight.material.wireframe = true skyBoxTight.material.opacity = 0.05 skyBoxTight.material.transparent = true //skyBoxTight.visible = false skyBoxTight.updateMatrixWorld()//不update的话raycaster是错的 meshGroup.add(skyBoxTight) this.skyBoxTight = skyBoxTight this.safeBound = safeBound = newBound boundConfirmed = true } bindEvents() { if(version != 'vision')return player.on(PlayerEvents.FlyingStarted, e => { //if(e.mode == 'dollhouse')return // 点位跳转前清除已有线框 this.traverse(obj => { if (obj.isMesh) { obj.geometry.dispose() obj.material.dispose() } }) this.wireframes.clear() this.currentId = null }) player.on(PlayerEvents.FlyingEnded, () => { // 点位跳转后加载线框 if (player.mode != Viewmode.PANORAMA) return // 只有PANORAMA模式下需要加载 this.load(player.currentPano.id) }) window.panoWireframe = this if(this.ifAnalyze ){ setTimeout(() => { { let btn = document.createElement('button') btn.innerHTML = '点击切换box显示' btn.onclick = () => { this.boxes.forEach(e => ((e.boxHelper.visible = !e.boxHelper.visible), (e.label.visible = !e.label.visible))) } document.querySelector('#app').appendChild(btn) btn.id = 'boxWire' btn.style.position = 'fixed' btn.style['z-index'] = '100' btn.style.background = '#e00472' btn.style.padding = '10px' btn.style.bottom = '80px' } { let btn = document.createElement('button') btn.innerHTML = '点击切换矩形框显示' btn.onclick = () => { this.wireframes.visible = !this.wireframes.visible } document.querySelector('#app').appendChild(btn) btn.id = 'wireframes' btn.style.position = 'fixed' btn.style['z-index'] = '100' btn.style.background = '#419aff' btn.style.padding = '10px' btn.style.bottom = '130px' } }, 1000) } } /** * 加载点位标记数据 * @param {*} panoId */ /* async */ load(panoId) { // fetch(texture.getImageURL('images/points/' + panoId + '.json')) // .then(data => data.json()) // .then(data => { //let data = await http.post('/service/scene/sceneMarkShape/getInfo', { num: player.$app.config.num, imagePath: panoId + '.jpg' }) // console.error(data) let data = this.datasMixed[panoId] if (!data) { if (!(panoId in this.datas)) setTimeout(() => { this.load(panoId) }, 100) //否则无数据 return } //if (!data.data || !data.success) return if (player.currentPano.id != panoId || player.flying || this.currentId == panoId) return // 防止连续跳转点位时,clear后才load好上一点位的数据,导致出现之前的标记 this.currentId = panoId let { shapes, imageHeight, imageWidth } = data //data.data shapes.forEach((shape ) => { // 填充色和线框色 // let { fill_color, line_color } = shape getUVs(shape) getCenterDir(shape, player.currentPano) let { fill_color, color = [56, 56, 255] } = shape let line_color = [...color, 255] if (!fill_color) fill_color = [255, 255, 255, 0] if (!line_color) line_color = [255, 0, 0, 255] this.showSignalFrom2d( shape.category + '-' + shape.sid, shape.bbox2, imageWidth, imageHeight, { fill: { color: new THREE.Color().setRGB(fill_color[0] / 255, fill_color[1] / 255, fill_color[2] / 255), opacity: fill_color[3] / 255, }, line: { color: new THREE.Color().setRGB(line_color[0] / 255, line_color[1] / 255, line_color[2] / 255), opacity: line_color[3] / 255, }, }, shape.centerDir ) }) // }) // .catch(err => console.log(`点位${panoId}无标记数据或数据出错:`, err)) } /** * 根据坐标标记全景图 * * 存在的问题:如果要准确复现全景图上的线框,上下边框会变为弧形。而按顶点连直线的话,180度以上会出bug。 * 解决方式:目前150度以下只画出4个顶点然后连直线,150度以上准确画出全景图线框。 */ showSignalFrom2d(name, rect, w, h, options, centerDir) { // 目前rect给的是矩形对角的两个点坐标,将它扩展成四个顶点 let cornerArr = [ new THREE.Vector2(rect[0], rect[1]), new THREE.Vector2(rect[2], rect[1]), new THREE.Vector2(rect[2], rect[3]), new THREE.Vector2(rect[0], rect[3]), ] // 2d坐标转3d坐标 /* let transform2dTo3d = point => { // 计算方向向量 let x = point[0], y = point[1] let yaw = (-x / w) * (Math.PI * 2) let pitch = Math.PI / 2 - (y / h) * Math.PI let dir = new THREE.Vector3() dir.copy(Vectors.RIGHT).applyAxisAngle(Vectors.BACK, pitch).applyAxisAngle(Vectors.UP, yaw).applyQuaternion(player.currentPano.quaternion) return dir } */ // // 计算矩形线框中点坐标(取x、y的平均值) // let center = [cornerArr.reduce((a, b) => [a[0] + b[0], 0])[0] / cornerArr.length, cornerArr.reduce((a, b) => [0, a[1] + b[1]])[1] / cornerArr.length] // let centerVec = transform2dTo3d(center) // 计算中点3d坐标 // 根据四个顶点,填充中间点 let pointArr = [] for (let i = 0; i < cornerArr.length; i++) { let corner1 = cornerArr[i] let corner2 = cornerArr[(i + 1) % cornerArr.length] pointArr.push(corner1) /* // 横向角度超过150度时,3d中边框的弧线已经不太明显,准确画出全景图线框 if ((rect[2] - rect[0]) / w < 5 / 12 && i % 2 == 0) continue const vec = [corner2[0] - corner1[0], corner2[1] - corner1[1]] let length = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]) let num = length / 150 for (let j = 1; j <= num; j++) { pointArr.push([corner1[0] + (vec[0] / num) * j, corner1[1] + (vec[1] / num) * j]) } */ } //pointArr.push(cornerArr[0], cornerArr[2], cornerArr[1], cornerArr[3]) //对角线 let points = [] pointArr.forEach(uv => { let dir = getDirByUV(uv, player.currentPano) // points.push(dir.sub(centerVec)) // 计算其他点相对于中点的坐标,方便旋转平移等 points.push(dir) }) // 线框 const lineGeometry = new THREE.BufferGeometry().setFromPoints(points) const lineMaterial = new THREE.LineBasicMaterial({ color: options.line.color, opacity: options.line.opacity, transparent: true, depthTest: false }) const wireframe = new THREE.LineLoop(lineGeometry, lineMaterial) // wireframe.position.copy(centerVec) // 将中点作为线框坐标 wireframe.renderOrder = 100 // 填充颜色 const fillGeometry = lineGeometry.clone().setIndex(new THREE.BufferAttribute(new Uint16Array([0, 1, 3, 2, 3, 1]), 1)) const fillMaterial = new THREE.MeshBasicMaterial({ color: options.fill.color, opacity: options.fill.opacity, transparent: true, side: THREE.DoubleSide, depthTest: false }) const plane = new THREE.Mesh(fillGeometry, fillMaterial) plane.renderOrder = wireframe.renderOrder - 1 wireframe.add(plane) // 名称 const textMesh = new TextSprite({ text: name, backgroundColor: { r: options.line.color.r * 255, g: options.line.color.g * 255, b: options.line.color.b * 255, a: 0.8 }, textColor: { r: 255, g: 255, b: 255, a: 1 }, borderRadius: 15, renderOrder: wireframe.renderOrder + 1, player: player, }) textMesh.position.copy(centerDir.clone().add(new THREE.Vector3(0, 0.1, 0))) textMesh.lookAt(0, 0, 0) // 看向相机 textMesh.scale.set(0.12, 0.12, 0.12) let group = new THREE.Group() group.position.copy(player.currentPano.position) group.add(wireframe) group.add(textMesh) this.wireframes.add(group) } }