Skip to content

语义分割

一、4 个基础工具

TIP

套索工具 + 多边形工具 + 矩形形工具 + 点工具

二、4 个标注模式

TIP

覆盖 + 不覆盖 + 更新 + 删除

三、语义分割整体方案

3.1、大体思路

  • 页面操作:在二维平面上绘制形状(套索、多边形、矩形和点),框选出三维点云。
  • 实现原理:将三维点云坐标转换成屏幕坐标,然后在二维平面上判断该点是否在绘制的形状内部。
  • 实现宗旨:将三维复杂的问题转换成二维简单的问题。

3.2、三维点云坐标转换成屏幕坐标

TIP

把大象放冰箱 总体分两步:三维点云坐标 -> NDC(归一化设备坐标)坐标 -> 二维屏幕坐标

3.2.1、三维点云坐标转换成 NDC(归一化设备坐标)坐标

js
// point 是单个点云坐标,camera 是相机
point.clone().project(camera);

// or 
const matrix = new THREE.Matrix4();
matrix.copy(camera.projectionMatrix);
matrix.multiply(camera.matrixWorldInverse);
point.clone().applyMatrix4(matrix);

3.2.2、NDC(归一化设备坐标)坐标转换成二维屏幕坐标

js
const unProject = (pos: { x: number; y: number }) => {
    return { x: ((pos.x + 1) / 2) * view.width, y: (-(pos.y - 1) / 2) * view.height };
};

3.3、在二维平面上判断一个点是否在绘制的形状内部

TIP

能偷懒就偷懒先判断包围框,可以简化计算

lasso

js
pos.x >= minV2.x
pos.y >= minV2.y
pos.x <= maxV2.x
pos.y <= maxV2.y

3.3.1、方案一:向量叉乘 ×

  • 遍历多边形的所有边,利用向量的叉乘计算方向,可以得出点在边的左侧还是右侧
  • 每次都需要重复计算,计算量大。

3.3.2、方案二:射线交点法 ×

  • 从该点向任意方向发出一条射线,统计射线与多边形的边相交的次数,奇数,则点在多边形内部;偶数,则点在多边形外部
  • 只适用于凸多边形。

3.3.3、方案三:夹角求和 ×

  • 计算点与多边形所有相邻顶点的夹角,并求和。如果夹角的总和等于 2π 或 -2π,则点在多边形内部;否则,点在外部。
  • 只适用于凸多边形。

3.3.4、方案四:离屏 canvas 颜色判断 √

  • 先根据多边形绘制离屏 canvas,将多边形内部的颜色填充为红色。只需要查询点的颜色即可:红色在多边形内部,白色多边形外部。
  • 离屏 canvas 只需要绘制一次,计算量小。

四、分割标注物体显示问题

4.1 着色器字段设计

TIP

大体思路:两个数字表示一个点的语义分割信息,在着色器中控制最终的颜色显示

4.1.1、第一个数字

  • 代表标签颜色
  • 需要预留无标签的颜色或者直接在着色器中写死

4.1.2、第二个数字

  • Default = 0, // 默认分割
  • Hide = 1, // 隐藏
  • Discard = 2, // 透明度 0
  • Highlight = 3, // 分割批注高亮
  • Fade = 4, // 透明度 0.6

4.2 顶点着色器代码(简化版)

glsl
    precision mediump float;
    precision mediump int;

    uniform int isSegmentMode;
    // 分割漏标
    uniform float missedHighlight;
    // color
    #ifdef COLOR_N
    uniform vec3 classColorTable[COLOR_N];
    #endif

    attribute vec2 property;

    // matrix
    uniform mat4 modelViewMatrix; 
    uniform mat4 projectionMatrix; 

    // X
    uniform vec2 lengthRange;
    // Y
    uniform vec2 widthRange;
    // Z
    uniform vec2 heightRange;
    uniform vec2 pointHeight;
    uniform float pointSize;

    attribute vec3 position;
    //attribute vec4 color;
    attribute vec3 color;

    varying vec3 vColor;
    varying float vOpacity;

    void main()	{

        vOpacity = 1.0;
        float vDiscard = -1.0;
        float vPointSize = pointSize;

        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        gl_PointSize = pointSize;
        if(position.z>heightRange.y||position.z<heightRange.x||position.x>lengthRange.y||position.x<lengthRange.x||position.y>widthRange.y||position.y<widthRange.x){
            vDiscard = 1.0;  
        }

        bool isSegmentObject = property.x > 0.0;

        bool isHide = property.y == 1.0;
        bool isDiscard = property.y == 2.0;
        bool isHighlight = property.y == 3.0;
        bool isFade = property.y == 4.0;
        bool isMissedLight = missedHighlight > 0.0;

        if(isHighlight){
            // 分割批注高亮
            vColor = vec3(1.0,0.0,0.0);
            vPointSize += 2.0;
        } else if (isSegmentObject && !isHide && isSegmentMode == 1) {
            int _id = int(abs(property.x));
            vColor = vec3(1.0, 1.0, 1.0);
            #ifdef COLOR_N
            if (_id > 0 && _id <= COLOR_N) {
                vColor = classColorTable[_id - 1];
            }
            #endif
        } else if(!isSegmentObject && isMissedLight && isSegmentMode == 1){
            isFade = false;
            vColor = vec3(1.0, 0.0, 0.0);
            vPointSize += 4.0;
        }

        if (isSegmentMode == 1) {
            if(isDiscard){
                vDiscard = 1.0;
            }else if(isFade){
                vOpacity = 0.6;
            }
        }

        if(vDiscard > 0.0) {
            gl_Position = vec4(2.0,2.0,2.0,1.0);
            vOpacity = 0.0;
        }

        // 漏标 size 修改
        gl_PointSize = vPointSize;

    }

五、分割物体被选中方案

TIP

select

5.1、点云拾取方案一:离屏渲染拾取 ×

js
    const { offsetX, offsetY } = event;
    const { renderer, height, width } = view;
    renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY - 1, 1, 1, pickUnit);
    const [r, g, b, a] = pickUnit;
    const points = view.pointCloud.groupPoints.children[0] as Points;
    const position = points?.geometry.getAttribute('position');
    const idx = r + g * 255 + b * 255 * 255 - 1;
    if (idx > 0 && position) {
        pickPos.fromBufferAttribute(position, idx);
        pickPos.idx = idx;
        return pickPos;
    }

5.1、点云拾取方案二:射线投射拾取 √

js
    const { offsetX, offsetY } = event;
    const { height, width } = view;
    const points = view.pointCloud.groupPoints.children[0] as Points;
    const position = points?.geometry.getAttribute('position');
    let x = (offsetX / width) * 2 - 1;
    let y = (-offsetY / height) * 2 + 1;
    view.raycaster.setFromCamera({ x, y }, view.camera);
    let intersects = view.raycaster.intersectObject(
        view.pointCloud.groupPoints.children[0],
        true,
    );
    if (intersects.length > 0) {
        const idx = intersects[0].index as number;
        if (idx > 0 && position) {
            pickPos.fromBufferAttribute(position, idx);
            pickPos.idx = idx;
            return pickPos;
        }
    }