MulSpectrums大家好Opencv在很多工程项目中都会用到而OpencvSharp则是以C#开发与实现的Opencv操作库对.NET开发人员友好但很多API的中文资料、应用场景及常见坑点等缺乏系统性归纳因此这系列博客将给大家带来Cv2及Mat对象全系列算子学习教案供大家参考学习。Cv2.MulSpectrums教案版本V1.0面向对象OpenCvSharp 初学者所属模块core源码位置OpenCvSharp/Cv2/Cv2_core.cs:2988摘要MulSpectrums 会对两个频谱做逐元素乘法并可选择是否对第二个频谱取共轭。本文用两组很小的复数频谱分别演示普通乘法和共轭乘法帮助初学者理解卷积、相关和频域乘法的关系。1. 函数名称带参数签名publicstaticvoidMulSpectrums(InputArraya,InputArrayb,OutputArrayc,DftFlagsflags,boolconjBfalse)2. 函数用途Cv2.MulSpectrums的作用是把两个频谱逐元素相乘。它最常见的用途有和Dft、Idft配合实现频域卷积。和Dft、Idft配合实现频域相关。在滤波、模板匹配和信号分析里作为中间步骤。对初学者来说最重要的知识点是这个函数本身不做变换它只是在频域里“乘”。如果你手里拿的是时域数据必须先 DFT如果你想把结果还原回时域还得再 Idft。3. 函数公式如果两个频谱分别是Akakjbk,Bkckjdk A_k a_k jb_k, \quad B_k c_k jd_kAkakjbk,Bkckjdk那么普通乘法的结果是CkAk⋅Bk C_k A_k \cdot B_kCkAk⋅Bk展开以后就是Ck(akck−bkdk)j(akdkbkck) C_k (a_k c_k - b_k d_k) j(a_k d_k b_k c_k)Ck(akck−bkdk)j(akdkbkck)如果把第二个频谱先取共轭那么结果就变成CkAk⋅Bk‾ C_k A_k \cdot \overline{B_k}CkAk⋅Bk这正是相关运算里经常用到的形式。本文示例使用的两组频谱是A[10, −22j, −2, −2−2j] A [10,\,-22j,\,-2,\,-2-2j]A[10,−22j,−2,−2−2j]B[1, −j, −1, j] B [1,\,-j,\,-1,\,j]B[1,−j,−1,j]普通乘法和共轭乘法的结果分别是A⋅B[10, 22j, 2, 2−2j] A \cdot B [10,\,22j,\,2,\,2-2j]A⋅B[10,22j,2,2−2j]A⋅B‾[10, −2−2j, 2, −22j] A \cdot \overline{B} [10,\,-2-2j,\,2,\,-22j]A⋅B[10,−2−2j,2,−22j]4. 函数原理说明Cv2.MulSpectrums可以理解成下面几步检查两个输入是否同尺寸、同类型。根据conjB决定是否对第二个频谱取共轭。对两个频谱做逐元素复数乘法。把结果写入c。这里最容易混淆的是MulSpectrums和Dft完全不是一类事情。Dft负责把数据搬到频域MulSpectrums负责在频域里做乘法而Idft负责把结果搬回时域。5. 参数含义解析参数名类型必填含义aInputArray是第一个频谱bInputArray是第二个频谱cOutputArray是输出频谱flagsDftFlags是频谱乘法的标志最常见是None或RowsconjBbool否是否先对第二个频谱取共轭默认false补充说明a、b和c必须同尺寸、同类型。如果输入是多行独立频谱才会考虑DftFlags.Rows。conjBtrue常用于相关conjBfalse常用于卷积。这个函数既能处理完整复数频谱也能处理 CCS-packed 频谱。6. 应用场景列表场景名场景说明典型用途场景A卷积频域乘法后再 Idft快速滤波场景B相关先对第二个频谱取共轭模板匹配场景C频域合成直接在频域里组合两个信号信号处理7. 函数使用示例下面的 Console 程序演示Cv2.MulSpectrums。为了让conjB的作用更明显我们给出两组很小的频谱并分别计算普通乘法和共轭乘法。usingSystem.Globalization;usingSystem.Text;usingOpenCvSharp;internalstaticclassProgram{/// summary/// 程序入口。/// /summaryprivatestaticvoidMain(){// 控制台要支持中文输出这样说明文字不会乱码。Console.OutputEncodingEncoding.UTF8;// 这里使用两组很小的频谱数据既能看清逐元素乘法又能明显看出 conjBtrue 之后的变化。varspectrumADatanewdouble[,]{{10.0,0.0},{-2.0,2.0},{-2.0,0.0},{-2.0,-2.0},};varspectrumBDatanewdouble[,]{{1.0,0.0},{0.0,-1.0},{-1.0,0.0},{0.0,1.0},};varexpectedProductnewdouble[,]{{10.0,0.0},{2.0,2.0},{2.0,0.0},{2.0,-2.0},};varexpectedConjugatedProductnewdouble[,]{{10.0,0.0},{-2.0,-2.0},{2.0,0.0},{-2.0,2.0},};usingvarspectrumACreateComplexRowVector(spectrumAData);usingvarspectrumBCreateComplexRowVector(spectrumBData);usingvarproductnewMat();usingvarconjugatedProductnewMat();// 第一次调用conjBfalse做普通逐元素乘法。Cv2.MulSpectrums(spectrumA,spectrumB,product,DftFlags.None,false);// 第二次调用conjBtrue先对第二个频谱取共轭再做逐元素乘法。Cv2.MulSpectrums(spectrumA,spectrumB,conjugatedProduct,DftFlags.None,true);varactualProductReadComplexMatrix(product);varactualConjugatedProductReadComplexMatrix(conjugatedProduct);PrintComplexMatrixComparison(A[k] * B[k],actualProduct,expectedProduct);PrintComplexMatrixComparison(A[k] * conj(B[k]),actualConjugatedProduct,expectedConjugatedProduct);}/// summary/// 把二维复数数组包装成 1 行 N 列的复数矩阵。/// /summaryprivatestaticMatCreateComplexRowVector(double[,]complexPairs){// 每一行只存一个复数因此这里只需要一行 N 列。varvectornewMat(1,complexPairs.GetLength(0),MatType.CV_64FC2);for(varindex0;indexcomplexPairs.GetLength(0);index){// Vec2d 的 Item0 是实部Item1 是虚部。vector.AtVec2d(0,index)newVec2d(complexPairs[index,0],complexPairs[index,1]);}returnvector;}/// summary/// 从 Mat 中读取复数矩阵并展开成 [实部, 虚部] 两列的形式。/// /summaryprivatestaticdouble[,]ReadComplexMatrix(Matsource){usingvarconvertednewMat();source.ConvertTo(converted,MatType.CV_64FC2);varresultnewdouble[converted.Rows*converted.Cols,2];varindex0;for(varrow0;rowconverted.Rows;row){for(varcol0;colconverted.Cols;col){varvalueconverted.AtVec2d(row,col);result[index,0]value.Item0;result[index,1]value.Item1;index;}}returnresult;}/// summary/// 打印复数矩阵把每一行当成一个复数显示。/// /summaryprivatestaticvoidPrintComplexMatrix(stringtitle,double[,]matrix){Console.WriteLine(title);for(varrow0;rowmatrix.GetLength(0);row){varrealTextmatrix[row,0].ToString(F6,CultureInfo.InvariantCulture);varimagValuematrix[row,1];varsignimagValue0?:-;varimagTextMath.Abs(imagValue).ToString(F6,CultureInfo.InvariantCulture);Console.WriteLine($[{row}]{realText}{sign}{imagText}i);}Console.WriteLine();}/// summary/// 打印复数矩阵对照结果并计算最大绝对误差。/// /summaryprivatestaticvoidPrintComplexMatrixComparison(stringtitle,double[,]actual,double[,]expected){PrintComplexMatrix(${title}- 实际值,actual);PrintComplexMatrix(${title}- 期望值,expected);Console.WriteLine($最大绝对误差{ComputeMaxAbsDiff(actual,expected):F12});Console.WriteLine();}/// summary/// 计算两个矩阵的最大绝对误差。/// /summaryprivatestaticdoubleComputeMaxAbsDiff(double[,]left,double[,]right){varmax0.0;for(varrow0;rowleft.GetLength(0);row){for(varcol0;colleft.GetLength(1);col){vardiffMath.Abs(left[row,col]-right[row,col]);if(diffmax){maxdiff;}}}returnmax;}}8. 运行结果解读如果你运行上面的示例应该能看到下面几个结论conjBfalse时结果就是两个频谱的普通逐元素乘法。conjBtrue时第二个频谱先取共轭因此虚部符号会反向。这说明相关和卷积在频域里的处理方式并不完全一样。9. 常见错误与排查把MulSpectrums当成变换函数忘记它本身不做 DFT。忘记两个输入必须同尺寸、同类型。搞混conjBtrue和conjBfalse的物理意义。把 CCS-packed 频谱和完整复数频谱混着算导致结果难以解释。10. 进阶说明MulSpectrums在工程里最常见的用法是和Dft、Idft连在一起conv(x,y)F−1(F(x)⋅F(y)) \text{conv}(x, y) \mathcal{F}^{-1}(\mathcal{F}(x) \cdot \mathcal{F}(y))conv(x,y)F−1(F(x)⋅F(y))corr(x,y)F−1(F(x)⋅F(y)‾) \text{corr}(x, y) \mathcal{F}^{-1}(\mathcal{F}(x) \cdot \overline{\mathcal{F}(y)})corr(x,y)F−1(F(x)⋅F(y))如果输入是多行独立频谱才会用到DftFlags.Rows。普通的单行示例里这个标志通常不需要。11. 总结Cv2.MulSpectrums的核心职责就是在频域里把两个频谱逐元素乘起来。只要你记住conjB会影响第二个输入的虚部符号再把它和Dft、Idft连起来看卷积和相关就会变得很直观。
OpencvSharp 算子学习教案之 - Cv2.MulSpectrums
发布时间:2026/6/5 14:56:41
MulSpectrums大家好Opencv在很多工程项目中都会用到而OpencvSharp则是以C#开发与实现的Opencv操作库对.NET开发人员友好但很多API的中文资料、应用场景及常见坑点等缺乏系统性归纳因此这系列博客将给大家带来Cv2及Mat对象全系列算子学习教案供大家参考学习。Cv2.MulSpectrums教案版本V1.0面向对象OpenCvSharp 初学者所属模块core源码位置OpenCvSharp/Cv2/Cv2_core.cs:2988摘要MulSpectrums 会对两个频谱做逐元素乘法并可选择是否对第二个频谱取共轭。本文用两组很小的复数频谱分别演示普通乘法和共轭乘法帮助初学者理解卷积、相关和频域乘法的关系。1. 函数名称带参数签名publicstaticvoidMulSpectrums(InputArraya,InputArrayb,OutputArrayc,DftFlagsflags,boolconjBfalse)2. 函数用途Cv2.MulSpectrums的作用是把两个频谱逐元素相乘。它最常见的用途有和Dft、Idft配合实现频域卷积。和Dft、Idft配合实现频域相关。在滤波、模板匹配和信号分析里作为中间步骤。对初学者来说最重要的知识点是这个函数本身不做变换它只是在频域里“乘”。如果你手里拿的是时域数据必须先 DFT如果你想把结果还原回时域还得再 Idft。3. 函数公式如果两个频谱分别是Akakjbk,Bkckjdk A_k a_k jb_k, \quad B_k c_k jd_kAkakjbk,Bkckjdk那么普通乘法的结果是CkAk⋅Bk C_k A_k \cdot B_kCkAk⋅Bk展开以后就是Ck(akck−bkdk)j(akdkbkck) C_k (a_k c_k - b_k d_k) j(a_k d_k b_k c_k)Ck(akck−bkdk)j(akdkbkck)如果把第二个频谱先取共轭那么结果就变成CkAk⋅Bk‾ C_k A_k \cdot \overline{B_k}CkAk⋅Bk这正是相关运算里经常用到的形式。本文示例使用的两组频谱是A[10, −22j, −2, −2−2j] A [10,\,-22j,\,-2,\,-2-2j]A[10,−22j,−2,−2−2j]B[1, −j, −1, j] B [1,\,-j,\,-1,\,j]B[1,−j,−1,j]普通乘法和共轭乘法的结果分别是A⋅B[10, 22j, 2, 2−2j] A \cdot B [10,\,22j,\,2,\,2-2j]A⋅B[10,22j,2,2−2j]A⋅B‾[10, −2−2j, 2, −22j] A \cdot \overline{B} [10,\,-2-2j,\,2,\,-22j]A⋅B[10,−2−2j,2,−22j]4. 函数原理说明Cv2.MulSpectrums可以理解成下面几步检查两个输入是否同尺寸、同类型。根据conjB决定是否对第二个频谱取共轭。对两个频谱做逐元素复数乘法。把结果写入c。这里最容易混淆的是MulSpectrums和Dft完全不是一类事情。Dft负责把数据搬到频域MulSpectrums负责在频域里做乘法而Idft负责把结果搬回时域。5. 参数含义解析参数名类型必填含义aInputArray是第一个频谱bInputArray是第二个频谱cOutputArray是输出频谱flagsDftFlags是频谱乘法的标志最常见是None或RowsconjBbool否是否先对第二个频谱取共轭默认false补充说明a、b和c必须同尺寸、同类型。如果输入是多行独立频谱才会考虑DftFlags.Rows。conjBtrue常用于相关conjBfalse常用于卷积。这个函数既能处理完整复数频谱也能处理 CCS-packed 频谱。6. 应用场景列表场景名场景说明典型用途场景A卷积频域乘法后再 Idft快速滤波场景B相关先对第二个频谱取共轭模板匹配场景C频域合成直接在频域里组合两个信号信号处理7. 函数使用示例下面的 Console 程序演示Cv2.MulSpectrums。为了让conjB的作用更明显我们给出两组很小的频谱并分别计算普通乘法和共轭乘法。usingSystem.Globalization;usingSystem.Text;usingOpenCvSharp;internalstaticclassProgram{/// summary/// 程序入口。/// /summaryprivatestaticvoidMain(){// 控制台要支持中文输出这样说明文字不会乱码。Console.OutputEncodingEncoding.UTF8;// 这里使用两组很小的频谱数据既能看清逐元素乘法又能明显看出 conjBtrue 之后的变化。varspectrumADatanewdouble[,]{{10.0,0.0},{-2.0,2.0},{-2.0,0.0},{-2.0,-2.0},};varspectrumBDatanewdouble[,]{{1.0,0.0},{0.0,-1.0},{-1.0,0.0},{0.0,1.0},};varexpectedProductnewdouble[,]{{10.0,0.0},{2.0,2.0},{2.0,0.0},{2.0,-2.0},};varexpectedConjugatedProductnewdouble[,]{{10.0,0.0},{-2.0,-2.0},{2.0,0.0},{-2.0,2.0},};usingvarspectrumACreateComplexRowVector(spectrumAData);usingvarspectrumBCreateComplexRowVector(spectrumBData);usingvarproductnewMat();usingvarconjugatedProductnewMat();// 第一次调用conjBfalse做普通逐元素乘法。Cv2.MulSpectrums(spectrumA,spectrumB,product,DftFlags.None,false);// 第二次调用conjBtrue先对第二个频谱取共轭再做逐元素乘法。Cv2.MulSpectrums(spectrumA,spectrumB,conjugatedProduct,DftFlags.None,true);varactualProductReadComplexMatrix(product);varactualConjugatedProductReadComplexMatrix(conjugatedProduct);PrintComplexMatrixComparison(A[k] * B[k],actualProduct,expectedProduct);PrintComplexMatrixComparison(A[k] * conj(B[k]),actualConjugatedProduct,expectedConjugatedProduct);}/// summary/// 把二维复数数组包装成 1 行 N 列的复数矩阵。/// /summaryprivatestaticMatCreateComplexRowVector(double[,]complexPairs){// 每一行只存一个复数因此这里只需要一行 N 列。varvectornewMat(1,complexPairs.GetLength(0),MatType.CV_64FC2);for(varindex0;indexcomplexPairs.GetLength(0);index){// Vec2d 的 Item0 是实部Item1 是虚部。vector.AtVec2d(0,index)newVec2d(complexPairs[index,0],complexPairs[index,1]);}returnvector;}/// summary/// 从 Mat 中读取复数矩阵并展开成 [实部, 虚部] 两列的形式。/// /summaryprivatestaticdouble[,]ReadComplexMatrix(Matsource){usingvarconvertednewMat();source.ConvertTo(converted,MatType.CV_64FC2);varresultnewdouble[converted.Rows*converted.Cols,2];varindex0;for(varrow0;rowconverted.Rows;row){for(varcol0;colconverted.Cols;col){varvalueconverted.AtVec2d(row,col);result[index,0]value.Item0;result[index,1]value.Item1;index;}}returnresult;}/// summary/// 打印复数矩阵把每一行当成一个复数显示。/// /summaryprivatestaticvoidPrintComplexMatrix(stringtitle,double[,]matrix){Console.WriteLine(title);for(varrow0;rowmatrix.GetLength(0);row){varrealTextmatrix[row,0].ToString(F6,CultureInfo.InvariantCulture);varimagValuematrix[row,1];varsignimagValue0?:-;varimagTextMath.Abs(imagValue).ToString(F6,CultureInfo.InvariantCulture);Console.WriteLine($[{row}]{realText}{sign}{imagText}i);}Console.WriteLine();}/// summary/// 打印复数矩阵对照结果并计算最大绝对误差。/// /summaryprivatestaticvoidPrintComplexMatrixComparison(stringtitle,double[,]actual,double[,]expected){PrintComplexMatrix(${title}- 实际值,actual);PrintComplexMatrix(${title}- 期望值,expected);Console.WriteLine($最大绝对误差{ComputeMaxAbsDiff(actual,expected):F12});Console.WriteLine();}/// summary/// 计算两个矩阵的最大绝对误差。/// /summaryprivatestaticdoubleComputeMaxAbsDiff(double[,]left,double[,]right){varmax0.0;for(varrow0;rowleft.GetLength(0);row){for(varcol0;colleft.GetLength(1);col){vardiffMath.Abs(left[row,col]-right[row,col]);if(diffmax){maxdiff;}}}returnmax;}}8. 运行结果解读如果你运行上面的示例应该能看到下面几个结论conjBfalse时结果就是两个频谱的普通逐元素乘法。conjBtrue时第二个频谱先取共轭因此虚部符号会反向。这说明相关和卷积在频域里的处理方式并不完全一样。9. 常见错误与排查把MulSpectrums当成变换函数忘记它本身不做 DFT。忘记两个输入必须同尺寸、同类型。搞混conjBtrue和conjBfalse的物理意义。把 CCS-packed 频谱和完整复数频谱混着算导致结果难以解释。10. 进阶说明MulSpectrums在工程里最常见的用法是和Dft、Idft连在一起conv(x,y)F−1(F(x)⋅F(y)) \text{conv}(x, y) \mathcal{F}^{-1}(\mathcal{F}(x) \cdot \mathcal{F}(y))conv(x,y)F−1(F(x)⋅F(y))corr(x,y)F−1(F(x)⋅F(y)‾) \text{corr}(x, y) \mathcal{F}^{-1}(\mathcal{F}(x) \cdot \overline{\mathcal{F}(y)})corr(x,y)F−1(F(x)⋅F(y))如果输入是多行独立频谱才会用到DftFlags.Rows。普通的单行示例里这个标志通常不需要。11. 总结Cv2.MulSpectrums的核心职责就是在频域里把两个频谱逐元素乘起来。只要你记住conjB会影响第二个输入的虚部符号再把它和Dft、Idft连起来看卷积和相关就会变得很直观。