五仁wr
type: channel
link: https://www.orzzz.cn/
description: 五仁のBLOG
items: 【五仁python】用代码处理Excel表格——pandas初级基础
items: 【五仁科技】如何在你的朋友中发现小团体？——社区检测
items: 【五仁C++】opencv 的安装与使用（源码编译）
items: 【五仁图形学】Unity Shader 来个最简单的顶点片元着色器 Helloworld!
items: 【五仁科技】高动态范围（HDR）成像——debevec算法
items: 【五仁图形学】Unity Shader 基础
items: 【五仁python】来给你的朋友做一个随机涩图程序
items: 【五仁python】用简单代码压缩图片
items: 【五仁C++】静态库？动态库？.a.so.lib.dll的生成与使用(g++版)

【五仁python】用代码处理Excel表格——pandas初级基础
type: item
date: 2024-06-13T19:27:00+08:00
description: 无论是打数模竞赛，还是日常办公，总会遇到以$Excel$ 表格提供源数据的场景。然后我们就需要对其中的数据作处理、运算，最终再以$Excel$ 表格的形式把结果给呈现回去。如果是简单的一行两列的操作，直接在$office$ 上就能完成，而一旦有一些稍微复杂点的需求，那么使用程序来编写脚本则是非常合适的选择！其中，python 的 pandas 库能满足大部分需求。就不能不用 pandas 么起初我也有这个质疑，毕竟在程序员眼里，表格不就是个二维数组嘛，那不如只使用读/写接口，其他的运算就转为普通的数组之后再自己实现嘛。而如果仅仅是对$Excel$ 表格（后缀为.xlsx的文件）作读写，python 内置的 csv 也能完成这个任务，根本不需要用 pandas，还搞得那么复杂。对于数模比赛，因为表格中一般没有字符串，确实可以只用读/写接口。而一旦涉及日常办公，里面往往出现较多的字符串，此时表格的每一列的格式不同，不能直接无脑遍历，然后还要记住字符串列的下标，再用 if 作额外判断。列数多起来时，变量数蹭蹭往上涨，难度不亚于$CSP$ 第3题的大模拟，而且可读性极差，要一边打开表格，一边对照数组的下标看看是用的哪一个数，过个两天根本看不懂自己写了啥。所以，用 pandas 也许不能优化算法的时间复杂度，但一定可以让代码变得美观易懂，并提升开发效率。先学IO当然是先学输入输出啦！读入都不会，怎么处理数据呢，QwQ。话不多说，先放代码：import pandas as pd df = pd.read_excel("./输入.xlsx") # 普通读入，会自动忽略第一行，将其视为表头 df = pd.read_excel("./输入.xlsx", header = None) # 把第一行视为数据，额外添加0,1,2,...作为默认表头 df.to_excel("./输出") # 普通输出，加一列填入行下标 df.to_excel("./输出",index = None) # 不输出行下标安装库的话，一句pip install pandas即可。导入库时，往往会命名为 pd，这只是大伙的习惯，跟 numpy 命名为 np是一个道理，在网上搜或是问$ChatGPT$ 也会得到这种命名方式。使用pandas库时，存储表格的对象类型叫做 DataFrame，所以往往命名为 df。让我们看上述代码，输入用一句read_excel函数即可搞定，但需要注意自己的$Excel$ 表格的真正的数据是从第几行开始的，read_excel函数默认最顶上的 1 行为表头，读入后成为列下标，实践中不注意这个的话可能会漏掉一条数据。输出也是用一句to_excel函数就行，也一样要区分是否输出行下标。pandas 的类的性质使用 pandas 库时，它的对象类型跟数组有关系，但依然有较大的区别。首先是存储表格的对象类型为 DataFrame，它类似一个二维数组，但是使用单个下标访问 DataFrame时，访问的是一整列，而不是一整行，而我们平时使用的numpy数组，单下标会访问一整行，这两者有一个转置的关系。所以想遍历表格的每一行时，不能用i作为下标去遍历，要用专门的遍历方式（后面会说）。其次，我们常常会接触到的一个类型为 Series，它类似一个一维数组，其实就可以理解为 DataFrame的某一行或某一列，当使用单下标去访问 DataFrame时，返回的对象就是 Series类型的。在 DataFrame 中，既有行下标，也有列下标。这两种下标既可以是自然数$0,1,2,\cdots$，也可以是字符串或布尔值等，它综合了python 中的列表和字典的访问方式，甚至额外加了其他的方式。所以后面一定要分清哪个是下标，哪个是真正返回的数。DataFrame 的访问当我们手上读入一个表格时，无论要遍历还是查询还是修改，前提都是访问数据。我们可能需要访问其中的某一列、某一行、某一格、满足条件的某一列、满足条件的某一行等等，这些其实都属于语法范畴，按模板抄就行。下面将列出访问表格数据的种种情况。转为numpy数组返璞归真式解决方案，如果表格本身是纯数值（没有字符串等），那么直接转为numpy数组处理是非常简单易读的，而且 DataFrame和numpy数组的相互转换非常方便。numpy_array = df.to_numpy() df = pd.DataFrame(numpy_array) # 处理完之后可以将 numpy 转回 DataFrame用下标访问 DataFrame自然数下标。df[0]，使用自然数作为下标时，返回的是一整列列名下标。df["学号"]，使用列名作为下标时，返回的是一整列布尔数组下标。df[df["语文成绩"]>90] 使用布尔Seires作为下…

【五仁科技】如何在你的朋友中发现小团体？——社区检测
type: item
date: 2022-08-09T00:45:00+08:00
description: 复杂网络学科大量的实证研究发现很多网络存在聚簇效应，比如常见的社交网络。正所谓人以群分，物以类聚。在数据挖掘中，有一个分支叫做社区检测（community detection），用于在复杂的网络中寻找无形的群聚关系。任务描述假如我们现在知道很多名微信用户，并且只知道这些微信用户之间的好友关系，即知道任意两个微信用户是否互为好友，就像下图一样。显然，在人与人的交往中难免产生一些小团体，就像现实生活中有同一个学校、同一个班级、同一个宿舍的学生会聚成不同层级的小团体，在微信中也存在群聊的功能。我们现在的任务是，试图只从多个微信用户之间的好友关系，去猜测这些用户的小团体，也就是把这些用户划分为一个一个不同的集合（假设一个用户只属于一个小团体）。数据挖掘就是在一堆看似杂乱无章的数据中发现隐含的有价值的信息。在一般的机器学习中，聚类算法是在一些各自独立的向量上进行操作的，目的是将独立的向量划分成若干个聚簇。而社区检测可以看作是图论上的聚类算法：在一个拥有若干个节点和边的图中，将这些节点划分为若干个社区（聚簇）。不同的地方在于，机器学习中的经典聚类是用样本自身的属性来计算，通过每个个体自身的值来得到个体之间的相似性，从而划分为不同类。而社区检测则不关注每个点，而是只知道每个个体之间的关系，通过点与点之间的边来把整个图的点划分成若干个集合。朴素思想我们不妨先看看下面这张图，看到下图中的网络，你想把它分成几个社区呢？怎么分，每个节点属于哪一个社区呢？我相信，绝大多数人的第一感觉都是把这个网络分成左右 2 个社区，其中[0,1,2,3,4]节点属于第 1 个社区，而[5,6,7,8,9]节点属于第 2 个社区（当然，社区的顺序可以随意）。为什么会有这样的第一感觉呢？虽然有很多种可能，但我们可以合理地怀疑跟边的密集程度有关。设想在学校中，同一个班级的学生，互为微信好友的可能性很高，甚至可能是完全图，即每个同学都与同班所有其他同学互为微信好友，而两个学生在分别属于两个不同班级的情况下，他们互为微信好友的可能性就比较低了。也就是说，假如我们划分出一个点集，这个点集的边的密度非常高，那么这个点集就应该能划分成一个社区。那么，如何来衡量一个点集的边的密度？直接用边的数量除以点的数量吗，可能是其中一种方法，但仔细想想却并不合理。这里就不再卖关子了，我们直接引出我们的 modularity（模块度） 的概念。在对网络进行某个社区划分之后，模块度就可以用于测量这样划分得好不好，也可以理解为用于测量社区内的边的密度高不高。Modularity 模块度Modularity(模块度), 这个概念是2003年一个叫Newman的人提出的。这个人先后发表了很多关于社区划分的论文，包括2002年发表的著名的Girvan-Newman(G-N)算法，和2004发表的Fast Newman(F-B)算法，Modularity就是F-B算法中提出的（03年提出的概念，04年确认发表）。在2006年的时候Newman重新定义了Modularity，实际上只是对原来的公式做了变换，使其适用于Spectral Optimization Algorithms。早期的算法不能够很好的确认什么样的社区划分是最优的。Modularity这个概念就是为了定义一个对于社区划分结果优劣的评判。接下来，我们以一种相对抽象的想法来理解模块度。假如一个网络，考虑它的某一个点集，点集里面的每一个点都与该点集里面的其他点有较多的边相连，而与其他点集的点只有较少的边相连，那么这些点的边是不均匀分布的。点集中某个点的邻边，有更大的概率与点集内的点相邻，这可以看作是社区的一种“特征”。那么这种不均匀的分布会导致什么呢？我们来考虑一种叫 “平均图” 的东西，它的点的邻边的分布是比较均匀，这种图的点与点之间一视同仁，并不存在明显小团体。那么，当我们用原来的不均匀分布的网络，减去这种“平均图”，那么边比较密集的那些区域就会体现出来。这样的说法有点抽象，可以慢慢理解。于是，我们现在就产生了一种方法，去求得这个“平均图”。如果使用正则图就有点太过离谱了，毕竟我们只在意每个点的邻边的分布，而并不需要改变每个点的度数（邻边的数量）。所以，我们可以通过点的度数来计算边权，从而在得到“平均图”的同时，保证邻边的期望值（边权的和）与原来的网络相等。所以，我们现在可以把“平均图”称作期望图。假设在原来的网络中，有 m 条边，则总度数为 2m，有任意两个节点 i 和 j (允许i=j)，它们的度数分别为 $k_i$ 和 $k_j$，那么在期望图中，节点 i 和 j 相邻边的边权就为 $\frac{k_ik_j}{2m}$。用数学表达就是：$$e(i,j)_{i\in N,j\in N}=\frac{k_ik_j}{2m}$$其中 N 为网…

【五仁C++】opencv 的安装与使用（源码编译）
type: item
date: 2022-04-27T22:47:00+08:00
description: 在计算机领域学习，多多少少都会遇到跟图像相关的领域，比如计算机视觉、图形学、图像处理。跟图像打交道的话，一般都会用到第三方库，而 opencv 就是一个非常常见的图像库。使用 opencv 需要先安装，对于 python 来说，安装只是一句命令行的事；而对于 c++ 来说，安装第三方库就没这么简单了，往往都会经历一些磕磕碰碰，那么这次就来介绍一下如何安装和使用 opencv。推荐阅读 【五仁C++】静态库？动态库？.a.so.lib.dll的生成与使用(g++版)为什么要源码编译opencv 是提供了 windows 二进制安装包的，也就是那种，用鼠标点几下就能成功安装，但是这种方式安装的 opencv 只能在 VS 里面使用，不能用通常的方式在其他的编译器中使用（微软不愧是你）。用过 VS-code 的应该知道，VS-code 本质上就是使用命令行来编译，通过调整 g++ 的参数来链接第三方库，想要以这种方式来使用 opencv，就必须进行源码编译。当然，源码编译出来的 opencv 在 VS 也能用。所以说，如果只想在 VS 里面使用 opencv 的话，就不需要看这篇文章后面的内容了，直接去官网下载个二进制安装包，鼠标点两下就安装完了。前置条件已安装 cmake 和 mingw先下载这是 opencv 的官网：https://opencv.org，从官网可以去到文档、论坛等地方，也可以去到 github。我们可以直接去 https://github.com/opencv/opencv/releases 找 opencv 的多个发行版本，在这些版本里面，以3开头的版本和以4开头的版本有较大的差别，它们的函数接口的规范可能都不一样，如果你的朋友使用 opencv 的 3.4.15 版本，而你下载了 4.5.5 版本，那么你朋友的代码在你的电脑上很可能无法运行，而如果你下载了 3.4.16 版本，虽然也可能有版本不兼容的问题，但这种可能性比较小。以我举例，我习惯了用 3 开头的版本，所以我就找到 3 开头的最晚的版本 3.4.16，然后下载它的源码。如下图，Source code 就是我们需要的源码，其他的不需要下载。提一下，下图那个 opencv-3.4.16-vc14_vc15.exe 就是前面提到过的用鼠标点几下就能安装的二进制安装包，不过只能在 VS 里面使用。下载完之后，解压到自己喜欢的位置（这个总不用教吧），然后随便新建一个文件夹，作为安装 opencv 的位置。如下图，我新建了一个名为 QwQ 的文件夹，这个文件夹的位置其实没有规定，你喜欢安装在哪里都行，但大伙通常都喜欢把它跟源码放在同一个位置，并且这个文件夹的命名一般为 build，不过这些都是约定俗成的东西。再重复一遍，我们需要新建一个文件夹来作为安装目录，文件夹的位置和命名都没用硬性要求，只不过一般情况下都跟源码放在一起，且命名为 build，不过我个人更喜欢用 QwQ 命名。跟着做下面将演示，如何利用 cmake 和 mingw 从源码编译出 opencv。运行 cmake 的 bin 目录下的 cmake-gui.exe，将会打开一个面板，我们首先要对 2 个地方进行设置。“where is the source code” 指的是源码目录，也就是我们从 github 上下载后解压出来的目录；“where to build the binaries” 指的是安装目录，也就是我们刚刚新建的目录。修改这两项，分别指向正确的目录（如下图所示）修改好两个目录之后，点击左下角的 Configure 按钮，此时正在进行的是配置编译器，它会弹出窗口让我们选择等下用于编译的编译器，如下图所示进行选择：点击 Next 按钮，如果是第一次选择某个编译器，它会弹出窗口让我们设置编译器在哪，选择 MinGW 中的 C 和 C++ 编译的 .exe 程序，他们在 bin 目录下（前提是已经安装好mingw）选择完编译器后就会开始第一步的配置，这一步主要是配置跟编译器相关的东西，请耐心等待右下角的进度条读完。上一步是配置编译器，完成之后，第二步将会进行安装选项的配置。opencv 的工程师们制作出了很多东西，但有些东西是我们不需要的，比如工具包里面有剪刀、锤子、钳等很多工具，我们往往只挑选自己需要的几个工具，这样可以节省空间。所以，第二步的配置主要是选择最后安装多少。每个选项都有默认值，我们唯一要修改的是 BUILD_opencv_world 这个选项，它的默认值是 false，也就是不打勾；我们需要手动将它的值设置为 true，也就是打勾。这个选项的主要作用是将编译后的多个库文件打包成一个，如果不勾选的话，使用 opencv 时常常要手动链接多个库文件，勾选之后就只用链接一个库…

【五仁图形学】Unity Shader 来个最简单的顶点片元着色器 Helloworld!
type: item
date: 2022-03-30T00:40:16+08:00
description: 任何的程序，任何的语言，都有 Helloworld，它是初学者迈入新世界的大门，Unity Shader 也不例外。在实现光彩夺目的画面之前，我们首先来实现一个最简单的 Unity Shader。Helloworld以我以往的经验，此时应该直接写出所有的步骤，而不涉及任何的原理。也就是说，只动手，不动脑。（1）在 Unity 中新建一个场景，操作是点击菜单栏中的 File → New Scene，然后点击 Create，即可创建一个含有摄像头和平行光的场景。（2）新建一个 Unity Shader，操作是在项目面板中单机创建按钮，然后选择 shader，选任意一个模板都可以。（3）新建一个材质，类似于上一步，在项目面板中创建。然后把第 2 步创建的 Unity Shader 赋给这个材质，操作是在项目面板中把 Unity Shader 文件拖拽到材质文件上。 （4）在场景中新建一个球体，操作是在层级面板中创建。然后把第 3 步创建的材质赋给这个球体，操作是把项目面板中的材质文件拖拽到层级面板的球体上。（5）双击打开第 2 步创建的 Unity Shader。删除里面的所有代码，把下面的代码粘帖进去。Shader "Wuren/Helloworld" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag float4 vert(float4 v : POSITION) : SV_POSITION { return UnityObjectToClipPos(v); } fixed4 frag() : SV_Target { return fixed4(1.0, 1.0, 1.0, 1.0); } ENDCG } } }保存之后，返回 Unity 会自动编译，就能查看结果了。这段 shader 代码的效果如下图所示：首先，代码的第一行通过 Shader 语义定义了这个 Unity Shader 的名字。正如 C++ 的函数不一定要传入参数，Unity Shader 也不一定需要 Properties 语义块，我们可以选择不声明任何材质属性。然后，我们声明了 SubShader 和 Pass 语义块，没有进行任何的渲染设置和标签设置。接着，就是由 CGPROGRAM 和 ENDCG 所包围的 CG 代码片段，这是我们的重点，在 Unity Shader，被 CGPROGRAM 和 ENDCG 包围的部分才是我们真正用于计算的代码，在其中可以发挥我们无穷无尽的创意。在 CG 代码中，首先是两行非常重要的编译指令:#pragma vertex vert #pragma fragment frag它们将告诉 Unity，哪个函数包含了顶点着色器的代码，哪个函数包含了片元着色器的代码，上面这两句对应了后面的 vert 函数和 frag 函数。当然，想要个性化也可以写 #pragma vertex QwQ 并且后面定义一个 QwQ 函数作为顶点着色器。接下来，我们看一下 vert 函数的定义：float4 vert(float4 v : POSITION) : SV_POSITION { return UnityObjectToClipPos(v); }它跟 C 语言很像，函数的输入是 float4 类型，即 4 维向量，变量名是 v，返回值也是 float4 类型的变量。特别的地方在于函数中的 POSITION 和 SV_POSITION，它们是 CG/HLSL 语言中的语义（semantics），这是不可省略的，它们的存在，给变量赋予了一个身份。在图形学的渲染管线中我们了解到，着色器是有特定的输入输出的，比如顶点的模型空间的坐标会作为顶点着色器的输入，且顶点着色器的输出至少包含顶点的裁剪空间的坐标。我们可以定义顶点着色器的输入和输出包含很多变量，那么哪个变量作为那些特定的输入输出呢？于是，我们用 CG/HLSL 语言中的语义来给变量赋予身份，比如这里 POSITION 将告诉 Unity，变量 v 的身份就是模型的顶点坐标， 于是 Shader 运行时就会把模型的顶点坐标填充到输入参数 v 中，SV_POSITION 将告诉 Unity，顶点着色器的输出是裁剪空间中的顶点坐标。而 UnityObjectToClipPos(*) 函数是 Unity Shader 的内置函数，我们用英文来理解它，就是把顶点在模型空间中的坐标变换成裁剪空间中的坐标。在以前，这种操作是通过 mul(UNITY_MATRIX_MVP,*) 这句代码来实现的，也就是左乘变换矩阵，UNITY_MATRIX_MVP 在以前是内置变量。现在只不过是换了种形式，原理都是一样的。像 Un…

【五仁科技】高动态范围（HDR）成像——debevec算法
type: item
date: 2022-03-23T19:52:10+08:00
description: 首先简述问题和任务：正常情况下拍的一张照片无法同时将明亮的部分和阴暗的部分都显示清楚，我们需要将曝光时间长的照片（用于看清暗部）和曝光时间短的照片（用于看清亮部）进行融合，得到一张不论明亮都非常清晰的照片。组合在不同曝光设置下获取的同一场景的不同图像的过程称为高动态范围 (HDR)成像。前置知识（1） 我们平时所看到的图片文件，比如jpg、png那些，像素值是0~255的整数，我们称为 LDR（低动态范围）图片；而文件后缀名为 .hdr 的图片的像素值是小数，不是整数，在正常情况下是不能直接打开的，需要通过某个算法将小数的像素值转化为整数，才能在计算机中正常打开，这个算法称为色调映射算法。而某些能直接打开.hdr文件的软件，如PS（Photoshop），其实是有一个自己内置的色调映射算法。（2） 辐射强度的概念：单位时间内物体单位表面积辐射出某特定波长射线的能量，称为单色波长的辐射强度。常用单位为：W/m2，瓦特/平方米。以下将辐射强度称为辐照度。（3） 响应曲线的概念：当我们拍照的时候，现实场景中的光照会转化照片中的像素值，这可看作是一种映射关系，即函数。我们用字母表示物理量，辐照度为 E，曝光时间为 Δt，曝光为 X，那么照片的像素值（整数）对曝光X的响应是一个函数，将现实场景映射到照片中，这个函数就称为相机的响应函数（CRF），也可以叫做响应曲线。在曝光时间 Δt 不变的前提下，当某个光源变成两倍亮时（辐照度变为两倍），照片中的像素值往往不会变成原来的两倍，所以说响应曲线是一个非线性函数。任务分解我们的目的是将多张不同曝光时间的照片合成一张照片，合成之后的那张照片既能看清楚很亮的区域，也能看清楚很暗的区域。我们的步骤一共有三步，请记住：① 生成相机响应曲线② 将多张照片融合，得到一个后缀为 .hdr 的图片文件，像素值为小数③ 色调映射，将上一步得到的 hdr 图片转化成能查看的 jpg、png 图片文件在计算机 opencv 库中，上述的三个步骤可以对应三个函数。需要注意的是，由于计算机的函数可以重载，所以同名函数可以传入不同的参数。为什么要说这个呢，因为上述的第 ② 步可以不依赖于第 ① 步。正常情况下，确实应该先求出响应曲线，然后第 ② 步利用这个响应曲线来融合照片。但是，如果没有求出第 ① 步的响应曲线，也不妨碍第 ② 步的执行，前面提到，真实的响应曲线一般是非线性的，换而言之，我们可以假设一个最理想但不真实的响应曲线，他是线性的，然后用这个线性响应曲线来融合图片。简单的说，就是第 ② 步的融合函数有两个重载，一个需要传入响应曲线后进行融合，这个响应曲线是第 ① 步生成的；另一个重载不需要传入响应曲线，它直接使用一个固定的线性响应曲线，当然，这种固定的线性响应曲线一般不符合真实场景，所以融合出的图片也容易不符合预期。Debevec算法下面我们来介绍一下 Debevec 算法：这个算法包含两个部分，对应第 ① 步和第 ② 步，即生成响应曲线和生成 hdr 图片。如何求相机响应曲线。已有的材料是几张有亮有暗的照片，已知是这几张照片的各个像素的像素值，以及这几张照片的曝光时间 Δt。各符号的意义：曝光 X，辐照度 E，曝光时间 Δt，像素值 Z前提假设：X=E·Δt，请务必记住这个假设，它是Debevec算法成功的前提，以后会介绍当这个假设不成立时，Debevec 算法会如何失败。X=E·Δt 可以理解为，当辐照度 E 固定时，曝光度 X 随着曝光时间 Δt 线性增大，从人体直观的感受上来看，就是曝光时间越久的照片越亮。事实上，严格满足 X=E·Δt 几乎是不可能的，但是，越接近这个式子，Debevec 算法的效果就越好。前面说过，相机响应曲线是曝光 X 到像素值 Z 的函数，我们设为 f，即 $Z = f(X) = f(E·Δt)$，我们对应到具体的某一张照片上，即 $Z_{ij} = f(E_i·Δt_j)$，其中 $Z_{ij}$ 为第 j 张照片的第 i 个像素，$E_i$ 为照片中第 i 个像素所对应的位置在现实世界中的辐照度，$Δt_j$ 为第 j 张照片的曝光时间。这里可能会有疑问，一张照片不是有长和宽吗，这明明需要 2 个坐标才能表示一个像素的位置，为什么下标 i 可以表示照片某个像素的位置呢？虽然图片是二维像素矩阵，但实际上可以压成一维，比如一张分辨率 2×5 的图片，实际上也可以表示为一个 10 维的向量，这样我们就能用一个下标来表示任意位置的像素了。第 6 个像素即对应于分辨率 2×5 图片中第 2 行第 1 列的像素。公式推演下面我们对 $Z_{ij} = f(E_i·Δt_j)$ 这条式子作变换，首先是初始公式：$$Z_{ij} = f(E_i·Δt_j) \tag{1}$$因为我们假设 f …

【五仁图形学】Unity Shader 基础
type: item
date: 2022-03-15T13:47:00+08:00
description: Shader 的中文翻译是着色器，它的作用是，将场景中的物体经过一些列计算，最终呈现在屏幕上的一个个像素点。要学会编写 Unity Shader，需要一些图形学的前置基础知识，包括渲染管线、线性代数、一点编程经验。在 Unity 中，如果想要呈现绚丽的画面，我们就要学会写 Unity Shader，本篇主要讲述了 Unity Shader 中的一些关键基础概念。一个单独的 Unity Shader 是无法发挥任何作用的，它必须和材质结合起来。Unity Shader 支持多种 shader 语言，但我们一般使用 CG 语言来写，即 C for Graphic。ShaderLabUnity 提供了一种专门为 Unity Shader 服务的语言——ShaderLab。在传统的编写着色器方式中，程序员往往要处理很多文件和设置，才能让画面呈现出想要的效果，比如载入模型、根据平台选择不同接口、设置着色器的输入、把资源加载到GPU、设置渲染状态、对渲染状态进行排序等等，而使用 ShaderLab 语言编写的 Unity Shader 可以完成这些所有的工作。ShaderLab 是一种说明性语言，它使用了一些嵌套在花括号内部的语义（syntax）来描述一个 Unity Shader 文件的结构Unity Shader 的结构Unity Shader 里有多种语义，比如 Properties、SubShader、Fallback等。这些语义定义了 Unity Shader 的结构，从而帮助 Unity 分析该 Unity Shader 文件，以便进行正确的编译。名字每个 Unity Shader 文件的第一行都需要通过 Shader 语义来指定该 Unity Shader 的名字。这个名字由一个字符串来定义，例如“QwQ”。当为材质选择使用的 Unity Shader 时，这些名称就会出现在材质面板的下拉列表里。通过在字符串中添加斜杠“/”，可以控制 Unity Shader 在材质面板出现的位置。例如：Shader "wuren/QwQ" { }材质和 Unity Shader 的桥梁: Properties在编写 Shader 时，我们往往需要一些“外来”的属性，比如场景中出现了两个球，我们想要它们使用同一种光照算法，那自然而然地，就会只写一个 Unity Shader，然后绑定到那两个球上。而如果我们想要两个球分别是红色和蓝色的，那么显然，“红”和“蓝”就要作为材质传给 Shader 的一个变量，然后 Shader 再根据这个变量进行一些列的运算。从 C++、Python 那些高级编程语言的角度来看，我们可以把 Unity Shader 看成一个大函数，Properties 就是这个函数的传入形参，只不过在 Unity Shader 中我们将其称为属性。Properties 语义块的定义通常如下：Properties{ Name ("display name", PropertyType) = DefaultValue Name ("display name", PropertyType) = DefaultValue }在上面的 Properties 语义块中，Name 指的是属性名，也是我们在 Shader 代码中使用的变量名；display name 指的是出现在材质面板上的名称，也就是说，一个属性其实有两个名，一个对内使用，一个对外作描述作用；PropertyType 指的是属性的类型，DefaultValue 指的是属性的默认值。如果对应 C++ 的话，C++ 函数的1个传入形参有变量类型、变量名和默认值，就像 void print (int a = 1) 这样，那么 Properties 语义块的 PropertyType 就对应了变量类型，Name 对应了变量名，DefaultValue 对应了默认值。下面是 Properties 语义块支持的属性类型：属性类型默认值的定义语法IntnumberFloatnumberRange(min,max)numberColor(number,number,number,number)Vector(number,number,number,number)2D"defaulttexture"{}Cube"defaulttexture"{}3D"defaulttexture"{}Int、Float、Range 是数字类型的属性，它们的值就是一个单独的数字；Color 和 Vector的值是四维向量，也就是 4 个数字；2D、Cube、3D 这 3 种是纹理类型，默认值是一个字符串后跟一个花括号，字符串要么是空的，要么是内置的纹理名称，如white、bump等，花括号本来用于指定纹理属性，…

【五仁python】来给你的朋友做一个随机涩图程序
type: item
date: 2022-03-08T23:59:00+08:00
description: 本篇仅供学习，请自觉遵守我国法律法规。科学上网，从我做起。偶然发现，有一个可以返回随机涩图的API https://api.lolicon.app/#/setu，不仅可以涩涩，还涉及了比较有意思的网络知识。这就又能水一篇博客了。 本篇有涉及 json 格式的文本，但没有对 json 作详细介绍，有兴趣的可以了解一下，不知道也不影响阅读。什么是API首先要知道什么是API，我从维基百科里找了一段解释：应用程序接口（英語：application programming interface），缩写为API，是一种计算接口，它定义多个软件中介之间的交互，以及可以进行的调用（call）或请求（request）的种类，如何进行调用或发出请求，应使用的数据格式，应遵循的惯例等。简单的来说，API 就是一种看不见的工具，它会告诉你怎么去使用这个 API，以及使用 API 后会得到什么样的结果，而我们并不知道其内部是怎么实现的。就好比我们都知道按下电饭煲的按钮就能煮好一锅饭，而不需要知道电饭煲内部的电路结构、电阻电压之类的具体实现。于是，对于一个 API 来说，我们只关心两个东西：用法和结果。API 可以分很多大类，我们将要用到的属于 Web API。可以试着选择下面的一个链接，复制粘贴到浏览器上，并进行访问，看看会的到什么结果。然后重复操作，重新粘贴再访问（无重定向的情况可以直接刷新），看看得到的结果有什么不同。https://v1.hitokoto.cn/https://api.9jojo.cn/hitokotohttps://api.9jojo.cn/acgpichttps://api.9jojo.cn/acgmp3这里以 https://v1.hitokoto.cn/ 为例，我们用浏览器访问这个链接之后，得到了如下所示的内容：不难看出，这是一段符合 JSON 语法的文本，将其展开如下：{ "id":476, "uuid":"7601bbef-a8a5-4d04-929e-05d07d0e793d", "hitokoto":"君生我未生，我生君已老。 君恨我生迟，我恨君生早。", "type":"g", "from":"全唐诗续拾", "from_who":null, "creator":"Sai", "creator_uid":0, "reviewer":0, "commit_from":"web", "created_at":"1468950842", "length":25 }https://v1.hitokoto.cn/ 是 一言 的 API 接口，随便百度搜索“一言”就能搜到，这是一个可以返回随机句子的 API，前面说过，我们只关心 API 的用法和结果，那么用浏览器访问这个链接就是一种用法，所返回的 json 对象就是结果，其中包括了句子的内容、长度、来源和其他多种属性。再具体一点，用浏览器访问可以看作是一个 get 请求，那么这就可以运用到我们的程序设计语言中，比如 python，我们只需向这个链接发送一个 get 请求，然后将返回的 json 对象进行解析，就能在程序运行时得到随机句子。用 python 发 get 请求并解析 json 对象我们要使用的随机涩图 API 的接口链接是 https://api.lolicon.app/setu/v2，跟前面一样，直接用浏览器访问就能看到效果，返回的也是一个 json 对象，我们先用 python 进行第一步处理:import requests response = requests.get("https://api.lolicon.app/setu/v2") datas = response.json() print(datas)在上述代码中，我们使用了 python 的 requests 库，如果没有的话可以使用命令行 pip install requests 进行安装。库如其名，这个库的主要功能就是发送 HTTP 请求和接收其响应，requests.get()函数传入一个链接，响起发送一个 get 请求，然后返回一个 response 对象。我们用浏览器访问时已经知道返回的是一个 json 格式的文本，于是可以调用 json() 函数将其解析，该函数返回一个 python 字典，可以直接用 print() 看到其内容。这里就不得不吐槽 python 的机制了，字典里可以套列表，列表里面又能套字典，print 又不分行，里面那么多个东西，一眼看过去是很难知道怎么用下标访问的，可以使用数括号的方法，也可以格式化上图中的字典：{ 'error': '', 'data': [{ 'pid': 85163311, 'p': 0, 'uid': 9009855, 'title': '…

【五仁python】用简单代码压缩图片
type: item
date: 2022-03-04T18:23:00+08:00
description: 这是我在写博客时遇到的问题，网页加载图片的速度并不快，想要网页显示图片又不拖慢速度的话，就要限制图片的大小。我们平时为了追求高质量图片，对于自己一见钟情的图片，通常都会选择保存原图，而原图大小几乎都是以 Mb 为单位的，肯定是不能直接放到网页上的，否则就会加载非常非常非常久。于是我百度了一下“压缩图片”，出来的全是广告，又要下载这个那个的，还不如自己写个程序，于是就有了这篇文章。不过，本篇只讨论函数接口的调用，并不打算详细研究其实现，也不深入探究各种图像压缩算法，也就是说，只讲使用，不讲原理。使用 python 的原因很简单：环境配置容易，代码简洁，有成熟的函数直接供我们使用。环境准备（安装python包）我们将会用到两个库，一个是 opencv，这个是著名的计算机视觉库了，另一个是 pillow，也就是 PIL(Python Image Library)，这个也是经典的 python 图像处理库。执行下面两句命令行，利用 pip 工具安装两个工具库：pip install opencv-python==3.4.17.61pip install pillow压缩分辨率以 .jpg 图片为例，图片文件的大小由很多因素决定，其中比较重要的有两个，一个是图像的分辨率，另一个是图像的质量。图像的分辨率决定了像素的总量，而图像的质量往往跟色彩有关。我们可以从这两方面去压缩图片的大小，下面先介绍如何改分辨率，先上代码：import cv2 img = cv2.imread("img.jpg") # 读取原图 print(img.shape) # 查看原图分辨率 new_img = cv2.resize(img, (1080, 1920)) # 修改图片分辨率 print(new_img.shape) # 查看修改后的图片分辨率 cv2.imwrite("new.jpg", new_img) # 保存修改后的图片上面的代码非常简单，只有三个步骤：读取原图→改分辨率→保存新图。python 中的 opencv 在使用时叫 cv2，所以在使用 opencv 的函数前，要先写一句 import cv2 导入 opencv 库。imread 函数用于读取图片，传入的是图片文件名（后缀名记得写对），返回一个多维数组，其中存储了图片的像素值。需要注意的是，我们平时描述图片的分辨率习惯用行数×列数的形式，比如 1920×1080、1280×720 之类的都是行×列的形式，但是 opencv 读取图片后的多维数组是列数×行数的形式，所以，在调用 resize 函数来修改分辨率时，我们要小心行数和列数的位置，不要写错。resize 函数的第一个传入参数是原图，第二个参数是一个元组，以（列，行）的形式表示新图的分辨率，返回新图的多维数组。imwrite 函数用于写入图片，第一个参数是文件名，第二个参数是多维数组，其中保存了图片的像素值。上面的代码执行完毕后，会把 img.jpg 修改成 1920×1080 的分辨率然后保存在 new.jpg 中。但是这显然有一个问题，因为 1920×1080 的图片的横纵比是 16:9，但是并不是所有的图片都是 16:9 ，如果每张图片都按这样的方法压缩的话，就会改变图片的横纵比，使图片变形。想要保持图片的横纵比的话，我们可以等比例压缩，使得新图的横纵比跟原图保持一致。实现等比例压缩有两种方法，一是我们自己手动算出最终的分辨率，然后代入上面的代码，比如上面的 resize 函数调用改成 new_img = cv2.resize(img, (img.shape[0]*0.5, img.shape[1]*0.5))，这样就把图片的行数和列数都变成原来的一半。前面所说的方法，都是以固定分辨率的形式进行传参，而 resize 函数有更直接的方式让我们按比例改变图片分辨率，我们将代码修改为下面的形式：import cv2 img = cv2.imread("img.jpg") # 读取原图 print(img.shape) # 查看原图分辨率 rate = 0.5 # 修改分辨率的比例 new_img = cv2.resize(img, (0, 0), fx = rate, fy = rate) # 修改图片分辨率 print(new_img.shape) # 查看修改后的图片分辨率 cv2.imwrite("new.jpg", new_img) # 保存修改后的图片可以看出，resize 函数允许我们用 fx 参数和 fy 参数来用百分比的形式修改图片的分辨率，这两个参数会分别乘上原图的行数和列数来得到新图的分辨率，当给 fx 和 fy 赋值之后，前面的第二项参数就失效了，所以可以直接用 (0,0) 来传参。压缩图片质量jpg 图片是…

【五仁C++】静态库？动态库？.a.so.lib.dll的生成与使用(g++版)
type: item
date: 2022-03-03T19:54:00+08:00
description: 如果你的计算机用的是 windows 系统，那么库文件将遍布电脑的每一个角落。在 windows 上安装的任何一款软件，其中都一定包含有大量的 .dll 文件（下图为原神目录的部分截图）。.dll 文件称为动态链接库，库包含了写好的，现有的，成熟的，可以复用的代码。作为程序员，需要懂得什么是库，如何生成和使用库，下面让我来一一讲解。g++编译基础也许你用过若干种 C++ 编译器，比如 Dev-C++，Visual Studio，这些编译器可以帮我们把写好的代码 .cpp 编译成可执行文件 .exe ，但事实上编译器只是帮我们输入了一句或多句命令，而这些命令就是我们理解文件编译和执行的关键。我们将一句命令称为命令行，命令行可以在很多地方上执行，比如命令提示符cmd，比如VS-code的终端。如果想要把自己写好的 main.cpp 文件用命令行编译的话，只需要输入下列语句就可以了（记得切换到 main.cpp 的目录）:g++ main.cpp -o main执行上面这条语句之后，就会生成 main.exe 文件，这跟点击 Dev-C++ 的“编译”按钮是一样的。对于这条语句来说，g++ 表示执行 g++.exe 文件，它是用来编译 c++ 文件的；main.cpp 是要被编译的文件；-o 是一个选项，它可以指定生成的文件的名字，后面跟着的 main 就是指定的名字，g++ 会自动补全后面的 .exe，所以最后生成 main.exe，当然写 -o main.exe 也没问题。想要执行 main.exe ，既可以双击执行，也可以用命令行 ./main 执行。不过，相信大家多多少少有听过程序编译的一些规范和方法。从代码文件 .cpp 到可执行文件 .exe 其实会经过多个步骤，也会产生多个中间文件。我们重点考虑两个大步骤——编译和链接。无论是C、C++，首先要把源文件编译成中间代码文件，因为 cpp 文件里面是字符，人能看懂，计算机可看不懂，首先要生成一个对应该 cpp 的能让计算机看得懂的二进制文件，即 Object File ，这个动作叫做编译（compile），然后再把大量的 Object File 合成可执行文件，这个动作叫作链接（link）。 于是，上面的g++ main.cpp -o main就可以拆成两句命令：g++ -c main.cpp和g++ main.o -o main这两句命令分别对应了编译和链接，前者的-c选项就是让g++生成中间代码文件 main.o，后者则是把多个 .o 文件链接起来，这里只有一个 main.o，下文的介绍将会出现 3 个 .o 文件。上面这两句命令是本文的核心，全文都会围绕这两句命令进行扩展。后缀的解释正常来说，我们将会接触到 6 种文件的后缀：.o，.a，.so，.obj，.lib，.dll，但其实只有 3 种文件，分别是中间代码文件、静态库文件、动态库文件，只不过这 3 种文件在 windows 系统和 linux 系统上的后缀不一样而已，在 windows 系统上可以兼容所有的 6 种后缀格式，但 linux 系统上只能用 3 种，这 6 种后缀的解释如下：|windows|linux|文件类型||:-:|:-:|:-:||.obj|.o|中间代码文件||.lib|.a|静态库文件||.dll|.so|动态库文件|简述静态库和动态库的异同静态库和动态库都是“库”，库指的是多个中间代码文件的集合，也就是说，一个库包含多个 .o 或 .obj 文件，这些 .o 文件里面有可执行的代码。静态库和动态库的区别在于链接步骤，静态库会在链接时全身心地参与构成可执行文件，链接静态库其实从某种意义上来说也是一种粘贴复制，因为静态库被链接后库就直接嵌入可执行文件中了，整个库的所有代码都会共同构成可执行文件 .exe，假如使用 1 个库生成了 100 个可执行文件，那么就会产生这个库的 100 个副本，这显然非常浪费空间，并且难以修改：一旦发现这个库中的某段代码有 bug，那么那 100 个副本都要修改，即重新生成所有的文件，非常麻烦。而动态库是在程序运行时被链接的，而只在可执行文件中留下接口的信息（比如函数名），当可执行文件调用接口时，会执行动态库中的代码。这就好像超人给每个人留下了一张名片，上面有电话号码，当有人需要超人的帮助时，就拨打电话呼叫超人，这样既能让每个人都能随时得到超人的帮助，也不需要复制很多个超人，而大家只需要在自己家里放留一张名片，而不是留一个超人，非常的节省空间。静态库和动态库各有优劣，复制多个副本不见得是坏事，比如你给不懂计算机的同学写一个很简单的程序，那使用静态库的话就可以只发一份可执行文件 .exe 到他电脑上，但如果使用动态库的话还需要把相关的动态库文件发过去，这是其中一…