UIImageView:如何在平移和缩放后仅捕获可见内容

Posted

技术标签:

【中文标题】UIImageView:如何在平移和缩放后仅捕获可见内容【英文标题】:UIImageView: How to capture just visible content after Pan & Zoom 【发布时间】:2020-09-21 15:49:43 【问题描述】:

我有一个 UIImageView,它由 ImagePicker 加载,照片(3k x 4k 大小)使用 AspectFit 模式填充到视图中。然后我使用 GestureRecognizers 平移和缩放图像,它调整视图的变换比例和平移。这很好用,代码如下:

@objc private func startZooming(_ sender: UIPinchGestureRecognizer) 
  let scaleResult = sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale)
  guard let scale = scaleResult, scale.a > 1, scale.d > 1 else  return 
  sender.view?.transform = scale
  sender.scale = 1

  
@objc private func startPanning(_ sender: UIPanGestureRecognizer) 
    let translate = sender.translation(in: mainImageView)
    let center = mainImageView.center
    mainImageView.center = CGPoint(x: center.x + translate.x, y: center.y + translate.y)
    self.currImageTranslation.x += translate.x
    self.currImageTranslation.y += translate.y
    sender.setTranslation(CGPoint(x: 0, y: 0), in: mainImageView)

我的问题是我需要将转换的视觉结果作为 CGImage 输入视觉推理引擎。当我从 UIImageView 获取图像时:

let image = myUIImageView.image // I get the original untransformed image

当我尝试时:

UIGraphicsBeginImageContextWithOptions(image!.size, false, 0.0)
  myImageView.layer.render(in: UIGraphicsGetCurrentContext()!)
  let newImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()

newImage 也只是原始图像(未转换)

当我尝试时:

let rect = mainImageView.bounds
let scale = UIScreen.main.scale
var t = self.currImageTranslation
let tScale = mainImageView.transform.a
let zoomOffsetFactorX = ((t.x /  rect.size.width) * tScale)
let zoomOffsetFactorY = ((t.y / rect.size.height) * tScale)
t.x = t.x - zoomOffsetFactorX
t.y = (t.y + 20.0) + zoomOffsetFactorY

// create a context
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
let transform = mainImageView.transform
let imrect = CGRect(origin: t, size: rect.size)
context.concatenate(transform)
let tempImage = mainImageView.image
tempImage!.draw(in: imrect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

newImage 可以正确转换,但仅在 scale = 1.0 时,无论是否缩放都会创建分数图像,即剪辑到零、零原点。

我想生成用户在该 ImageView 屏幕上看到的内容的 UIImage。有人可以帮忙吗?

在@DonMag 的慷慨建议下,我尝试了以下方法:

func enableZoom() 
  let pinchGesture = UIPinchGestureRecognizer(target: self, 
                action: #selector(startZooming(_:)))
  let panGesture = UIPanGestureRecognizer(target: self, 
                action: #selector(startPanning(_:)))
    containerView.isUserInteractionEnabled = true
    containerView.addGestureRecognizer(pinchGesture)
    containerView.addGestureRecognizer(panGesture)



@objc private func startZooming(_ sender: UIPinchGestureRecognizer) 
  let scaleResult = sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale)
  guard let scale = scaleResult, scale.a > 1, scale.d > 1 else  return 
  sender.view?.transform = scale
  newImage()
  sender.scale = 1

  
@objc private func startPanning(_ sender: UIPanGestureRecognizer) 
    let translate = sender.translation(in: mainImageView)
    let center = mainImageView.center
    mainImageView.center = CGPoint(x: center.x + translate.x, y: center.y + translate.y)
    self.currImageTranslation.x += translate.x
    self.currImageTranslation.y += translate.y
    sender.setTranslation(CGPoint(x: 0, y: 0), in: mainImageView)
    newImage()


//MARK:- create cropped/panned image and infer
func newImage() -> Void 
    
    let format = UIGraphicsImageRendererFormat()
    // we want a 1:1 points-to-pixels output
    format.scale = 1
    let renderer = UIGraphicsImageRenderer(size: containerView.bounds.size, format: format)
    let image = renderer.image  ctx in
        containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
    
    self.currCGImage = image.cgImage
    //repeatInference()   // runs only once every 0.3 s
    return

  

现在“newImage”正确地反映了 panGesture 创建的翻译,但似乎忽略了 pinchGesture 的比例变化。 drawHierarchy 似乎忽略了 UIView 中强加的变换。我哪里错了?

【问题讨论】:

澄清...你想要原始图像大小吗?还是屏幕尺寸?因此,例如,如果您的滚动视图在 @2x 设备上为 200x200 pts,并且您正在缩放显示 3k x 4k 图像的 1600x1600 部分...您是否希望结果是 1600x1600 矩形缩小到400x400 像素的图像?或者您想从原始图像中剪裁出 1600x1600 像素 矩形? 感谢您的回复!为了尝试澄清,我没有使用滚动和播放内容大小。只需对图像视图中的 UIImage 进行转换/翻译。 uiimageview 在 plus 手机上约为 440x500,这是我希望返回的尺寸。本质上就像视图的位图。推理引擎将根据需要进行缩放/裁剪。 ***.com/a/66396409/2033377 这个答案有部分答案。肯定没有简短的答案。 【参考方案1】:

我不完全确定你在用你的转换代码做什么,但这可能对你有用——而且更简单......

如果我理解正确,你有一个UIImageView.aspectFit,它在一个UIView“容器”中,你应用一个比例并将转换转换到图像视图:

    let format = UIGraphicsImageRendererFormat()
    // we want a 1:1 points-to-pixels output
    format.scale = 1
    let renderer = UIGraphicsImageRenderer(size: containerView.bounds.size, format: format)
    let image = renderer.image  ctx in
        containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
    

这是一个完整的示例实现:

class TransImageViewController: UIViewController 

    let containerView = UIView()
    let imageView = UIImageView()

    override func viewDidLoad() 
        super.viewDidLoad()
        
        guard let img = UIImage(named: "bkg_2400x1600") else 
            fatalError("Could not load the image!!!")
        
        
        let stack = UIStackView()
        stack.spacing = 20
        stack.distribution = .fillEqually
        
        let b1 = UIButton()
        let b2 = UIButton()
        
        [b1, b2].forEach  b in
            b.backgroundColor = .red
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.gray, for: .highlighted)
        
        
        b1.setTitle("Transform", for: [])
        b2.setTitle("Capture", for: [])
        
        stack.addArrangedSubview(b1)
        stack.addArrangedSubview(b2)

        [stack, containerView, imageView].forEach 
            $0.translatesAutoresizingMaskIntoConstraints = false
        
        
        view.addSubview(stack)
        view.addSubview(containerView)
        containerView.addSubview(imageView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            // buttons stack at top
            stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            // container 400x500 centered
            containerView.widthAnchor.constraint(equalToConstant: 400),
            containerView.heightAnchor.constraint(equalToConstant: 500),
            containerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            containerView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            // imageView constrained all 4 sides to container
            imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
            imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),

        ])
        
        containerView.clipsToBounds = true
        imageView.contentMode = .scaleAspectFit
        imageView.image = img
        
        view.backgroundColor = .blue
        containerView.backgroundColor = .yellow
        imageView.backgroundColor = .orange

        b1.addTarget(self, action: #selector(self.doTransform), for: .touchUpInside)
        b2.addTarget(self, action: #selector(self.grabImage), for: .touchUpInside)
    

    @objc func doTransform() -> Void 
        var t = CGAffineTransform.identity
        t = t.scaledBy(x: 4.0, y: 4.0)
        t = t.translatedBy(x: -120, y: 40)
        imageView.transform = t
    
    
    @objc func grabImage() -> Void 

        let format = UIGraphicsImageRendererFormat()
        // we want a 1:1 points-to-pixels output
        format.scale = 1
        let renderer = UIGraphicsImageRenderer(size: containerView.bounds.size, format: format)
        let image = renderer.image  ctx in
            containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
        
        
        // do what you want with the resulting image
        print("Resulting image size:", image.size)

    


将此图片用作我的“bkg_2400x1600”图片资源:

上面的代码是这样开始的(我的“容器”视图是 400x500 pts):

点击“转换”按钮将应用.scaledBy(x: 4.0, y: 4.0).translatedBy(x: -120, y: 40)

然后点击“Capture”给我这个 400x500 像素的图像:

【讨论】:

看起来完美!将在几个小时内对其进行测试,并很快将其标记为正确。非常感谢! 我修改了我的 ViewController 以将目标图像视图放置在容器 UIView 中,并将 userInteractions/GestureRecognizers 设置为容器 UIView。抓取的图像现在按预期在帧中移动,但是,捏合手势(即缩放或放大容器)在屏幕上确实发生了变化,但不会改变抓取的图像。抓取的图像不会放大。我正在修改 OP 以包含当前的相关代码。【参考方案2】:

@DonMag 让我走上了正轨。但是,为了在抓取的图像中获得正确的缩放,我需要将 scaleBy 转换应用于 UIImageView,而不是容器视图。此外,我需要将格式比例设置为 pinchGesture 中的当前比例因子。这是工作版本:

    func enableZoom() 
      let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(startZooming(_:)))
      let panGesture = UIPanGestureRecognizer(target: self, action: #selector(startPanning(_:)))
        containerView.isUserInteractionEnabled = true
        containerView.addGestureRecognizer(pinchGesture)
        containerView.addGestureRecognizer(panGesture)
    

    @objc private func startZooming(_ sender: UIPinchGestureRecognizer) 
//        let scaleResult = sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale)
//        guard let scale = scaleResult, scale.a > 1, scale.d > 1 else  return 
//        sender.view?.transform = scale
      let scaleResult = mainImageView?.transform.scaledBy(x: sender.scale, y: sender.scale)
      guard let scale = scaleResult, scale.a > 1, scale.d > 1 else  return 
      mainImageView.transform = scale
      currImageScale = sender.scale
      newImage()
      sender.scale = 1
    
      
    @objc private func startPanning(_ sender: UIPanGestureRecognizer) 
        let translate = sender.translation(in: mainImageView)
        let center = mainImageView.center
        mainImageView.center = CGPoint(x: center.x + translate.x, y: center.y + translate.y)
        self.currImageTranslation.x += translate.x
        self.currImageTranslation.y += translate.y
        sender.setTranslation(CGPoint(x: 0, y: 0), in: mainImageView)
        newImage()
    
    
    //MARK:- create cropped/panned image and infer
    func newImage() -> Void 
        
        let format = UIGraphicsImageRendererFormat()
        // we want a 1:1 points-to-pixels output
        format.scale = currImageScale
        let renderer = UIGraphicsImageRenderer(size: containerView.bounds.size, format: format)
        let image = renderer.image  ctx in
            containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
        
        self.currCGImage = image.cgImage
        repeatInference()   // runs only once every 0.3 s
        return
    
    
    

【讨论】:

以上是关于UIImageView:如何在平移和缩放后仅捕获可见内容的主要内容,如果未能解决你的问题,请参考以下文章

UIImageView 上的捏合、平移和双击手势

如何在不缩放图像子视图的情况下缩放图像?迅速的ios

如何在 Scrollview 中捕获缩放的 UIImageView 以在 Android 中进行裁剪?

UIScrollView 中的 UIImageView:平移失败

以全分辨率从 UIScrollView 中捕获缩放的 UIImageView

UIScrollView 缩放:将平移限制为 imageView.image 边界