iOS下OpenCV Objective-C++鼻子缩放仅改裁剪区域而非直接调面部问题
iOS Objective-C++结合OpenCV实现自然鼻子缩放的解决方案
问题现状
我在iOS平台使用Objective-C++结合OpenCV开发面部鼻子缩放功能,目标是实现鼻子的自然拉长/缩短,但当前代码仅裁剪鼻子区域修改尺寸后叠加回原图,效果生硬(要么覆盖下方面部,要么留下空白),无法达到自然变形的效果。
当前代码片段:
+ (UIImage *)makeNoseLongerInImage:(UIImage *)image withFactor:(int)scaleFactor { cv::Mat mat; [image convertToMat:&mat :false]; NSString *path = [[NSBundle mainBundle] pathForResource:@"haarcascade_frontalface_default" ofType:@"xml"]; cv::CascadeClassifier face_cascade; if (!face_cascade.load([path UTF8String])) { NSLog(@"Failed to load face cascade from path: %@", path); return image; } std::vector<cv::Rect> faces; face_cascade.detectMultiScale(mat, faces, 1.1, 2, 0 | cv::CASCADE_SCALE_IMAGE, cv::Size(30, 30)); for (const cv::Rect& face : faces) { cv::Rect noseArea( std::max(0, face.x + static_cast<int>(face.width * 0.25)), std::max(0, face.y + static_cast<int>(face.height * 0.60)), std::min(mat.cols - face.x, static_cast<int>(face.width * 0.50)), std::min(mat.rows - face.y, static_cast<int>(face.height * 0.12)) ); if (noseArea.width == 0 || noseArea.height == 0) { NSLog(@"nose area computed with zero width or height."); continue; } cv::Mat noseRegion = mat(noseArea); int newHeight = std::max(1, static_cast<int>(noseRegion.rows * scaleFactor / 100.0)); cv::Mat resizednoseRegion; cv::resize(noseRegion, resizednoseRegion, cv::Size(noseRegion.cols, newHeight), 0, 0, cv::INTER_LINEAR); NSLog(@"Original nose region: (%d, %d), Resized: (%d, %d)", noseRegion.cols, noseRegion.rows, resizednoseRegion.cols, resizednoseRegion.rows); // Place the resized nose region back into the original mat if (!resizednoseRegion.empty() && resizednoseRegion.cols == noseRegion.cols && resizednoseRegion.rows == newHeight) { // Update the noseArea to match the resized dimensions cv::Rect newnoseArea = noseArea; newnoseArea.height = newHeight; // Update height to match resized region resizednoseRegion.copyTo(mat(newnoseArea)); } else { NSLog(@"Mismatch in dimensions after resize or empty resized region."); } } return MatToUIImage(mat); }
效果对比
- 当前输出效果:

- 期望实现的效果:

解决方案
核心思路
放弃简单的裁剪替换,改用面部关键点驱动的局部图像变形,让鼻子及周围区域自然过渡,避免生硬的覆盖/空白。
具体步骤
- 替换面部检测方式:用OpenCV的Facemark模型(如LBF)替代Haar cascade,获取精准的面部关键点(尤其是鼻子区域的锚点:鼻尖、鼻翼、鼻底等)。
- 构建变形网格:基于鼻子关键点的原始位置和目标位置(拉长时下移鼻尖、调整鼻底;缩短时上移鼻尖),生成变形网格。
- 局部图像变形:使用Thin Plate Spline(TPS)或三角剖分仿射变换,对鼻子及周围小范围区域进行变形,保证面部整体连贯性。
修改后的代码示例(核心部分)
+ (UIImage *)makeNoseLongerInImage:(UIImage *)image withFactor:(float)scaleFactor { cv::Mat mat; [image convertToMat:&mat :false]; cv::Mat gray; cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY); // 加载面部检测器和关键点模型 NSString *faceCascadePath = [[NSBundle mainBundle] pathForResource:@"haarcascade_frontalface_default" ofType:@"xml"]; cv::CascadeClassifier faceCascade; if (!faceCascade.load([faceCascadePath UTF8String])) { NSLog(@"Failed to load face cascade"); return image; } NSString *facemarkPath = [[NSBundle mainBundle] pathForResource:@"lbfmodel.yaml" ofType:@"yaml"]; cv::Ptr<cv::face::Facemark> facemark = cv::face::FacemarkLBF::create(); if (!facemark->loadModel([facemarkPath UTF8String])) { NSLog(@"Failed to load facemark model"); return image; } // 检测面部 std::vector<cv::Rect> faces; faceCascade.detectMultiScale(gray, faces, 1.1, 2, 0 | cv::CASCADE_SCALE_IMAGE, cv::Size(30, 30)); std::vector<std::vector<cv::Point2f>> landmarks; if (!facemark->fit(mat, faces, landmarks)) { NSLog(@"Failed to detect landmarks"); return image; } for (size_t i = 0; i < faces.size(); i++) { std::vector<cv::Point2f> noseLandmarks; // 提取鼻子区域关键点(OpenCV Facemark LBF中27-35为鼻子关键点) for (int j = 27; j <= 35; j++) { noseLandmarks.push_back(landmarks[i][j]); } // 计算鼻子中心和变形偏移 cv::Point2f tip = noseLandmarks[6]; // 鼻尖点 cv::Point2f base = noseLandmarks[0]; // 鼻底中点 float noseLength = cv::norm(tip - base); float newLength = noseLength * scaleFactor; float offsetY = newLength - noseLength; // 构建目标关键点:鼻尖下移,鼻底保持,中间点线性过渡 std::vector<cv::Point2f> targetNoseLandmarks = noseLandmarks; targetNoseLandmarks[6].y += offsetY; // 移动鼻尖 // 中间点(28-34)按比例偏移 for (int j = 1; j < 6; j++) { float ratio = (float)j / 6.0; targetNoseLandmarks[j].y += offsetY * ratio; } // 准备全局关键点(加入面部其他锚点避免整体变形) std::vector<cv::Point2f> srcPoints, dstPoints; srcPoints.insert(srcPoints.end(), noseLandmarks.begin(), noseLandmarks.end()); dstPoints.insert(dstPoints.end(), targetNoseLandmarks.begin(), targetNoseLandmarks.end()); // 加入面部轮廓点增强稳定性(如下巴、眼角) srcPoints.push_back(landmarks[i][0]); // 左眼角 srcPoints.push_back(landmarks[i][16]); // 右眼角 srcPoints.push_back(landmarks[i][8]); // 下巴尖 dstPoints.push_back(landmarks[i][0]); dstPoints.push_back(landmarks[i][16]); dstPoints.push_back(landmarks[i][8]); // 生成TPS变形矩阵并应用 cv::Mat warpMat = cv::createThinPlateSplineShapeTransformer()->estimateTransformation(dstPoints, srcPoints); cv::Mat warpedMat; cv::warpPerspective(mat, warpedMat, warpMat, mat.size()); // 融合变形区域和原图(可选,增强自然度) cv::Mat mask = cv::Mat::zeros(mat.size(), CV_8UC1); cv::fillConvexPoly(mask, cv::Mat(noseLandmarks), cv::Scalar(255)); cv::dilate(mask, mask, cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(15,15))); warpedMat.copyTo(mat, mask); } return MatToUIImage(mat); }
注意事项
- 需要提前下载OpenCV的LBF关键点模型(
lbfmodel.yaml)放入项目资源。 - 调整
scaleFactor参数时,建议限制在0.7-1.5之间,避免过度变形导致面部失真。 - 可优化融合步骤,用渐变mask让变形区域和原图过渡更自然。
内容的提问来源于stack exchange,提问作者Diana
相关产品推荐
相关产品推荐

