2024年11月

这两个滤波器也是很久前就看过的,最近偶然翻起那本比较经典的matlab数字图像处理(冈萨雷斯)书,里面也提到了这个算法,觉得效果还行,于是也还是稍微整理下。

为了自己随时翻资料舒服和省事省时,这个算法的原理我们还是把他从别人的博客里搬过来吧:

摘自:
图像处理基础(2):自适应中值滤波器(基于OpenCV实现)

自适应的中值滤波器也需要一个矩形的窗口S
xy
,和常规中值滤波器不同的是这个窗口的大小会在滤波处理的过程中进行改变(增大)。需要注意的是,滤波器的输出是一个像素值,该值用来替换点(x,y)处的像素值,点(x,y)是滤波窗口的中心位置。

原理说明

过程
A的目的是确定当前窗口内得到中值
Zmed是否是噪声。如果Z
min
<Z
med
<Z
max
,则中值Z
med
不是噪声,这时转到过程
B测试,当前窗口的中心位置的像素
Zxy是否是一个噪声点。如果Z
min
<Z
xy
<Z
max
,则Z
xy
不是一个噪声,此时滤波器输出Z
xy
;如果不满足上述条件,则可判定Zxy是噪声,这是输出中值Z
med
(在
A中已经判断出
Z
med
不是噪声)。

如果在过程
A中,得到则
Z
med
不符合条件Z
min
<Z
med
<Z
max
,则可判断得到的中值Z
med
是一个噪声。在这种情况下,需要增大滤波器的窗口尺寸,在一个更大的范围内寻找一个非噪声点的中值,直到找到一个非噪声的中值,跳转到
B;或者,窗口的尺寸达到了最大值,这时返回找到的中值,退出。

从上面分析可知,噪声出现的概率较低,自适应中值滤波器可以较快的得出结果,不需要去增加窗口的尺寸;反之,噪声的出现的概率较高,则需要增大滤波器的窗口尺寸,这也符合种中值滤波器的特点:噪声点比较多时,需要更大的滤波器窗口尺寸。

摘抄完成..............................................

这个过程理解起来也不是很困难,
图像处理基础(2):自适应中值滤波器(基于OpenCV实现)
这个博客也给出了参考代码,不过我很遗憾的告诉大家,这个博客的效果虽然可以,但是编码和很多其他的博客一样,是存在问题的。

核心在这里:

for (int j = maxSize / 2; j < im1.rows - maxSize / 2; j++)
{
for (int i = maxSize / 2; i < im1.cols * im1.channels() - maxSize / 2; i++)
{
im1.at
<uchar>(j, i) =adaptiveProcess(im1, j, i, minSize, maxSize);
}
}

他这里的就求值始终是对同一份图,这样后续的处理时涉及到的领域实际上前面的部分已经是被修改的了,不符合真正的原理的。 至于为什么最后的结果还比较合适,那是因为这里的领域相关性不是特别强。

我这里借助于最大值和最小值滤波以及中值滤波,一个简单的实现如下所示:

/// <summary>
///实现图像的自使用中值模糊。更新时间2015.3.11。///参考:Adaptive Median Filtering Seminar Report By: PENG Lei (ID: 03090345)/// </summary>
/// <param name="Src">需要处理的源图像的数据结构。</param>
/// <param name="Dest">保存处理后的图像的数据结构。</param>
/// <param name="Radius">滤波的半径,有效范围[1,127]。</param>
/// <remarks>1: 能处理8位灰度和24位及32位图像。</remarks>
/// <remarks>2: 半径大于10以后基本没区别了。</remarks>
int IM_AdaptiveMedianFilter(unsigned char* Src, unsigned char* Dest, int Width, int Height, int Stride, int MinRadius, intMaxRadius)
{
int Channel = Stride /Width;if ((Src == NULL) || (Dest == NULL)) returnIM_STATUS_NULLREFRENCE;if ((Width <= 0) || (Height <= 0)) returnIM_STATUS_INVALIDPARAMETER;if ((MinRadius <= 0) || (MaxRadius <= 0)) returnIM_STATUS_INVALIDPARAMETER;if ((Channel != 1) && (Channel != 3)) returnIM_STATUS_NOTSUPPORTED;int Status =IM_STATUS_OK;int Threshold = 10;if (MinRadius >MaxRadius) IM_Swap(MinRadius, MaxRadius);bool AllProcessed = false;
unsigned
char* MinValue = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
unsigned
char* MaxValue = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
unsigned
char* MedianValue = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
unsigned
char* Flag = (unsigned char*)malloc(Height * Width * sizeof(unsigned char));if ((MinValue == NULL) || (MaxValue == NULL) || (MedianValue == NULL) || (Flag ==NULL))
{
Status
=IM_STATUS_OUTOFMEMORY;gotoFreeMemory;
}
memset(Flag,
0, Height * Width * sizeof(unsigned char));if (Channel == 1)
{
//The median filter starts at size 3-by-3 and iterates up to size MaxRadius-by-MaxRadius
for (int Z = MinRadius; Z <= MaxRadius; Z++)
{
Status
=IM_MinFilter(Src, MinValue, Width, Height, Stride, Z);if (Status != IM_STATUS_OK) gotoFreeMemory;
Status
=IM_MaxFilter(Src, MaxValue, Width, Height, Stride, Z);if (Status != IM_STATUS_OK) gotoFreeMemory;
Status
= IM_MedianBlur(Src, MedianValue, Width, Height, Stride, Z, 50);if (Status != IM_STATUS_OK) gotoFreeMemory;for (int Y = 0; Y < Height; Y++)
{
int Index = Y *Stride;int Pos = Y *Width;for (int X = 0; X < Width; X++, Index++, Pos++)
{
if (Flag[Pos] == 0)
{
int Min = MinValue[Index], Max = MaxValue[Index], Median =MedianValue[Index];if ((Median > Min + Threshold) && (Median < Max -Threshold))
{
int Value =Src[Index];if ((Value > Min + Threshold) && (Value < Max-Threshold))
{
Dest[Index]
=Value;
}
else{
Dest[Index]
=Median;
}
Flag[Pos]
= 1;
}
}
}
}
AllProcessed
= true;for (int Y = 0; Y < Height; Y++)
{
int Pos = Y *Width;for (int X = 0; X < Width; X++)
{
if (Flag[Pos + X] == 0) //检测是否每个点都已经处理好了
{
AllProcessed
= false;break;
}
}
if (AllProcessed == false) break;
}
if (AllProcessed == true) break;
}
/*Output zmed for any remaining unprocessed pixels. Note that this
zmed was computed using a window of size Smax-by-Smax, which is
the final value of k in the loop.
*/

if (AllProcessed == false)
{
for (int Y = 0; Y < Height; Y++)
{
int Index = Y *Stride;int Pos = Y *Width;for (int X = 0; X < Width; X++)
{
if (Flag[Pos + X] == 0) Dest[Index + X] = Src[Index +X];
}
}
}
}
else{

}
FreeMemory:
if (MinValue !=NULL) free(MinValue);if (MaxValue !=NULL) free(MaxValue);if (MedianValue !=NULL) free(MedianValue);if (Flag !=NULL) free(Flag);returnStatus;
}


注意这里,我们还做了适当的修改,增加了一个控制阈值Threshold,把原先的
if ((Median > Min) && (Median < Max))

修改为:


if ((Median > Min + Threshold) && (Median < Max - Threshold))


也可以说是对应用场景的一种扩展,增加了函数的韧性。


当我们的噪音比较少时,这个函数会很快收敛,也就是不需要进行多次的计算的。


另外还有一个算法叫Conservative Smoothing,翻译成中文可以称之为保守滤波,这个算法在
https://homepages.inf.ed.ac.uk/rbf/HIPR2/csmooth.htm
有较为详细的说明,其基本原理是:

 This is accomplished by a procedure which first finds the minimum and maximum intensity values of all the pixels within a windowed region around the pixel in question. If the intensity of the central pixel lies within the intensity range
spread of its neighbors, it is passed on to the output image unchanged. However, if the central pixel intensity is greater than the maximum value, it is set equal to the maximum value; if the central pixel intensity is less than the minimum value,
it is set equal to the minimum value. Figure 1 illustrates this idea.


  

比如上图的3*3领域,除去中心点之外的8个点其最大值为127,最小值是115,而中心点的值150大于这个最大值,所以中心点的值会被修改为127。

注意这里和前面的自适应中值滤波有一些不同。

1、虽然他也利用到了领域的最大值和最小值,但是这个领域是不包含中心像素本身的,这个和自适应中值是最大的区别。

2、这个算法在满足某个条件时,不是用中值代替原始像素,而是用前面的改造的最大值或者最小值。

3、这个算法也可以改造成和自适应中值一样,不断的扩大半径。

4、对于同一个半径,这个函数多次迭代效果不会有区别。

一个简单的实现如下:

    for (int X = 0; X < Width * Channel; X++, LinePD++)
{
int P0 = First[X], P1 = First[X + Channel], P2 = First[X + 2 *Channel];int P3 = Second[X], P4 = Second[X + Channel], P5 = Second[X + 2 *Channel];int P6 = Third[X], P7 = Third[X + Channel], P8 = Third[X + 2 *Channel];int Max0 =IM_Max(P0, P1);int Min0 =IM_Min(P0, P1);int Max1 =IM_Max(P2, P3);int Min1 =IM_Min(P2, P3);int Max2 =IM_Max(P5, P6);int Min2 =IM_Min(P5, P6);int Max3 =IM_Max(P7, P8);int Min3 =IM_Min(P7, P8);int Max =IM_Max(IM_Max(Max0, Max1), IM_Max(Max2, Max3));int Min =IM_Min(IM_Min(Min0, Min1), IM_Min(Min2, Min3));if (P4 >Max)
P4
=Max;else if (P4 <Min)
P4
=Min;
LinePD[
0] =P4;
}

因为去除了中心点后进行的最大值和最小计算,这个算法如果要实现高效率的和半径无关的版本,还是需要做一番研究的,目前我尚没有做出成果。

我们拿标准的Lena图测试,使用matlab分别为他增加0.02及0.2范围的椒盐噪音,然后使用自适应中值滤波及保守滤波进行处理。



添加0.02概率的椒盐噪音图                         最小半径1,最大半径13时的去噪效果                          3*3的保守滤波



添加0.2概率的椒盐噪音图                              最小半径1,最大半径5时的去噪效果                                3*3的保守滤波

可以看到,自适应中值滤波在去除椒盐噪音的效果简直就是逆天,基本完美的复现了原图,有的时候我自己都不敢相信这个结果。而保守滤波由于目前我只实现了3*3的版本,因此对于噪音比较集中的地方暂时无法去除,这个可以通过扩大半径和类似使用自适应中值的方式处理,而这种噪音的图像使用DCT去噪、小波去噪、NLM等等都不具有很好的效果,唯一能和他比拟的就只有蒙版和划痕里面小参数时的效果(这也是以中值为基础的)。所以去除椒盐噪音还是得靠中值相关的算法啊。

和常规的中值滤波器相比,自适应中值滤波器能够更好的保护图像中的边缘细节部分,当然代价就是增加了算法的时间复杂度。

书接上回,我们继续来聊聊图的遍历与实现。

01
、遍历

在图的基本功能中有个很重要的功能——遍历,遍历顾名思义就是把图上所有的点都访问一遍,具体来说就是从一个连通的图中某一个点出发,沿着边访问所有的点,并且每个点只能访问一遍。

下面我们介绍两种常见的遍历方式:深度优先遍历(DFS)和广度优先遍历(BFS)。

1、深度优先遍历

如果我们把边当作路,深度优先遍历就是路一直往下走,直到没路了再返回走其他路。其实优点像树的先序遍历从根节点沿着子节点一直向下直到叶子节点再调头。

下面我们梳理一下深度优先遍历大致分为以下几个步骤:

(1)从图中任意一个点A出发,并访问点;

(2)找出点A的第一个未被访问的邻接点,并访问该点;

(3)以该点为新的点,重复步骤(2),直至新的邻接点没有未被访问的邻接点;

(4)返回前一个点并依次访问前一个点为未被访问的其他邻接点,并访问该点;

(5)重复步骤(3)和(4),直至所有点都被访问过;

如上图演示了从点A出发进行深度优先遍历过程,其中红色虚线表示前进路线,蓝色虚线表示回退路线。最后输出:A->B->E->F->C->G->D。

2、广度优先遍历

如果说深度优先遍历是找到一条路一直走到底,那么广度优先遍历就是先把所有的路走一步再说。其实优点像树的层次遍历从根节点出发先遍历其子节点然后再遍历其孙子节点直至遍历完所有节点。

下面我们梳理一下广度优先遍历大致分为以下几个步骤:

(1)从图中任意一点A出发,并访问点A;

(2)依次访问点A所有未被访问的邻接点,访问完邻接点后,然后按邻接点顺序把邻接点作为新的出发执行步骤(1);

(3)重复步骤(1)和(2)直至所有点都被访问到。

如上图演示了从点A出发进行广度优先遍历过程,其中红色虚线表示前进路线。最后输出:A->B->C->D->E->F->G。

02
、实现(邻接矩阵)

下面我们就以邻接矩阵的存储方式实现一个无向图。

1、定义

根据图的定义,我们需要定义点集合、边集合两个私有变量用于存储核心数据,为了操作访问我们再定义点数量和边数量两个私有变量,代码如下:

//点集合
private T[] _vertexArray { get; set; }
//边集合
private int[,] _edgeArray { get; set; }
//点数量
private int _vertexCount;
//边数量
private int _edgeCount { get; set; }

2、初始化 Init

此方法主要是初始化上面定义的私有变量,同时确定点集合大小,具体代码如下:

//初始化
public MyselfGraphArray<T> Init(int length)
{
    //初始化指定长度点集合
    _vertexArray = new T[length];
    //初始化指定长度边集合
    _edgeArray = new int[length, length];
    //初始化点数量
    _vertexCount = 0;
    //初始化边数量
    _edgeCount = 0;
    return this;
}

3、获取点数量 VertexCount

我们可以通过点数量私有变量快速获取图的点数量,代码如下:

//返回点数量
public int VertexCount
{
    get
    {
        return _vertexCount;
    }
}

4、获取边数量 EdgeCount

我们可以通过边数量私有变量快速获取图的点数量,代码如下:

//返回边数量
public int EdgeCount
{
    get
    {
        return _edgeCount;
    }
}

5、获取点索引 GetVertexIndex

该方法是通过点元素获取其索引值,具体代码如下:

//返回指定点元素的索引   
public int GetVertexIndex(T vertex)
{
    if (vertex == null)
    {
        return -1;
    }
    //根据值查找索引
    return Array.IndexOf(_vertexArray, vertex);
}

6、获取点元素 GetVertexByIndex

该方法通过点索引获取点元素,具体代码如下:

//返回指定点索引的元素
public T GetVertexByIndex(int index)
{
    //如果索引非法则报错
    if (index < 0 || index > _vertexArray.Length - 1)
    {
        throw new InvalidOperationException("索引错误");
    }
    return _vertexArray[index];
}

7、插入点 InsertVertex

插入点元素时,我们需要先通过点元素获取其索引,如果索引已存在或者点集合已经满了则直接返回,否则添加点元素同时更新点数量,具体代码如下:

//插入点
public void InsertVertex(T vertex)
{
    //获取点索引
    var index = GetVertexIndex(vertex);
    //如果索引大于-1说明点已存在,则直接返回
    if (index > -1)
    {
        return;
    }
    //如果点集合已满,则直接返回
    if (_vertexCount == _vertexArray.Length)
    {
        return;
    }
    //添加点元素,并且更新点数量
    _vertexArray[_vertexCount++] = vertex;
}

8、插入边 InsertEdge

插入边时可以同时指定边的权值。我们首先需要把两个点元素转换为点索引,同时验证索引,验证不通过则直接返回。否则开始添加边,因为无向图的特性,所以需要添加两点索引相反的边。同时更新边数量,具体代码如下:

//插入边
public void InsertEdge(T vertex1, T vertex2, int weight)
{
    //根据点元素获取点索引
    var vertexIndex1 = GetVertexIndex(vertex1);
    //如果索引等于-1说明点不存在,则直接返回
    if (vertexIndex1 == -1)
    {
        return;
    }
    //根据点元素获取点索引
    var vertexIndex2 = GetVertexIndex(vertex2);
    //如果索引等于-1说明点不存在,则直接返回
    if (vertexIndex2 == -1)
    {
        return;
    }
    //更新两点关系,即边信息
    _edgeArray[vertexIndex1, vertexIndex2] = weight;
    //用于无向图,对于有向图则删除此句子
    _edgeArray[vertexIndex2, vertexIndex1] = weight;
    //更新边数量
    _edgeCount++;
}

9、获取边权值 GetWeight

该方法可以获取边的权值,权值可以根据需要在插入边方法中设置,需要对输入的点进行验证,如果点不存在则报错,具体代码如下:

//返回两点之间边的权值
public int GetWeight(T vertex1, T vertex2)
{
    //根据点元素获取点索引
    var vertexIndex1 = GetVertexIndex(vertex1);
    //如果索引等于-1说明点不存在
    if (vertexIndex1 == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    //根据点元素获取点索引
    var vertexIndex2 = GetVertexIndex(vertex2);
    //如果索引等于-1说明点不存在
    if (vertexIndex2 == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    return _edgeArray[vertexIndex1, vertexIndex2];
}

10、深度优先遍历 DFS

深度优先遍历正常有两种实现方法,一种是使用递归调用,一种是使用栈结构实现,下面我们使用递归的方式来实现。

因为我们需要保证每个点只会被访问一次,因此需要定义一个数组用来记录元素已经被访问过。我们这里是以无向图为例,因为无向图的对称性,索引我们选用一维数组即可满足记录被访问元素,而如果是有向图我们则需要使用二维数组记录被访问元素。

具体代码如下:

//深度优先遍历
public void DFS(T startVertex)
{
    //根据点元素获取点索引
    var startVertexIndex = GetVertexIndex(startVertex);
    //如果索引等于-1说明点不存在
    if (startVertexIndex == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    //定义已访问标记数组
    //因为无向图对称特性因此一维数组即可
    //如果是有向图则需要定义二维数组
    var visited = new bool[_vertexCount];
    DFSUtil(startVertexIndex, visited);
    Console.WriteLine();
}
//深度优先遍历
private void DFSUtil(int index, bool[] visited)
{
    //标记当前元素已访问过
    visited[index] = true;
    //打印点
    Console.Write(_vertexArray[index] + " ");
    //遍历查找与当前元素相邻的元素
    for (var i = 0; i < _vertexCount; i++)
    {
        //如果是相邻的元素,并且元素未被访问过
        if (_edgeArray[index, i] == 1 && !visited[i])
        {
            //则递归调用自身方法
            DFSUtil(i, visited);
        }
    }
}

11、广度优先遍历 BFS

广度优先遍历可以借助队列来实现。首先把起始点添加入队列,然后把点出队列,同时把该点的所有邻接点添加入队列,循环往复,一直到把所有元素处理完为止。

//广度优先遍历
public void BFS(T startVertex)
{
    //根据点元素获取点索引
    var startVertexIndex = GetVertexIndex(startVertex);
    //如果索引等于-1说明点不存在
    if (startVertexIndex == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    //定义已访问标记数组
    //因为无向图对称特性因此一维数组即可
    //如果是有向图则需要定义二维数组
    var visited = new bool[_vertexCount];
    //使用队列实现广度优先遍历
    var queue = new Queue<int>();
    //将起点入队
    queue.Enqueue(startVertexIndex);
    //标记起点为已访问
    visited[startVertexIndex] = true;
    //遍历队列
    while (queue.Count > 0)
    {
        //出队点
        var vertexIndex = queue.Dequeue();
        //打印点
        Console.Write(_vertexArray[vertexIndex] + " ");
        //遍历查找与当前元素相邻的元素
        for (var i = 0; i < _vertexCount; i++)
        {
            //如果是相邻的元素,并且元素未被访问过
            if (_edgeArray[vertexIndex, i] == 1 && !visited[i])
            {
                //则将相邻元素索引入队
                queue.Enqueue(i);
                //并标记为已访问
                visited[i] = true;
            }
        }
    }
    Console.WriteLine();
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

为了构建生成式AI应用,需要完成两个部分:

  • AI大模型服务:有两种方式实现,可以使用大厂的API,也可以自己部署,本文将采用ollama来构建
  • 应用构建:调用AI大模型的能力实现业务逻辑,本文将采用Spring Boot + Spring AI来实现

Ollama安装与使用

进入官网:
https://ollama.com/
,下载、安装、启动 ollama

具体步骤可以参考我之前的这篇文章:
手把手教你本地运行Meta最新大模型:Llama3.1

构建 Spring 应用

  1. 通过
    spring initializr
    创建Spring Boot应用

  2. 注意右侧选择Spring Web和Spring AI对Ollama的支持依赖

  1. 点击“generate”按钮获取工程

  2. 使用IDEA或者任何你喜欢的工具打开该工程,工程结构如下;

  1. 写个单元测试,尝试在Spring Boot应用里调用本地的ollama服务
@SpringBootTest(classes = DemoApplication.class)
class DemoApplicationTests {

    @Autowired
    private OllamaChatModel chatModel;

    @Test
    void ollamaChat() {
        ChatResponse response = chatModel.call(
                new Prompt(
                        "Spring Boot适合做什么?",
                        OllamaOptions.builder()
                                .withModel(OllamaModel.LLAMA3_1)
                                .withTemperature(0.4)
                                .build()
                ));
        System.out.println(response);
    }

}

运行得到如下输出:

ChatResponse [metadata={ id: , usage: { promptTokens: 17, generationTokens: 275, totalTokens: 292 }, rateLimit: org.springframework.ai.chat.metadata.EmptyRateLimit@7b3feb26 }, generations=[Generation[assistantMessage=AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=Spring Boot是一个基于Java的快速开发框架,主要用于创建独立的、生产级别的应用程序。它提供了一个简化的配置过程,使得开发者能够快速构建和部署Web应用程序。

Spring Boot适合做以下几件事情:

1. **快速开发**: Spring Boot提供了一系列的自动配置功能,可以帮助开发者快速创建基本的应用程序,减少手动编写配置代码的时间。
2. **独立运行**: Spring Boot可以作为一个独立的应用程序运行,不需要额外的容器或服务器支持。
3. **生产级别的应用程序**: Spring Boot提供了许多生产级别的特性,例如安全、监控和部署等功能,可以帮助开发者创建高性能、可靠的应用程序。
4. **Web 应用程序**: Spring Boot可以用于创建Web应用程序,包括RESTful API、WebSockets和其他类型的Web应用程序。
5. **微服务架构**: Spring Boot支持微服务架构,允许开发者将一个大型应用程序分解成多个小型服务,每个服务都可以独立运行和部署。

总之,Spring Boot是一个强大的框架,可以帮助开发者快速创建、测试和部署生产级别的应用程序。, metadata={messageType=ASSISTANT}], chatGenerationMetadata=ChatGenerationMetadata{finishReason=stop,contentFilterMetadata=null}]]]

上述样例工程打包放公众号了,如果需要的话,关注"程序猿DD",发送关键词
spring+ollama
获得下载链接。

小结

通过本文的介绍,我们就已经完成了Spring Boot应用与Ollama运行的AI模型之间的对接。剩下的就是与业务逻辑的结合实现,这里读者根据自己的需要去实现即可。

可能存在的一些疑问

  1. 如何使用其他AI模型

通过ollama的
Models
页面,可以找到各种其他模型:

选择你要使用的模型来启动即可。

  1. 如何植入现有应用?

打开上面工程的
pom.xml
,可以看到主要就下面两个依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

所以,如果要在现有工程引入的话只要引入
spring-ai-ollama-spring-boot-starter
依赖就可以了。

好了,今天的分享就到这里。最近较忙,分享较少,感谢持续的关注与支持
_

如果您学习过程中如遇困难?可以加入我们超高质量的
技术交流群
,参与交流与讨论,更好的学习与进步!

一、关于条件构造器(Wrapper)

1.1 简介

MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。

edae4c45-b7c2-4e1c-a975-ff823dacb29c

1.2 发展

  1. 核心功能的发展


    • 从早期的MyBatis-Plus版本开始,条件构造器(Wrapper)就已经作为核心功能之一,用于构建复杂的数据库查询条件。随着版本的迭代,条件构造器的功能不断增强,提供了更多的方法来支持各种查询操作,如
      eq
      ​(等于)、
      ne
      ​(不等于)、
      gt
      ​(大于)、
      lt
      ​(小于)等。
  2. 链式调用的优化


    • 条件构造器支持链式调用,这使得代码更加简洁和易读。随着MyBatis-Plus的发展,链式调用的流畅性和易用性得到了进一步的优化,使得开发者可以更加方便地构建复杂的查询条件。
  3. Lambda表达式的引入


    • 随着Java 8的普及,MyBatis-Plus引入了基于Lambda表达式的条件构造器,如
      LambdaQueryWrapper
      ​和
      LambdaUpdateWrapper
      ​,这使得开发者可以使用更加现代的编程方式来构建查询和更新条件,提高了代码的可读性和安全性。
  4. 功能扩展


    • MyBatis-Plus条件构造器的功能不断扩展,新增了许多方法,如
      eqSql
      ​、
      gtSql
      ​、
      geSql
      ​、
      ltSql
      ​、
      leSql
      ​等,这些方法允许开发者直接在条件构造器中嵌入SQL片段,提供了更高的灵活性。
  5. 性能优化


    • 随着MyBatis-Plus的发展,条件构造器在性能上也得到了优化。通过减少不必要的SQL拼接和优化条件构造逻辑,提高了查询的效率。
  6. 易用性的提升


    • MyBatis-Plus不断改进条件构造器的易用性,例如通过提供更多的方法来支持不同的查询场景,如
      groupBy
      ​、
      orderBy
      ​、
      having
      ​等,使得开发者可以更加方便地构建复杂的查询条件。

1.3 特点

MyBatis-Plus的条件构造器具有以下特点:

  1. 链式调用
    :Wrapper类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的SQL语句,从而提高开发效率。
  2. 安全性
    :通过使用Wrapper,可以避免直接拼接SQL片段,减少SQL注入的风险。
  3. 灵活性
    :支持多种查询操作,如等于、不等于、大于、小于等,以及逻辑组合如
    and
    ​和
    or
    ​。
  4. Lambda表达式
    :LambdaQueryWrapper和LambdaUpdateWrapper通过Lambda表达式引用实体类的属性,避免了硬编码字段名,提高了代码的可读性和可维护性。
  5. 减少代码量
    :Wrappers类作为一个静态工厂类,可以快速创建Wrapper实例,减少代码量,提高开发效率。
  6. 线程安全性
    :Wrapper实例不是线程安全的,建议每次使用时创建新的Wrapper实例,以避免多线程环境下的数据竞争和潜在错误。
  7. 支持复杂查询
    :支持嵌套查询和自定义SQL片段,通过
    nested
    ​和
    apply
    ​方法,可以构建更复杂的查询条件。
  8. 类型处理器
    :在Wrapper中可以使用TypeHandler处理特殊的数据类型,增强了对数据库类型的支持。
  9. 更新操作简化
    :使用UpdateWrapper或LambdaUpdateWrapper时,可以省略实体对象,直接在Wrapper中设置更新字段。

1.4 主要类型

MyBatis-Plus 提供了多种条件构造器,以满足不同的查询需求:

  1. QueryWrapper<T>
    :用于构建查询条件,支持链式调用,可以非常方便地添加各种查询条件。
  2. UpdateWrapper<T>
    :用于构建更新条件,支持链式调用,可以方便地添加各种更新条件。
  3. LambdaQueryWrapper<T>
    :使用 Lambda 表达式来构建查询条件,避免了字段名错误的问题,增强了代码的可读性和健壮性。
  4. LambdaUpdateWrapper<T>
    :使用 Lambda 表达式来构建更新条件,同样可以避免字段名错误的问题。
  5. AbstractWrapper<T>
    :是一个抽象类,其他 Wrapper 类继承自这个类,提供了一些基础的方法实现。

1e12f7c4-aa70-4024-bbbc-f820a4772f8c

二、基本运用

2.1
使用方法

条件构造器允许开发者以链式调用的方式构造SQL的WHERE子句,提供了极大的灵活性和便利性。例如,使用QueryWrapper可以这样构建查询条件:

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "Kimi").lt("age", 30);

这将生成SQL:
SELECT * FROM user WHERE name = 'Kimi' AND age < 30
​。

2.2 示例

QueryWrapper 示例

// 创建 QueryWrapper 对象
QueryWrapper<User> queryWrapper = new QueryWrapper<>();

// 添加查询条件
queryWrapper.eq("name", "张三") // 字段等于某个值
            .gt("age", 18)      // 字段大于某个值
            .like("email", "%@gmail.com"); // 字段包含某个值

// 使用条件进行查询
List<User> users = userMapper.selectList(queryWrapper);

UpdateWrapper 示例

// 创建 UpdateWrapper 对象
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

// 设置更新条件
updateWrapper.eq("id", 1); // 更新 id=1 的记录

// 设置要更新的数据
User user = new User();
user.setName("李四");
user.setAge(20);

// 执行更新操作
int result = userMapper.update(user, updateWrapper);

LambdaQueryWrapper 示例

// 创建 LambdaQueryWrapper 对象
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();

// 添加查询条件
lambdaQueryWrapper.eq(User::getName, "张三")
                  .gt(User::getAge, 18)
                  .like(User::getEmail, "%@gmail.com");

// 使用条件进行查询
List<User> users = userMapper.selectList(lambdaQueryWrapper);

三、Wrapper 类

3.1 简介

在 MyBatis-Plus 中,Wrapper 类是构建查询和更新条件的核心工具。

image

image

3.2 方法

MyBatis-Plus的Wrapper类提供了一系列方法来构建复杂的数据库查询条件。以下是一些常用的Wrapper类方法汇总:

  1. 基本条件方法


    • eq
      ​:等于条件,例如
      wrapper.eq("name", "zhangsan")
      ​。
    • ne
      ​:不等于条件,例如
      wrapper.ne("name", "
      zhangsan
      ")
      ​。
    • gt
      ​:大于条件,例如
      wrapper.gt("age", 18)
      ​。
    • lt
      ​:小于条件,例如
      wrapper.lt("age", 18)
      ​。
    • ge
      ​:大于等于条件,例如
      wrapper.ge("age", 18)
      ​。
    • le
      ​:小于等于条件,例如
      wrapper.le("age", 18)
      ​。
    • between
      ​:介于两个值之间,例如
      wrapper.between("age", 18, 30)
      ​。
    • notBetween
      ​:不介于两个值之间,例如
      wrapper.notBetween("age", 18, 30)
      ​。
    • like
      ​:模糊匹配,例如
      wrapper.like("name", "%zhangsan%")
      ​。
    • notLike
      ​:不模糊匹配,例如
      wrapper.notLike("name", "%zhangsan%")
      ​。
    • likeLeft
      ​:左模糊匹配,例如
      wrapper.likeLeft("name", "zhangsan%")
      ​。
    • likeRight
      ​:右模糊匹配,例如
      wrapper.likeRight("name", "%zhangsan")
      ​。
    • isNull
      ​:字段值为null,例如
      wrapper.isNull("name")
      ​。
    • isNotNull
      ​:字段值不为null,例如
      wrapper.isNotNull("name")
      ​。
    • in
      ​:字段值在指定集合中,例如
      wrapper.in("name", "zhangsan", "Tom")
      ​。
    • notIn
      ​:字段值不在指定集合中,例如
      wrapper.notIn("name", "zhangsan", "Tom")
      ​。
  2. 逻辑组合方法


    • and
      ​:添加一个AND条件,例如
      wrapper.and(wq -> wq.eq("name", "zhangsan"))
      ​。
    • or
      ​:添加一个OR条件,例如
      wrapper.or(wq -> wq.eq("name", "zhangsan"))
      ​。
  3. SQL片段方法


    • apply
      ​:添加自定义SQL片段,例如
      wrapper.apply("name = {0}", "zhangsan")
      ​。
    • last
      ​:添加自定义SQL片段到末尾,例如
      wrapper.last("order by name")
      ​。
  4. 子查询方法


    • inSql
      ​:子查询IN条件,例如
      wrapper.inSql("name", "select name from user where age > 21")
      ​。
    • notInSql
      ​:子查询NOT IN条件,例如
      wrapper.notInSql("name", "select name from user where age > 21")
      ​。
  5. 分组与排序方法


    • groupBy
      ​:分组,例如
      wrapper.groupBy("name")
      ​。
    • orderByAsc
      ​:升序排序,例如
      wrapper.orderByAsc("age")
      ​。
    • orderByDesc
      ​:降序排序,例如
      wrapper.orderByDesc("age")
      ​。
  6. 其他方法


    • exists
      ​:存在条件,例如
      wrapper.exists("select * from user where name = {0}", "zhangsan")
      ​。

    • notExists
      ​:不存在条件,例如
      wrapper.notExists("select * from user where name = {0}", "zhangsan")
      ​。

    • set
      ​:更新操作时设置字段值,例如
      updateWrapper.set("name", "zhangsan")
      ​。

    • having(String column, Object val): HAVING 过滤条件,用于聚合后的过滤,例如

      queryWrapper.select("name", "age")
                          .groupBy("age")
                          .having("count(id) > 1");
      

以上方法提供了构建查询和更新条件的灵活性和强大功能,使得MyBatis-Plus在数据库操作方面更加高效和安全。

在 chatGPT 的推动下。LLM 简直火出天际,各行各业都在蹭。听说最近 meta 开源的 llama3 模型可以轻松在普通 PC 上运行,这让我也忍不住来蹭一层。以下是使用 ollama 试玩 llama3 的一些记录。

什么是 llama

LLaMA(Large Language Model Meta AI)是Meta开发的大规模预训练语言模型,基于Transformer架构,具有强大的自然语言处理能力。它在文本生成、问答系统、机器翻译等任务中表现出色。LLaMA模型有多个规模,从几亿到上千亿参数,适用于不同的应用场景。用户可以通过开源平台如Hugging Face获取LLaMA模型,并根据需要进行微调。LLaMA的灵活性和可扩展性使其在自然语言处理领域具有广泛的应用前景。

什么是 ollama

Ollama是一款用于本地安装和管理大规模预训练语言模型的工具。它简化了模型的下载、安装和使用流程,支持多种流行的模型如GPT-4和llama。Ollama通过易于使用的命令行界面和API,帮助用户快速部署和运行自然语言处理任务。它还支持多GPU配置和模型微调,适应各种计算资源和应用需求。总之,Ollama为研究人员和开发者提供了一个高效、灵活的本地化大模型解决方案。

下载 ollama

ollama 官网提供了各种平台的安装包,那么这里选择 windows 系统的。以下是下载地址:
https://ollama.com/download

在 windows 上安装

在 windows 上安装那简直太简单了,一路 next 就行了。

安装成功后可以在命令行下执行

ollama -v


如果能成功打印出版本信息,那么说明你安装成功了。

下载模型并运行

安装好 ollama 之后我们需要把训练好的模型拉到本地,然后才能运行它。

查找 模型

ollama 提供了一个页面供用户查询可以使用的开源模型。

https://ollama.com/search?q=&p=1

可以看到主流的开源 LLM 几乎都能找到。什么 llama3 啊,phi3 啊,国产的 qwen2 啊。让我们点击 llama3 看看详情。

里面可以选模型的参数大小。这里我们选 8b 试一下。模型大小是 4.7 GB。复制右上角的命令并在命令行运行:

ollama run llama3:8b

程序会开始下载模型到本地。这里得夸一下,ollama 是不是在国内接了 CDN,这速度杠杆的,直接跑满了我的千兆网络。

对话

下载完成后命令行就会跳转到对话模型,等待你输入问题。随便先来一个吧。
Q:飞机为什么会飞?
A: balabala 一大堆,都是英文。

Q: what is SOLID principle?
A:

总结

到这,我们本地运行大模型基本上是初步成功了。简直超级无敌简单,属于有手就行。问题就是本地限制于PC的性能,回答的速度比较慢,大概一秒2-3个单词。CPU大概吃掉50%。当然如果你有 N 卡可能会好很多。内存倒是还好才吃了300多M。好了,下一次我们来试试 open-webui,把本地的模型搞的跟 chatGPT 一样。