如何在使用变换(缩放、旋转和平移)时合并UIImages?

12

我正在尝试复制Instagram的一个功能,即在您的照片上添加贴纸(其他图片),然后保存。

因此,在包含我的照片的UIImageView上,我将贴纸(另一个UIImageView)作为子视图添加到其中,并将其定位在父UIImageView的中心位置。

要移动贴纸,我使用CGAffineTransform(不是移动UIImageView的中心)。我还应用了旋转和缩放的CGAffineTransform来旋转和缩放贴纸。

要保存带有贴纸的图片,我使用以下CGContext:

    extension UIImage {
        func merge2(in rect: CGRect, with imageTuples: [(image: UIImage, viewSize: CGSize, transform: CGAffineTransform)]) -> UIImage? {
            UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)

            guard let context = UIGraphicsGetCurrentContext() else { return nil }

            draw(in: CGRect(size: size), blendMode: .normal, alpha: 1)

            // Those multiplicators are used to properly scale the transform of each sub image as the parent image (self) might be bigger than its view bounds, same goes for the subviews
            let xMultiplicator = size.width / rect.width
            let yMultiplicator = size.height / rect.height

            for imageTuple in imageTuples {
                let size = CGSize(width: imageTuple.viewSize.width * xMultiplicator, height: imageTuple.viewSize.height * yMultiplicator)
                let center = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
                let areaRect = CGRect(center: center, size: size)

                context.saveGState()

                let transform = imageTuple.transform
                context.translateBy(x: center.x, y: center.y)
                context.concatenate(transform)
                context.translateBy(x: -center.x, y: -center.y)

// EDITED CODE
                context.setBlendMode(.color)
                UIColor.subPink.setFill()
                context.fill(areaRect)
// EDITED CODE
                imageTuple.image.draw(in: areaRect, blendMode: .normal, alpha: 1)

                context.restoreGState()
            }

            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()

            return image
        }
    }

在变换中考虑旋转。

在变换中考虑缩放。

但是,变换中的平移似乎不起作用(存在微小的平移,但它并不能反映真实的平移)。

很明显我漏掉了什么,但找不到是什么。

有什么想法吗?

编辑:

这里有一些应用程序中贴纸的截图以及库中保存的最终图像。

如您所见,最终图像的旋转和比例(宽度/高度比)与应用程序中的图像相同。

包含UIImageUIImageView具有与其图像相同的比例。

在绘制贴纸时,我还添加了一个背景,以清楚地看到实际图像的边界。

没有旋转或缩放:

enter image description here enter image description here

旋转和缩放:

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

编辑2:

这里是一个测试项目,重现了上述描述的行为。


4
你能发布一张你想要实现的效果的图片吗? - Jakub
我测试了这段代码并且翻译是正确的,所以不清楚在你的情况下出了什么问题。或者说,你的期望和结果之间不太清楚。 - Ken Boreham
抱歉回复晚了。@Kuba我已经更新了问题并附上了截图。 - Nico
@KenBoreham请查看屏幕截图。 - Nico
2个回答

7
问题在于你有多个几何图形(坐标系)、比例因素和参考点在使用,很难保持它们的一致性。你有根视图的几何图形,在其中定义了图像视图和贴纸视图的框架,然后你有图形上下文的几何图形,但你没有使它们匹配。图像视图的原点不在其父视图的几何图形的原点处,因为你将其限制在安全区域内,并且我不确定你是否正确地补偿了该偏移量。你试图通过调整绘制贴纸时的贴纸大小来处理图像在图像视图中的缩放。你没有正确补偿这样一个事实,即贴纸的 center 属性和其 transform 影响其姿态(位置/缩放/旋转)。

让我们简化一下。

首先,让我们引入一个“画布视图”作为图像视图的父视图。可以按照您想要的方式对画布视图进行布局以与安全区域相关联。我们将约束图像视图以填充画布视图,因此图像视图的原点将为 .zero

new view hierarchy

接下来,我们将设置贴纸视图的layer.anchorPoint.zero。这使得视图的transform相对于贴纸视图的左上角进行操作,而不是其中心。它还使view.layer.position(与view.center相同)控制视图左上角的位置,而不是控制视图中心的位置。我们希望进行这些更改,因为它们与Core Graphics在合并图像时绘制areaRect的方式匹配。
我们还将view.layer.position设置为.zero。这简化了我们计算何处绘制贴纸图像以合并图像的方式。
private func makeStickerView(with image: UIImage, center: CGPoint) -> UIImageView {
    let heightOnWidthRatio = image.size.height / image.size.width
    let imageWidth: CGFloat = 150

    //      let newStickerImageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageWidth * heightOnWidthRatio)))
    let view = UIImageView(frame: CGRect(x: 0, y: 0, width: imageWidth, height: imageWidth * heightOnWidthRatio))
    view.image = image
    view.clipsToBounds = true
    view.contentMode = .scaleAspectFit
    view.isUserInteractionEnabled = true
    view.backgroundColor = UIColor.red.withAlphaComponent(0.7)
    view.layer.anchorPoint = .zero
    view.layer.position = .zero
    return view
}

这意味着我们需要完全使用贴纸的transform来定位,因此我们希望初始化transform以使贴纸居中:
@IBAction func resetPose(_ sender: Any) {
    let center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
    let size = stickerView.bounds.size
    stickerView.transform = .init(translationX: center.x - size.width / 2, y: center.y - size.height / 2)
}

由于这些变化,我们需要更复杂地处理捏和旋转。我们将使用辅助方法来管理复杂性:
extension CGAffineTransform {
    func around(_ locus: CGPoint, do body: (CGAffineTransform) -> (CGAffineTransform)) -> CGAffineTransform {
        var transform = self.translatedBy(x: locus.x, y: locus.y)
        transform = body(transform)
        transform = transform.translatedBy(x: -locus.x, y: -locus.y)
        return transform
    }
}

然后我们像这样处理捏和旋转:
@objc private func stickerDidPinch(pincher: UIPinchGestureRecognizer) {
    guard let stickerView = pincher.view else { return }
    stickerView.transform = stickerView.transform.around(pincher.location(in: stickerView), do: { $0.scaledBy(x: pincher.scale, y: pincher.scale) })
    pincher.scale = 1
}

@objc private func stickerDidRotate(rotater: UIRotationGestureRecognizer) {
    guard let stickerView = rotater.view else { return }
    stickerView.transform = stickerView.transform.around(rotater.location(in: stickerView), do: { $0.rotated(by: rotater.rotation) })
    rotater.rotation = 0
}

这也使得缩放和旋转比以前更好地工作。在你的代码中,缩放和旋转总是发生在视图的中心。使用这段代码,它们会在用户手指之间的中心点周围发生,感觉更自然。
最后,为了合并图像,我们将从缩放图形上下文的几何结构开始,方式与 imageView 缩放其图像相同,因为贴纸变换相对于 imageView 的大小而不是图像大小。由于我们现在完全使用变换来定位贴纸,并且由于我们已将图像视图和贴纸视图原点设置为 .zero,因此我们不需要进行任何奇怪的原点调整。
extension UIImage {

    func merge(in viewSize: CGSize, with imageTuples: [(image: UIImage, viewSize: CGSize, transform: CGAffineTransform)]) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)

        print("scale : \(UIScreen.main.scale)")
        print("size : \(size)")
        print("--------------------------------------")

        guard let context = UIGraphicsGetCurrentContext() else { return nil }

        // Scale the context geometry to match the size of the image view that displayed me, because that's what all the transforms are relative to.
        context.scaleBy(x: size.width / viewSize.width, y: size.height / viewSize.height)

        draw(in: CGRect(origin: .zero, size: viewSize), blendMode: .normal, alpha: 1)

        for imageTuple in imageTuples {
            let areaRect = CGRect(origin: .zero, size: imageTuple.viewSize)

            context.saveGState()
            context.concatenate(imageTuple.transform)

            context.setBlendMode(.color)
            UIColor.purple.withAlphaComponent(0.5).setFill()
            context.fill(areaRect)

            imageTuple.image.draw(in: areaRect, blendMode: .normal, alpha: 1)

            context.restoreGState()
        }

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image
    }
}

你可以在这里找到我修复后的测试项目。


嗨@rob mayoff,我正在使用您的代码,它运行得非常完美。但是由于锚点设置为.zero,当我编写一个使用矩阵变换旋转UIView的函数时,它将从左上角作为中心旋转。有没有方法解决这个问题?如果我将锚点设置为CGPoint(0.5,0.5),那么保存后会使stickerView错位。 - donmj
1
你所描述的问题听起来就像我在这个答案中编写CGAffineTransform.around方法的全部原因。 - rob mayoff
1
嘿@rob mayoff,我使用了您的示例来实现它,但是我遇到了一个问题,当主图像内容模式更改为.scaleAspectFit时,它无法正常工作并在另一个位置绘制。 - mohsen
谢谢@robmayoff,你救了我的一天。我怎么才能限制顶部图像在底部图像上的平移/缩放区域? - Emre Değirmenci
在你的 UIGestureRecognizerDelegate 中实现 gestureRecognizer(_:shouldReceive:) - rob mayoff
显示剩余2条评论

0

我认为CGAffineTransform属性(比例、旋转和平移)每次只能作用于一个view。 因此,当你说你正在尝试实现所有三个变换属性时,我相信你只是在两个UIImageViews上尝试(一个用于旋转,另一个用于缩放) 再添加一个UIImageView来实现平移属性。

希望这可以帮到你。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接