项目上用到了segmentAnything,得到的蒙版有可能会有一些小的干扰区域,这个时候可以通过计算连通域,剔除掉脱离主体的干扰部分,主要用到opencv的 connectedComponentsWithStats函数

js中的函数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* This is an overloaded member function, provided for convenience. It differs from the above function
* only in what argument(s) it accepts.
*
* @param image the 8-bit single-channel image to be labeled
*
* @param labels destination labeled image
*
* @param stats statistics output for each label, including the background label, see below for
* available statistics. Statistics are accessed via stats(label, COLUMN) where COLUMN is one of
* ConnectedComponentsTypes. The data type is CV_32S.
*
* @param centroids centroid output for each label, including the background label. Centroids are
* accessed via centroids(label, 0) for x and centroids(label, 1) for y. The data type CV_64F.
*
* @param connectivity 8 or 4 for 8-way or 4-way connectivity respectively
*
* @param ltype output image label type. Currently CV_32S and CV_16U are supported.
*/
export declare function connectedComponentsWithStats(image: InputArray, labels: OutputArray, stats: OutputArray, centroids: OutputArray, connectivity?: int, ltype?: int): int;

假设输入是1024*768的图像
image 是输入图像, Uint8的Mat,尺寸为1024*768*4
labels 是输出结果,Int32的Mat, 尺寸为1024*768
stats 是每个连通域的信息,大小为 连通域数量* 5,每个row分别是长度为5的数组,分别记录了连通域外接矩形左上角像素点的xy宽度高度连通域的面积
centroids 是连通域的中心点坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. 将原始蒙版转化为灰度图像
const grayIm = new cv.Mat();
cv.cvtColor(im, grayIm, cv.COLOR_BGR2GRAY);
// 2. 得到连通域
const labels = new cv.Mat();
const stats = new cv.Mat();
const centroids = new cv.Mat();
const cnt = cv.connectedComponentsWithStats(grayIm, labels, stats, centroids, 8);
const miniAreaIndexes = [];
// 3. 计算总面积
let totalArea = 0;
for (let i = 0; i < cnt; i++) {
const area = stats.row(i).data32S[4];
if (i !== 0) totalArea += area;
}
// 4. 记录需要过滤的连通域index
for (let i = 1; i < cnt; i++) {
const area = stats.row(i).data32S[4];
// stats.row(0)表示的是整个原始蒙版背景
// 这里判断需要过滤的连通域标准是:面积小于原始蒙版面积的1%且小于所有连通域平均面积的10%
if (area < stats.row(0).data32S[4] * 0.01 && area < (totalArea / cnt) * 0.1) {
miniAreaIndexes.push(i);
}
}
//5.将原始蒙版中不符合要求的部分剔除
for (let i = 0; i < labels.data32S.length; i++) {
const label = labels.data32S.at(i);
if (miniAreaIndexes.includes(label) && label !== 0) {
im.data.set([0, 0, 0, 0], i * 4);
}
}

基本概念

仿射变换(Affine transformation),又称仿射映射,是指在几何中,对一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。

矩阵表示

计算机中,通常使用4*4的矩阵来表示放射变换
放射变换矩阵

平移

\[ \left[ \begin{matrix} 1 & 0 & 0 & x \\ 0 & 1 & 0 & y \\ 0 & 0 & 1 & z \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]

旋转

  • 绕X轴 \[ \left[ \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & cos\theta & -sin\theta & 0 \\ 0 & sin\theta & cos\theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]
  • 绕Y轴 \[ \left[ \begin{matrix} cos\theta & 0 & sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -sin\theta & 0 & cos\theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]
  • 绕Z轴 \[ \left[ \begin{matrix} cos\theta & -sin\theta & 0 & 0 \\ sin\theta & cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]

缩放

\[ \left[ \begin{matrix} x & 0 & 0 & 0 \\ 0 & y & 0 & 0 \\ 0 & 0 & z & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]

剪切

\[ \left[ \begin{matrix} 1 & yx & zx & 0 \\ xy & 1 & zy & 0 \\ xz & yz & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]

所有复杂的变换都可以通过上述矩阵通过连乘得到

变换方法

OCCT中有BRepBuilderAPI_TransformBRepBuilderAPI_GTransform两种

BRepBuilderAPI_Transform

  • 保持Shape的类型
  • 保留变换矩阵,存储在Shape的Location
  • 本质上是修改Shape的Location,在Shape本身已有Location变换的情况下,省去了额外的计算
  • 速度快
  • 缩放只能能比缩放

BRepBuilderAPI_GTransform

  • 缩放可以任意比例缩放
  • Shape的类型有可能发生改变 (例如Line变成Spline,Plane变成SplineFae)
  • 不保留变换矩阵
  • 速度慢

基本概念

在OCCT中,主要有两种数据,一种是Geometry几何数据,一种是Topology拓扑数据。

Topology拓扑数据是抽象类型可以总结为以下几种: - Vertex 点 0维 - Edge 边 - Wire 由顶点相连的边组成的序列 - Face plane2D平面 或者 surface 3D表(曲)面 上由Wire所围成的一块区域 - Shell 由一组通过边连接的面组成的Wire boundaries线框 - SolidShell包围的3D空间的一部分 - Compound Solid Solid的集合

WireSolid 可以闭合也可以infinity不闭合

对应到OCCT中的类型有,即TopAbs_ShapeEnum的枚举值有: - COMPOUND: 一组任意类型的拓扑数据 - COMPSOLID: Compound Solid - SOLID: Solid - SHELL: Shell - FACE: Face - WIRE: Wire - EDGE Edge - VERTEX: Vertex - SHPAE: TopoDS_Shape是所有类型的基类

遍历方法

通常的遍历方法是通过TopExp_Explorer
例如遍历一个面的所有边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// oc 是已经加载的 OpenCascadeInstance 实例
// face 是 TopoDS_Face 类

// explorer 是一个迭代器
const explorer = new oc.TopExp_Explorer_2(face, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE)
while (explorer.More()) {
// 通过explorer.Current() 拿到当前只向的TopoDS_Shape
// 由于OCCT基于C++编写,是强类型的,所以需要通过下面的方法声明为TopoDS_Edge类型
const edge = oc.TopoDS.Edge_1(explorer.Current())
// 后续操作
// ...
// 后续操作
}

不过这样的遍历并不保证顺序,即遍历得到的边不保证是首尾相连的
如果需要按照顺序遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// oc 是已经加载的 OpenCascadeInstance 实例
// face 是 TopoDS_Face 类

// explorer 是一个迭代器
// 使用 BRepTools_WireExplorer 替代 TopExp_Explorer 遍历Face/Wire的Edge可以保证顺序
const explorer = new OcctInstance.oc.BRepTools_WireExplorer_3(outerWire, face)
while (explorer.More()) {
// 通过explorer.Current() 拿到当前只向的TopoDS_Shape
// 由于OCCT基于C++编写,是强类型的,所以需要通过下面的方法声明为TopoDS_Edge类型
const edge = oc.TopoDS.Edge_1(explorer.Current())
// 后续操作
// ...
// 后续操作
}

最佳实践

opencascade.js中,创建的OCCT对象不会被JavaScript的垃圾回收机制自动清理,需要手动调用delete方法删除内存占用。因此,应该尽可能少地创建对象 例如,需要遍历face1 和 face2 的边,可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在某处创建迭代器
const explorer = new oc.TopExp_Explorer_1()
for (explorer.Init(face1, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE); explorer.More(); explorer.Next()) {
// ...
}

for (explorer.Init(face2, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE); explorer.More(); explorer.Next()) {
// ...
}

// 销毁迭代器
explorer.delete()

2024年已经过去将近1个月,但是距离放假春节还有半个月。
年度的汇报已经结束,整个人处于一种无心工作的状态。
年度汇报时,研发总监提到了一点"我知道你的个人能力很强,但是我希望你能把这种能力分给其他人"
我一直很在意这句话
我一度一名技术领导(组长)的责任,仅限于 1. 确定合适的技术路线 2. 合理的拆解并分配工作

但是仅仅这样并不能提升整个团队的水平。团队水平的提高需要技术的沉淀与积累和成员之间的默契配合
于是,忽然想到,对于一些技术文档就是最好的技术沉淀形式

过去的问题

曾经也有尝试过些一些文章,但技术水平有限,更多的只能是操作步骤的记录。 也尝试过更深层一点的,但是太基础的不想写,太深层的又没有足够的理解能将其通俗地表述。

今后的方向

  1. 关于个人博客的整个工具链
  • 图床
    ......
  1. 回顾项目中遇到的问题和解决办法并重新整理

这是一个新的FLAG

一、 使用一个材质

利用材质的OnBeforeCompile, 例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extrudeMeshMat.onBeforeCompile = function(shader) {
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
`
#ifdef OPAQUE
diffuseColor.a = 1.0;
#endif

#ifdef USE_TRANSMISSION
diffuseColor.a *= material.transmissionAlpha + 0.1;
#endif

if ( gl_FrontFacing ) { \
gl_FragColor = vec4( 1.0, 0.0, 0.0, diffuseColor.a ); \
} else { \
gl_FragColor = vec4( 0.0, 1.0, 0.0, diffuseColor.a ); \
}
`
)
};

着色器参数diffuseColor对应的是material.color设置的颜色,gl_FragColor对应的是最终渲染的颜色,gl_FrontFacing表示正面还是反面,在适当的位置,将gl_FragColor的初始化根据gl_FrontFacing对gl_FragColor进行不同的操作就可以得正反面颜色不同的效果

二、 使用材质数组

1. 创建材质数组,例如

1
2
3
const material1 = new THREE.MeshBasicMaterial({color: 0xff0000});
const material2 = new THREE.MeshBasicMaterial({color: 0x00ff00});
const materials = [material1, material2]

2. 如果有顶点索引,则遍历顶点索引,再添加反面的顶点索引;如果没有顶点索引,则遍历顶点,创建正面和反面的顶点索引。使用setIndex函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const finalVertices = <THREE.Vector3[]>[...] // 几何体的顶点
const geo = new THREE.BufferGeometry()
geo.setFromPoints(finalVertices)
// ! 计算法向量要在只有一个面的时候计算
geo.computeVertexNormals()
const newIndexes = <number[]>[]
const count = finalVertices.length
for(let i = 0; i < count; i += 3) {
newIndexes.push(i, i + 1, i + 2)
}
for(let i = 0; i < count; i += 3) {
newIndexes.push(i, i + 2, i + 1)
}
geo.setIndex(newIndexes)

3. 使用setGroup函数,将当前几何体分割成组进行渲染

1
2
geo.addGroup(0, count, 0)
geo.addGroup(count, count, 1)
demo

个人而言,对微商非常反感,因为这些人会在朋友圈刷屏。然而不久前连手机都不会用的我妈,居然也加入了微商的行列。本文针对“归农”进行具体分析,剖析微商的本质。带有个人主观色彩的话尽量当做备注放在括号中。

阅读全文 »

百度前端技术学院刚开课的那段时间,也是我准备开始钻研前端的时候。水平差得脸看过了百度阿里的前端面试题之后,都不知道差在哪里。可那时课程看起来似乎过于基础。现在看起来是到了可以刷一波的时候了。在没有一定的认识之前是没有资格谈论喜好的。

从开始折腾ASP.NET CORE到现在开始折腾Node.js。虽然已经使用ASP.NET CORE搭建过小型动态网站,但是对于HTTP协议的认识还是基本为零。不得不承认.NET的强大。但我一直认为只有对细节有足够的了解才能走得更远。

响应和请求

客户端(浏览器)通过URL或者页面上的交互组件向服务器发出请求,服务器经过处理向客户端(浏览器)返回响应

请求分为getpost两种,具体差别

简单来说get快速,但是数据大小有限,不安全;post慢,但是数据没有限制,相对安全

在Express中,有专门的对象用以处理不同类型的请求

1
2
3
4
5
6
7
8
var app=express();

app.get('/',response(req,res){ //用以处理get请求

})
app.post('/',response(req,res){ //用以处理post请求

})

记录一些经常需要复制粘贴的代码

数据库连接字符串(EF Core)

MySql >Server= ;port= ;Database= ;User Id = ;Password= ;sslmode=none

SqlServer >Server= , ;Database= ;User Id = ;Password= ;

划重点MySql使用port声明端口,SqlServer使用,

EF Core

DB First

MySql
不贴了,对2.0支持不好,可以利用SqlServer中转

SqlServer >Scaffold-DbContext "Server= ;Database= ;User Id = ;Password= " Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

SqlServer在执行操作后,需要将XXXContext中的OnConfiguring()替换为 >public XXXContext(DbContextOptions options) : base(options) { }

其中XXX为数据库名称

0%