c/c++的opencv伽马噪声
理解与实现 C++/OpenCV 中的伽马噪声 🖼️
噪声是大多数图像采集过程中固有的组成部分。理解和模拟不同类型的噪声对于开发鲁棒的图像处理算法至关重要,尤其是在去噪方面。虽然高斯噪声和椒盐噪声是常被讨论的类型,但伽马噪声(通常与爱尔朗分布相关)是另一种会遇到的噪声类型,尤其是在激光散斑或声纳等成像系统中。
本文将指导您如何使用 C++ 和 OpenCV 向图像中添加伽马噪声。
什么是伽马噪声?
伽马噪声是一种乘性噪声,其噪声值遵循伽马分布。伽马分布的概率密度函数 (PDF) 通常由形状参数 k k k(也常表示为 α \alpha α)和尺度参数 θ \theta θ(也常表示为 β \beta β)定义。
f ( x ; k , θ ) = x k − 1 e − x / θ θ k Γ ( k ) for x > 0 and k , θ > 0 f(x; k, \theta) = \frac{x^{k-1}e^{-x/\theta}}{\theta^k \Gamma(k)} \quad \text{for } x > 0 \text{ and } k, \theta > 0 f(x;k,θ)=θkΓ(k)xk−1e−x/θfor x>0 and k,θ>0
其中 Γ ( k ) \Gamma(k) Γ(k) 是伽马函数。当 k = 1 k=1 k=1 时,伽马分布变为指数分布。当 k k k 是整数时,它是爱尔朗分布。
在图像处理中,添加伽马噪声意味着每个像素的强度乘以一个从伽马分布中抽取的随机变量。然而,OpenCV 的 cv::randn
直接支持高斯噪声,cv::randu
支持均匀噪声。对于伽马噪声,我们通常需要自己生成遵循伽马分布的随机数或近似其效果。一个常见的方法是生成具有特定方差和均值的噪声,如果直接生成伽马噪声比较复杂或并非严格要求,则通常使用相关的分布。
在图像噪声模拟的实际应用中,“伽马噪声”有时被宽泛地用来描述经过伽马校正的图像中的噪声,或者其特性最适合用伽马分布建模的噪声。通常,会采用一种更简单的方法,即添加具有一定均值和方差的噪声,然后对其进行缩放。
一种更直接模拟伽马噪声的方法是根据伽马分布生成随机数。虽然 OpenCV 没有像 randn
(针对高斯分布)那样直接为整个矩阵生成伽马分布随机数的内置函数,但 C++11 及更高版本在 <random>
头文件中提供了从各种分布(包括 std::gamma_distribution
)生成数字的功能。
在 OpenCV (C++) 中添加类伽马噪声
我们将使用 C++ 的 <random>
库来生成伽马分布的噪声值,然后将它们与图像像素相加或相乘。
步骤:
- 包含头文件:
opencv2/opencv.hpp
用于 OpenCV 功能,<random>
用于 C++11 随机数生成。 - 加载图像:读取输入图像。通常最好将其转换为浮点类型(例如
CV_32F
或CV_64F
)以进行噪声添加,特别是当噪声是乘性的或具有非整数均值时,以保持精度。 - 初始化随机数生成器:使用所需的形状 ( k k k) 和尺度 ( θ \theta θ) 参数设置伽马分布。
- 迭代并应用噪声:遍历图像的每个像素。对于每个像素,使用伽马分布生成一个噪声值。
- 加性噪声:
新像素值 = 原始像素值 + 噪声值
- 乘性噪声:
新像素值 = 原始像素值 * 噪声值
乘性噪声通常更能代表散斑等现象。
- 加性噪声:
- 归一化/裁剪:确保像素值保持在有效范围内(例如,对于
CV_8U
类型,范围是 0-255)。 - 显示/保存:显示或保存带噪声的图像。
C++ 代码示例
此示例演示了如何向图像添加乘性伽马噪声。
#include <opencv2/opencv.hpp>
#include <random> // 需要 <random> 来使用 std::gamma_distribution
#include <iostream>// 添加乘性伽马噪声的函数
cv::Mat addGammaNoise(const cv::Mat& src, double shape, double scale) {if (src.empty()) {std::cerr << "错误: 输入图像为空!" << std::endl;return src;}cv::Mat noisyImage;// 如果图像不是浮点类型,则将其转换为浮点类型以进行计算// 对于乘性噪声,噪声均值通常在 1.0 左右src.convertTo(noisyImage, CV_32F);// 设置伽马分布的随机数生成器std::default_random_engine generator(std::random_device{}());// 对于乘性噪声,通常期望均值在 1.0 左右。// 伽马分布 Gamma(shape, scale) 的均值是 shape * scale。// 如果你想要均值 = 1,你可以选择 scale = 1.0 / shape。// 或者,调整生成的噪声:noise = (noise_val - mean) * desired_std_dev + desired_mean// 为简单起见,我们这里直接使用 shape 和 scale。// 用户应调整 shape 和 scale 以获得期望的噪声强度。// 示例:shape=1, scale=0.2。均值=0.2。如果相乘,这将显著使图像变暗。// 我们尝试将噪声中心设在 1.0 附近以便进行乘法操作。// 一种常见的方法是生成噪声然后适当地缩放它。// 这里我们演示直接乘以伽马分布的值。// 请考虑 'shape' (k) 和 'scale' (theta) 的值// 较高的 shape 值和较小的 scale 值可以使分布更集中在其均值附近。std::gamma_distribution<float> distribution(shape, scale);for (int y = 0; y < noisyImage.rows; ++y) {for (int x = 0; x < noisyImage.cols; ++x) {float noise_val = distribution(generator);if (noisyImage.channels() == 1) { // 灰度图noisyImage.at<float>(y, x) *= noise_val;} else if (noisyImage.channels() == 3) { // 彩色图cv::Vec3f& pixel = noisyImage.at<cv::Vec3f>(y, x);pixel[0] *= noise_val; // 对所有通道应用相同的噪声pixel[1] *= noise_val; // 或者为每个通道生成不同的噪声pixel[2] *= noise_val;}}}// 如果需要,进行归一化并转换回原始类型// cv::normalize(noisyImage, noisyImage, 0, 255, cv::NORM_MINMAX);// noisyImage.convertTo(noisyImage, src.type());// 为了显示,如果转换为 CV_8U,最好裁剪到 [0, 255]// 如果原始图像是 CV_8Uif (src.type() == CV_8U) {// 方案 1:归一化到 0-255 (可能会显著改变外观)// cv::normalize(noisyImage, noisyImage, 0, 255, cv::NORM_MINMAX);// noisyImage.convertTo(noisyImage, CV_8U);// 方案 2:裁剪值并转换 (对于乘性噪声更直接)// 确保由于乘法,值不会超出典型范围// 这一步在很大程度上取决于所选的 shape 和 scale 参数cv::Mat tempImage;// noisyImage.convertTo(tempImage, CV_8U, 1.0, 0); // 如有必要进行缩放和移位,这里只是基本转换// convertTo 会处理裁剪// 如果需要在转换为 CV_8U 之前手动裁剪,以避免饱和伪影。for (int y = 0; y < noisyImage.rows; ++y) { // noisyImage.rows 而非 tempImage.rows (tempImage此时可能未初始化)for (int x = 0; x < noisyImage.cols; ++x) { // noisyImage.colsif (noisyImage.channels() == 1) {float val = noisyImage.at<float>(y, x);if (val > 255.0f) tempImage.at<uchar>(y, x) = 255;else if (val < 0.0f) tempImage.at<uchar>(y, x) = 0;else tempImage.at<uchar>(y, x) = static_cast<uchar>(val);} else if (noisyImage.channels() == 3) {cv::Vec3f& n_pixel = noisyImage.at<cv::Vec3f>(y, x);cv::Vec3b& t_pixel = tempImage.at<cv::Vec3b>(y, x); // 需要确保tempImage已正确初始化大小和类型// 在循环外初始化 tempImage = cv::Mat(noisyImage.size(), CV_8UC3);for(int c=0; c<3; ++c) {if (n_pixel[c] > 255.0f) t_pixel[c] = 255;else if (n_pixel[c] < 0.0f) t_pixel[c] = 0;else t_pixel[c] = static_cast<uchar>(n_pixel[c]);}}}}// 正确的 tempImage 初始化应在循环前:// tempImage = cv::Mat::zeros(noisyImage.size(), CV_8UC(noisyImage.channels()));// 或者更简单的方式是直接用 convertTo 并依赖其裁剪:noisyImage.convertTo(tempImage, CV_8U); // convertTo 会自动处理裁剪到 [0, 255]return tempImage;}return noisyImage;
}int main(int argc, char** argv) {if (argc < 2) {std::cout << "用法: " << argv[0] << " <ImagePath>" << std::endl;return -1;}cv::Mat image = cv::imread(argv[1], cv::IMREAD_COLOR); // 加载为彩色图像// cv::Mat image = cv::imread(argv[1], cv::IMREAD_GRAYSCALE); // 加载为灰度图像if (image.empty()) {std::cerr << "错误: 无法读取图像: " << argv[1] << std::endl;return -1;}// 伽马分布参数:// shape (k 或 alpha): 决定分布的形状。// scale (theta 或 beta): 决定分布的展宽。// 均值 = shape * scale, 方差 = shape * scale^2// 对于大致保持亮度的乘性噪声,你可能希望噪声均值在 1.0 左右。// 示例: shape = 10.0, scale = 0.1 (均值 = 1.0, 方差 = 0.1)// 示例: shape = 20.0, scale = 0.05 (均值 = 1.0, 方差 = 0.05, 噪声较小)// 示例: shape = 1.0, scale = 0.2 (均值 = 0.2, 方差 = 0.04, 会显著使图像变暗)// 根据期望的噪声特性仔细选择参数。double gamma_shape = 10.0;double gamma_scale = 0.1; // 使得噪声因子的均值为 1.0cv::Mat noisyImage = addGammaNoise(image, gamma_shape, gamma_scale);if (noisyImage.empty()) {std::cerr << "错误: 添加噪声失败。" << std::endl;return -1;}cv::imshow("原始图像", image);cv::imshow("伽马噪声图像", noisyImage);cv::waitKey(0);// 可选:保存图像// cv::imwrite("gamma_noisy_image.png", noisyImage);return 0;
}
代码修正说明:
在原始英文版本的 addGammaNoise
函数中,将 CV_32F
图像转换回 CV_8U
时,手动裁剪部分存在一个潜在问题:tempImage
在被写入之前可能没有正确初始化大小和类型。更安全和简洁的做法是:
- 在循环之前初始化
tempImage
,例如:cv::Mat tempImage = cv::Mat::zeros(noisyImage.size(), CV_8UC(noisyImage.channels()));
- 或者,更简单地,直接使用
noisyImage.convertTo(tempImage, CV_8U);
因为convertTo
方法在转换到CV_8U
时会自动处理超出 [0, 255] 范围值的裁剪。上面的中文代码示例中已倾向于使用convertTo
。
代码解释
-
头文件:
opencv2/opencv.hpp
: OpenCV 核心功能。<random>
: 用于std::default_random_engine
,std::random_device
, 和std::gamma_distribution
。<iostream>
: 用于控制台输入/输出。
-
addGammaNoise
函数:- 接收源图像 (
src
)、伽马分布的shape
( k k k) 和scale
( θ \theta θ) 参数作为输入。 - 将输入图像转换为
CV_32F
(32位浮点型),以便与噪声值进行精确的浮点乘法运算。这对于乘性噪声至关重要。 std::default_random_engine generator(std::random_device{}())
: 初始化一个随机数引擎。std::random_device{}
提供一个非确定性种子。std::gamma_distribution<float> distribution(shape, scale)
: 创建一个伽马分布对象。生成的值将是float
类型。- 关于参数的重要说明: 伽马分布的均值是
shape * scale
。对于乘性噪声,如果希望平均亮度大致保持不变,目标均值应在 1.0 左右。因此,scale
可以设置为1.0 / shape
,或者对生成的噪声进行归一化。在示例中,选择gamma_shape = 10.0
和gamma_scale = 0.1
,使得它们的乘积(噪声因子的均值)为1.0
。
- 关于参数的重要说明: 伽马分布的均值是
- 代码遍历每个像素。对于每个像素,
distribution(generator)
生成一个遵循伽马分布的随机数。 - 这个
noise_val
与像素的强度相乘。如果是彩色图像,相同的噪声值将应用于 R、G、B 所有通道。如果需要,您可以修改此部分,为每个通道应用不同的噪声。 - 裁剪/转换: 添加噪声后,像素值可能会超过典型的 0-255 范围(对于
CV_8U
图像)或低于 0。代码中包含将CV_32F
图像转换回CV_8U
(用于显示/保存的常见类型)的部分。这涉及到缩放和裁剪。如上所述,cv::Mat::convertTo
在转换到CV_8U
时会自动处理到 [0, 255] 范围的裁剪。
- 接收源图像 (
-
main
函数:- 从命令行参数加载图像。
- 设置
gamma_shape
和gamma_scale
参数。调整这些值以控制噪声的强度和特性。 - 调用
addGammaNoise
获取带噪声的图像。 - 显示原始图像和带噪声的图像。
编译
要编译此 C++ 代码,您需要 g++(或任何兼容 C++11 的编译器)和已安装的 OpenCV。
g++ -o gamma_noise_app gamma_noise.cpp `pkg-config --cflags --libs opencv4` -std=c++11
(如果您使用的是较旧的 OpenCV 版本或者您的 pkg-config
设置不同,请使用 opencv
替换 opencv4
)。
运行:
./gamma_noise_app path_to_your_image.jpg
注意事项
- 参数调整: 伽马分布的
shape
( k k k) 和scale
( θ \theta θ) 参数显著影响噪声特性。请尝试不同的值。- 较高的
shape
值(对于固定的均值)倾向于使分布更窄且更对称,接近高斯分布。 - 伽马噪声的方差是
shape * scale^2
。
- 较高的
- 加性与乘性噪声: 该示例实现了乘性噪声,这对于传感器噪声(随信号强度变化)或散斑噪声等现象很常见。对于加性伽马噪声,您需要将生成的噪声值相加而不是相乘。
- 归一化: 根据所选的
shape
和scale
参数,特别是对于乘性噪声,生成的图像可能会明显变亮或变暗。您可能需要对带噪声的图像进行归一化,或调整噪声的均值使其在 1.0 附近(对于乘法)或 0.0 附近(对于加法),以保持整体亮度。该示例尝试设置参数使乘性因子的均值为 1.0。 - 彩色图像: 该示例对彩色像素的所有通道应用相同的噪声值。对于更复杂的模拟,您可以为每个通道生成独立的噪声。
本指南为在您的 C++/OpenCV 项目中模拟伽马噪声提供了坚实的基础。请记住,准确的噪声建模通常需要了解您试图复制的特定成像系统或现象。
相关文章:
c/c++的opencv伽马噪声
理解与实现 C/OpenCV 中的伽马噪声 🖼️ 噪声是大多数图像采集过程中固有的组成部分。理解和模拟不同类型的噪声对于开发鲁棒的图像处理算法至关重要,尤其是在去噪方面。虽然高斯噪声和椒盐噪声是常被讨论的类型,但伽马噪声(通常…...

LiveGBS国标视频平台收流模式:UDP、TCP被动与TCP主动传输模式之差异剖析
LiveGBS国标视频平台收流模式:UDP、TCP被动与TCP主动传输模式之差异剖析 1、背景2、信令传输3、视频流传输3.1、UDP传输模式3.2、TCP被动传输模式3.3、TCP主动传输模式 4、WEB配置流传输模式4.1、编辑模式4.2、下拉切换模式 5、搭建GB28181视频直播平台 1、背景 在…...
跳表(Skip List)查找算法详解
1、原理 跳表是一种概率型数据结构,通过多层有序链表实现高效查找,时间复杂度接近平衡树(O(log n))。其核心思想是通过层级索引加速搜索,结构类似火车时刻表的“快车-慢车”模式。 关键特性: 多层链表&a…...
React从基础入门到高级实战:React 核心技术 - React 与 TypeScript:构建类型安全的应用
React 与 TypeScript:构建类型安全的应用 在现代前端开发中,TypeScript 因其强大的类型系统和编译时错误检查功能,已成为 React 开发者的热门选择。通过为代码添加类型定义,TypeScript 能够显著提升代码的健壮性、可维护性和团队…...
Django orm详解--组成部件
Django ORM 的核心部件可分为模型系统、查询系统、数据库后端和辅助工具四大类,每个部件负责不同的职责,共同实现对象与关系数据库的映射。以下是核心部件的分层解析: 一、模型系统(Model System) 1. 模型基类&#…...

Tomcat 使用与配置全解
一、 Tomcat简介 Tomcat服务器是Apache的一个开源免费的Web容器。它实现了JavaEE平台下部分技术规范,属于轻量级应用服务器。 1. Tomcat版本 Tomcat版本 JDK版本 Servlet版本 JSP版本 10.0.X 8 and later 5.0 3.0 9.0.x 8 and later 4.0 2.3 8.0.x 7…...
Chrome 开发中的任务调度与线程模型实战指南
内容 概述 快速入门指南 核心概念线程词典 线程任务优先使用序列而不是物理线程 发布并行任务 直接发布到线程池通过 TaskRunner 发布 发布顺序任务 发布到新序列发布到当前(虚拟)主题 使用序列代替锁将多个任务发布到同一线程 发布到浏览器进程中的主线…...

aws instance store 的恢复
1: aws instance store 要在launch instance 才可以创建,而且,通过snapshot 恢复后,instance store 里面的数据会丢失。 下面是创建instance store 的过程,和通过两种方式恢复,发现/etc/fstab 不同的写法,有的不能启动: [root@ip-xx ~]# lsblk NAME MAJ:MIN RM …...
从零开始创建 Vue 3 开发环境并构建第一个 Demo
Vue 3 是目前前端开发中非常流行的渐进式 JavaScript 框架。本文将手把手带你完成从环境搭建到运行一个基础 Vue 3 示例的全过程。 📦 一、环境准备 1. 安装 Node.js Vue 项目依赖 Node.js 运行环境,请确保你的电脑已安装 Node.js(建议使用…...

EasyRTC音视频实时通话助力微信小程序:打造低延迟、高可靠的VoIP端到端呼叫解决方案
一、方案概述 在数字化通信浪潮下,端到端实时音视频能力成为刚需。依托庞大用户生态的微信小程序,是实现此类功能的优质载体。基于WebRTC的EasyRTC音视频SDK,为小程序VoIP呼叫提供轻量化解决方案,通过技术优化实现低延迟通信&a…...

STM32 SPI通信(软件)
一、SPI简介 SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slav…...

每日刷题c++
快速幂 #include <iostream> using namespace std; #define int long long int power(int a, int b, int p) {int ans 1;while (b){if (b % 2){ans * a;ans % p; // 随时取模}a * a;a % p; // 随时取模b / 2;}return ans; } signed main() {int a, b, p;cin >> a …...
(自用)Java学习-5.19(地址管理,三级联动,预支付)
1. 地址管理模块 地址展示 前端:通过 showAddress() 发起 Ajax GET 请求,动态渲染地址列表表格,使用 #{tag}、#{name} 等占位符替换真实数据。 后端: 控制器层调用 AddressService,通过 AddressMapper 查询用户地址数…...
【容器】docker使用问题处理
问题一、systemctl start docker启动报 ERROR: ZONE_CONFLICT: docker0 already bound to a zone 处理方法 firewall-cmd --permanent --zonedocker --change-interfacedocker0 systemctl restart firewalld 问题二、启动容器报 ptables failed/iptables: No chain/target/…...

ChemDraw 2023|Win英文|化学结构编辑器|安装教程
软件下载 【名称】:ChemDraw 2023 【大小】:1.34G 【语言】:英文界面 【安装环境】:Win10/Win11 【夸克网盘下载链接】(务必手机注册): https://pan.quark.cn/s/320bcb67da80 【网站下载…...
Vue3实现提示文字组件
Vue3 实现一个文字提示组件(Tooltip) 文字提示(Tooltip)是前端开发中非常常见的组件,通常用于在用户悬停某个元素时显示额外的信息。 一、需求分析 我们要实现一个 Vue3 的文字提示组件,具备以下功能&…...
JAVA与C语言之间的差异(一)
一、代码习惯以及主函数 JAVA中{在使用的时候不要换行 public static void main(String[] args) {int[] array {1, 2, 3};for(int i 0; i < array.length; i){System.out.println(array[i] " ");}} 其次,以main函数为主函数: public …...
深入剖析 C 语言中的指针数组与数组指针
资料合集下载链接: https://pan.quark.cn/s/472bbdfcd014 在C语言中,指针是其强大和灵活性的核心。然而,围绕指针的概念有很多容易混淆的地方,其中“指针数组”和“数组指针”就是一对常见的“双胞胎”概念。它们名称相似,但含义和用法却大相径庭。 本文旨在清…...

4.1.1 Spark SQL概述
Spark SQL是Apache Spark的一个模块,专门用于处理结构化数据。它引入了DataFrame这一编程抽象,DataFrame是带有Schema信息的分布式数据集合,类似于关系型数据库中的表。用户可以通过SQL、DataFrames API和Datasets API三种方式操作结构化数据…...
【VSCode-Qt】Docker远程连接的项目UI文件在 VSCode 上无法预览
Docker远程连接的UI文件在 VSCode 上无法预览,通常是因为 VSCode 通过远程开发扩展(Remote - SSH/Docker)连接到 Docker 容器时,某些图形化功能未正确配置或支持。以下是可能原因和解决方案: 原因分析 X11 转发未配置…...

redis五种数据结构详解(java实现对应的案例)
一、简述 Redis是一款高性能的键值对存储数据库,它支持五种基本数据类型,分别是字符串(String)、列表(List)、哈希(Hash)、集合(Set)、有序集合(Sorted Set)。 二、五种基本数据类型 2.1 字符串(String) String是Redis最基本的类型,一个key对…...
Telnet 命令详解
Telnet 命令详解:从基础到实战应用 Telnet 是一种历史悠久的网络协议,广泛用于远程登录和管理设备。尽管如今更安全的 SSH 协议已逐渐取代其地位,但 Telnet 在特定场景下依然发挥着重要作用。本文将深入解析 Telnet 命令的参数、使用场景及注…...
深度解析新能源汽车结构与工作原理
一、核心系统架构 新能源汽车主要由三大核心系统构成: 电力驱动系统:包含永磁同步电机、电机控制器(MCU)及减速器,采用三合一集成设计实现轻量化。永磁同步电机通过电磁感应原理将电能转化为机械能,其效率可…...

React 生命周期与 Hook:从原理到实战全解析
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 持续学习,不断…...
OpenSSL 与 C++ 搭建一个支持 TLS 1.3 的服务器
好的,我们可以使用 OpenSSL 与 C 搭建一个支持 TLS 1.3 的服务器。下面是: ✅ 一、完整示例代码(基于 OpenSSL) 使用 C 和 OpenSSL 创建一个简单的 TCP TLS 服务器,支持 TLS 1.3。 ✅ 代码:tls_server.cp…...
HOW - 简历和求职面试宝典(六)
文章目录 1. 如何更好地认识自己?一、认清自己的实力二、明确求职方向三、认识求职岗位与自己的匹配度2. 如何判断公司是否合适自己?一、网站平台二、内部人员三、通过面试官1. 如何更好地认识自己? 一、认清自己的实力 我们经常会听到这样的话:我现在的工作做的好不开心…...

【机器学习基础】机器学习入门核心算法:逻辑回归(Logistic Regression)
机器学习入门核心算法:逻辑回归(Logistic Regression) 一、算法逻辑1.1 基本概念1.2 Sigmoid函数1.3 决策边界 二、算法原理与数学推导2.1 概率建模2.2 损失函数推导2.3 梯度下降优化2.4 正则化处理 三、模型评估3.1 常用评估指标3.2 ROC曲线…...
深入理解设计模式之命令模式
下面是一篇关于设计模式之命令模式(Command Pattern)的详细博客,并附有 Java 实现代码示例。 深入理解设计模式之:命令模式(Command Pattern) 一、什么是命令模式? 命令模式(Comma…...

智能仓储落地:机器人如何通过自动化减少仓库操作失误?
仓库作业的速度和准确性至关重要,尤其是在当前对无差错、高效作业的要求达到前所未有的环境下。每一个错误,无论是物品放错位置还是库存差异,都会在供应链中产生连锁反应,造成延误、增加成本,并最终影响客户满意度。 …...
Android 架构演进之路:从 MVC 到 MVI,拥抱单向数据流的革命
在移动应用开发的世界里,架构模式的演进从未停歇。从早期的 MVC 到后来的 MVP、MVVM,每一次变革都在尝试解决前一代架构的痛点。而今天,我们将探讨一种全新的架构模式 ——MVI(Model-View-Intent),它借鉴了…...