Kotlin Bytedeco OpenCV 图像图像58 无缝克隆
- [0 原理解释](#0 原理解释)
-
- 核心思想
- 数学原理(泊松方程)
- [seamlessClone 的几种模式](#seamlessClone 的几种模式)
- 优缺点
- 和普通直接copy对比
- 简单伪代码逻辑
- 常见踩坑
- [1 添加依赖](#1 添加依赖)
-
- [1.1 build.gradle.kts](#1.1 build.gradle.kts)
- [1.2 settings.gradle.kts](#1.2 settings.gradle.kts)
- [2 测试代码](#2 测试代码)
- [3 测试结果](#3 测试结果)
0 原理解释
OpenCV 的 cv2.seamlessClone() 就是实现泊松图像编辑(Poisson Image Editing) ,核心目标:把一张小图(前景源图)粘贴到目标背景图上,消除拼接边界,融合过渡自然,看不到硬缝合线。
普通拷贝:直接把源图像素覆盖到背景,边界会有明显一圈硬边、色彩断层。
无缝克隆:不直接拷贝像素颜色,拷贝的是图像梯度(变化),求解泊松方程重建像素值。
核心思想
图像可以看作二维函数 (f(x,y))
- 梯度 ∇f:像素灰度/颜色的变化(边缘、纹理、明暗变化)
- 泊松编辑:在粘贴的ROI区域内部,尽量保留源图像的梯度纹理;在区域边界,强制和背景图像素保持一致。
通俗一句话:内部用源图的纹理变化,边界接上背景图的颜色,求解出区域内每个点最终的像素值。
两个关键输入
- 源图 src:要贴过去的物体
- 掩码 mask:标记哪些像素是要粘贴的物体(白色=前景,黑色忽略)
- 目标图 dst:背景大图
- 中心点:源物体放在背景图上的中心坐标
数学原理(泊松方程)
在粘贴区域Ω内求解泊松偏微分方程:
\\Delta f = \\Delta f\^\* \\quad \\text{区域}\\ \\Omega \\text{内部}
f = f_{background} \\quad \\text{在区域边界}\\ \\partial\\Omega
- (\Delta):拉普拉斯算子,本质就是梯度的散度,代表图像二阶差分(纹理变化)
- (f^):源图像;(\Delta f^) 就是源图的拉普拉斯(源图纹理信息)
- 边界条件 Dirichlet:粘贴区域的边缘像素,直接等于背景图像素
含义拆解:
- 在粘贴的物体内部:让合成图像的拉普拉斯 = 源图像拉普拉斯 → 保留源物体纹理、明暗、细节
- 在物体的轮廓边界:像素值强制等于背景,消除硬边界断层
把连续微分方程离散化,变成大型稀疏线性方程组 Ax=b,解方程组得到每个点新的R/G/B像素(三通道分别求解),得到融合后的图片。
OpenCV内部就是求解这套稀疏线性方程组,这就是无缝克隆的计算本质。
seamlessClone 的几种模式
python
cv2.seamlessClone(src, dst, mask, center, flags)
cv2.NORMAL_CLONE普通泊松克隆(最常用):上面标准泊松方程,复制源梯度,边界贴合背景cv2.MIXED_CLONE混合克隆:源和背景梯度做混合;物体和背景光照差异大时效果更好cv2.MONOCHROME_TRANSFER:只迁移纹理梯度,丢弃源图颜色;只把纹理贴到背景上。
优缺点
✅ 优点:
- 物体轮廓边界过渡自然,消除硬拼接缝
- 物体内部纹理细节完整保留
- 自动适配背景的亮度、色调
❌ 缺点:
- 非常依赖mask掩码质量:mask轮廓不准,融合就会有伪影、晕边;掩码要稍微比物体轮廓大一圈
- 大面积区域求解线性方程组,大mask会慢
- 如果源图和背景光照差异极端,会出现色彩偏移、发灰;此时改用 MIXED_CLONE
- 只融合边界附近,物体本身大的颜色差异无法完全修复
和普通直接copy对比
- 直接拷贝:
dst[roi] = src[roi]→ 复制像素值,边界生硬 - 泊松无缝克隆:复制梯度(纹理),边界约束背景像素,解方程重建颜色。不是简单像素拷贝。
简单伪代码逻辑
- 根据mask得到待融合区域Ω
- 对每个颜色通道R/G/B,构建稀疏线性方程组
- Ω内部:约束等于源图拉普拉斯
- Ω边界:约束等于背景图像素
- 求解方程组得到每个点新像素
- 把解出来的像素回填到背景图对应位置输出
常见踩坑
- mask必须是和src一样尺寸的单通道图,前景物体是白色(255),不是作用在dst上;很多人mask尺寸搞错导致完全不对。
- mask轮廓尽量紧贴物体,可以稍微外扩1~2像素,不要留有黑边缝隙。
- center坐标是目标图上的中心点,不是左上角。
1 添加依赖
1.1 build.gradle.kts
kotlin
plugins {
kotlin("jvm") version "2.4.0"
application
}
group = "com.xuhyac"
version = "1.0.0"
repositories {
maven("https://maven.aliyun.com/repository/public")
mavenCentral()
}
dependencies {
implementation("org.bytedeco:opencv:4.13.0-1.5.13")
runtimeOnly("org.bytedeco:opencv:4.13.0-1.5.13:windows-x86_64")
runtimeOnly("org.bytedeco:openblas:0.3.31-1.5.13:windows-x86_64")
testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain(25)
}
application {
mainClass.set(providers.gradleProperty("mainClass").orElse("com.xuhyac.MainKt"))
applicationDefaultJvmArgs = listOf(
"--enable-native-access=ALL-UNNAMED",
"-Dstdout.encoding=UTF-8",
"-Dstderr.encoding=UTF-8",
)
}
tasks.test {
useJUnitPlatform()
}
1.2 settings.gradle.kts
kotlin
pluginManagement {
repositories {
maven("https://maven.aliyun.com/repository/gradle-plugin")
maven("https://maven.aliyun.com/repository/public")
gradlePluginPortal()
mavenCentral()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
rootProject.name = "opencv"
2 测试代码
kotlin
package com.xuhyac.photo
import com.xuhyac.common.DemoSupport.materializeResource
import org.bytedeco.opencv.global.opencv_core.CV_8UC1
import org.bytedeco.opencv.global.opencv_core.CV_8UC3
import org.bytedeco.opencv.global.opencv_highgui
import org.bytedeco.opencv.global.opencv_imgcodecs.IMREAD_COLOR
import org.bytedeco.opencv.global.opencv_imgcodecs.imread
import org.bytedeco.opencv.global.opencv_imgproc.*
import org.bytedeco.opencv.global.opencv_photo.NORMAL_CLONE
import org.bytedeco.opencv.global.opencv_photo.seamlessClone
import org.bytedeco.opencv.opencv_core.Mat
import org.bytedeco.opencv.opencv_core.Point
import org.bytedeco.opencv.opencv_core.Scalar
import kotlin.io.path.absolutePathString
/**
* 使用泊松图像编辑把前景内容无缝克隆到另一张图中。
*
* 普通 Alpha 混合直接组合像素值,前景与背景亮度不一致时容易出现明显边界。`seamlessClone` 在掩膜区域
* 内求解泊松方程,尽量保留源图的梯度,同时让边界颜色服从目标图,因此能自然适配目标环境的光照。
* `NORMAL_CLONE` 保留源图整体梯度;需要更多目标纹理时可以尝试 `MIXED_CLONE`。
*
* 示例程序生成一个带文字和环形边缘的前景,用圆形掩膜将其克隆到 OpenCV 官方篮球图片中央。源图必须
* 与掩膜同尺寸,克隆中心还应确保整个源图范围不会超出目标图。
*
*/
fun main() {
val path = materializeResource("opencv/images/basketball1.png")
val destination = imread(path.absolutePathString(), IMREAD_COLOR)
val sourceSize = 180
val source = Mat(sourceSize, sourceSize, CV_8UC3, Scalar(35.0, 150.0, 235.0, 0.0))
val mask = Mat.zeros(sourceSize, sourceSize, CV_8UC1).asMat()
val center = Point(sourceSize / 2, sourceSize / 2)
circle(source, center, 76, Scalar(230.0, 80.0, 35.0, 0.0), 12, LINE_AA, 0)
circle(source, center, 58, Scalar(50.0, 205.0, 90.0, 0.0), -1, LINE_AA, 0)
putText(source, "CV", Point(42, 108), FONT_HERSHEY_DUPLEX, 1.8, Scalar(245.0, 245.0, 245.0, 0.0), 4, LINE_AA, false)
circle(mask, center, 83, Scalar(255.0, 255.0, 255.0, 0.0), -1, LINE_AA, 0)
val cloneCenter = Point(destination.cols() / 2, destination.rows() / 2)
val blended = Mat()
seamlessClone(source, destination, mask, cloneCenter, blended, NORMAL_CLONE)
opencv_highgui.imshow("src", source)
opencv_highgui.imshow("mask", mask)
opencv_highgui.imshow("destination", destination)
opencv_highgui.imshow("blended", blended)
opencv_highgui.waitKey(0)
println("已在目标坐标 (${cloneCenter.x()}, ${cloneCenter.y()}) 完成 NORMAL_CLONE")
}
3 测试结果
