2024年11月

Chrome 130 版本新特性& Chrome 130 版本发行说明

一、Chrome 130 版本浏览器更新

1. 新的桌面提示

Chrome 130 引入了一种新的 Toast 样式,用于在用户操作后提供视觉确认,或快速提供后续操作的途径。

当用户执行某个操作时,Toast 会在屏幕上短暂弹出,确认操作成功或提供快捷链接。
例如,当将某项内容添加到阅读列表时,Toast 会确认项目已添加,并提供一个快速链接以打开阅读列表侧边栏。Toast 以小型提示的形式显示,部分覆盖网页内容,部分覆盖浏览器顶部工具栏。

image

2. 用于 macOS 系统的屏幕共享平台选择器

在 macOS X Sequoia 上的 Chrome 中共享屏幕时,用户现在可以使用更新后的平台选择器选择要共享的窗口或屏幕。此新平台选择器无需为 Chrome 分配屏幕录制权限,并且与其他 macOS 应用程序中的屏幕共享一致。

新的选择器将不会在 macOS Sequoia 的第一次更新之前启用,预计 15.1 版本将在 15.0 初始版本发布后一个月内发布。在此之前,Chrome 用户可能会看到一个警告对话框,提示 Chrome 尚未使用新的选择器 API。

image

3.
新帐户菜单

一些用户现在可以通过在新标签页上点击他们的头像来访问新的账户菜单。新的账户菜单允许他们轻松地注销、切换账户以及解决与 Chrome 账户相关的错误。

3.1. IOS 上的 130

image

4.
Android 上的 PDF 查看器

此功能提供了在 Chrome 浏览器 UI 中查看 PDF 的功能。

使用此功能,PDF 将在 Chrome 中无缝呈现。用户仍然可以下载 PDF 并使用其他首选或第三方应用打开。

5.
节能模式下冻结标签

image

当节能模式激活时,Chrome 现在会冻结已隐藏且静默超过 5 分钟且占用大量 CPU 的标签。

这将延长电池寿命并通过减少 CPU 使用率来加快 Chrome 的速度。

5.1. 特殊标签除外

  1. 改标签有音频或视频会议功能
  2. 改标签控制外部设备(使用 Web USB、Web 蓝牙、Web HID 等)

6. 共享 Brotli 和共享 Zstandard 的压缩字典传输

功能增加了使用指定的先前响应作为外部字典,以 Brotli 或 Zstandard 对内容编码的响应进行压缩的支持。

7. 键盘可聚焦滚动容器

Chrome 130 通过使滚动容器在顺序焦点导航中可聚焦,改进了可访问性。目前,Tab 键不会聚焦滚动容器,除非
tabIndex
被显式设置为 0 或更大。

现在,默认情况下滚动容器可聚焦,这样无法(或不想)使用鼠标的用户可以通过 Tab 键和方向键来聚焦被裁剪的内容。只有当滚动容器中没有可通过键盘聚焦的子元素时,此行为才会启用。这样做是为了避免对包含可聚焦元素(如
<textarea>
)的滚动容器造成兼容性问题。

8. 支持非特殊的 URL

Chrome 130 支持非特殊 scheme 的 URL,例如
git://example.com/path
。此前,Chromium 的 URL 解析器不支持非特殊 URL,且将这些 URL 解析为不透明路径(opaque path),这与 URL 标准不一致。现在,Chromium 的 URL 解析器能够正确解析非特殊 URL,符合 URL 标准。

9. Android 版 Chrome 支持第三方自动填充和密码提供商

在 Chrome 130 中,添加了对 Android 自动填充的直接支持,这意味着这些提供商现在可以在 Chrome 的 Android 版本中正常工作,无需依赖无障碍 API。这将提高 Chrome 在 Android 上的性能。要利用此功能,用户需要确保在 Android 设置中配置了第三方提供商。然后,在 Chrome 中,用户需要打开设置 > 自动填充服务,并选择使用其他服务进行自动填充。

新设置将在 Chrome 130 中可用。如果用户使用新的设置,将立即生效。如果不使用新的设置,用户将继续通过无障碍功能使用 Google 和第三方提供商(如果已安装)。

10.
<meter>
元素的后备样式

在 Chrome 130 中,具有
appearance: none

<meter>
元素现在有了合理的后备样式,匹配 Safari 和 Firefox,而不是仅仅从页面上消失。此外,开发者现在可以自定义样式
<meter>
元素。

11. Chrome 浏览器中的新政策和更新政策

策略 描述
DataURLWhitespacePreservationEnabled 所有媒体类型的 DataURL 空白字符保留
CloudProfileReportingEnabled 为托管配置文件启用 Google Chrome 云报告

二、ChromeOS 130 版本更新

1. 快速插入

快速插入提供了一种快速插入表情符号、符号、GIF、Google Drive 链接以及快速计算和单位转换的方法,可以通过键盘按键(在部分型号上)或键盘快捷键实现。

在 ChromeOS 130 中,所有 ChromeOS 设备都可以使用新的快捷键 Launcher + f。新的硬件按键最初仅在三星 Galaxy Chromebook Plus 上提供,但快速插入键将在 2025 年推出,适用于更多设备。

image

2. 设置和快捷键更改

更新了设置中的快捷键和输入设备选项,包括:

  • 快速插入:Launcher + f

3. 专注于 ChromeOS

设计了“专注于 ChromeOS”功能,以帮助用户减少干扰,创建更高效的工作空间。通过“专注”,可以轻松设置和调整专注时间,启用或禁用请勿打扰(DND)模式,整理或创建新的 Google 任务,并沉浸在精选的播放列表中,这些播放列表可以帮助更好地专注,包括专注音效等。

要使用“专注”,请前往

Quick Settings > Focus

image

4. 增强的 Drive 文件访问

除了在 Tote 中标记的文件之外,还可以直接从架子上访问所有标记的 Drive 文件,现已支持离线访问。

5. Tote 中的新建议

通过本地和 Drive 文件建议,快速访问和固定最需要的文件。Tote 中的新建议部分会向用户推荐文件,提高了那些对用户有用的文件的可见性,以便他们固定并离线访问。

6. 欢迎回顾

新的欢迎回顾功能帮助用户在启动时恢复工作并探索新选项。一旦启用此功能,将能够预览并恢复上次会话中的应用和标签页。欢迎回顾还提供有用的信息,如天气、下一个日历事件、来自其他设备的最近标签页以及相关的 Google Drive 建议。

要启用此功能,请选择
Settings > System Preferences > Startup > Welcome Recap
,并确保为设备选择“
Ask every time
”。

设置 > 系统偏好 > 启动 > 欢迎回顾

image

7. 工作室风格麦克风

通过在视频通话控制中启用此功能,使 Chromebook 的内置麦克风听起来像专业的工作室麦克风。工作室风格麦克风包括现有的噪声取消和去混响效果,并通过先进的平衡、细节重建和房间适应进一步增强这些效果。启用噪声取消的用户将默认获得工作室风格麦克风的增强效果,从此版本开始。如果用户想恢复到旧的仅噪声取消效果,可以在
Settings > Device > Audio
中选择相应的选项。此功能仅适用于 Chromebook Plus 设备。

设置 > 设备 > 音频

8. AI 驱动的录音应用

ChromeOS 130 引入了新的 Google AI 驱动的录音应用,可创建转录,能够检测和标记说话者,并提供录音内容的摘要。不仅限于录音,还提供语音转文本、内容摘要和标题建议,均由 Google AI 提供支持。

9. 内容扫描用于托管访客会话

现在允许组织将 Chrome Enterprise Premium 强大的扫描和基于内容和上下文的保护扩展到 ChromeOS 上托管访客会话中的本地文件。例如,当用户尝试将包含社会安全号码的错误文件复制到外部驱动器时,该文件会立即被阻止,从而保护这一机密信息。

10. 在 Kiosk 模式中允许额外的 URL

如果 Kiosk 应用使用多个 URL 源,IT 管理员现在可以输入额外的源。所有指定的源将自动获得权限。任何不在此列表中的其他源将被拒绝权限。

image

11. 外观效果

外观效果在相机、虚拟会议和短视频产品中长期以来一直很受欢迎,并已在一些 Google 产品中推出。在 ChromeOS 130 中,将此功能集成到 Chromebook 的视频通话控制中。仅在 Chromebook Plus 设备上可用。

12. 更加可访问的隐私控制

在此次发布中,使 Chrome 浏览器的操作系统级隐私控制对用户更易获取。旨在让用户更加意识到,要使摄像头或麦克风工作,他们需要启用操作系统级的隐私控制。

image

13. 增强的键盘亮度控制

Chromebook 用户现在可以直接从设置应用轻松调整键盘亮度和控制环境光传感器。这个新功能允许将键盘亮度设置到合适的水平,并根据需要开启或关闭环境光传感器。

14. 增强的显示亮度控制

Chromebook 用户现在可以直接从设置应用轻松调整显示亮度和控制环境光传感器。这个新功能允许在设置中将屏幕亮度设置到合适的水平,并根据需要开启或关闭环境光传感器。

15. 在 ChromeOS 上帮助我阅读

在 ChromeOS 上帮助我阅读提供了一个 AI 驱动的解决方案,帮助快速找到任何文本中所需的信息。只需右键单击空白区域,即可在现有上下文菜单上方显示“帮助我阅读”卡片,轻松获取在浏览器和图库中阅读内容的要点。帮助我阅读面板展示了文本的摘要和一个自由问答字段,允许询问有关文本的具体问题。仅在 Chromebook Plus 设备上可用。

image

16. 多日历支持

新版本推出了多日历支持,允许用户查看他们在 Google 日历中选择的多个日历中的所有事件。

image

17. 画中画窗口

ChromeOS 用户现在可以享受更大的灵活性,使用画中画(PiP)窗口。PiP Tuck 允许用户将 PiP 窗口临时移动到屏幕的一侧,腾出宝贵的屏幕空间,同时保持视频的便捷访问。此外,可以通过快速双击轻松调整 PiP 窗口的大小,在两种尺寸之间切换,以获得最佳观看体验。

image

18. 改进的 ARC++ 用户体验

为了改善 ChromeOS 和 ARC++ 的用户体验,将 ARC++ 的非紧急后台和错误通知移至系统托盘。这可以防止这些消息不必要地弹出在前台,从而打扰用户的使用体验。

19. 新的策略以控制接入点名称

对于具有蜂窝功能的 Chromebook,接入点名称(APN)策略允许管理员限制自定义 APN 的使用。通过在一般网络设置中将 AllowAPNModification 标志设置为限制,他们可以防止最终用户添加或使用任何自定义 APN。

image

20. Microsoft SCEP SID 更新

仅适用于使用 Microsoft NPS 进行 RADIUS 的 SCEP 部署。如果没有将 SCEP 证书与 Microsoft NPS 结合使用于 Chromebook 网络连接,则可以忽略这些指令的其余部分。

三、Chrome 130 版本更新日期

1. Chrome 130

1.1. Beta 版

2024 年 9 月 18 日,星期三

1.2. 稳定版本

2024 年 10 月 15 日,星期二

2. Chrome操作系统

2.1. Beta 版

2024 年 10 月 1 日,星期二

2.2. 稳定版本

2024 年 10 月 29 日,星期二

参考资料

这两个滤波器也是很久前就看过的,最近偶然翻起那本比较经典的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在数据库操作方面更加高效和安全。