0

After searching everywhere I found there is nothing specific source available to remove the background using bezierPath. Basically I am trying to achieve a similar feature like image cutout (You can check PicsArt >> Image editor >> CutOut). In this where user can draw any shape on the image and selected area can be highlighted and the rest of the part is removed.

Here is what I am using to draw the line on image

class DrawingImageView: UIImageView {
    var path = UIBezierPath()
    var previousTouchPoint = CGPoint.zero
    var shapeLayer = CAShapeLayer()
    var isClear: Bool = false {
        didSet {
            updateView()
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        setupView()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    func updateView() {
        self.shapeLayer.shadowOffset = .init(width: 1, height: 1)
        self.shapeLayer.shadowColor = UIColor.black.cgColor
        self.shapeLayer.shadowOpacity = 1

        self.shapeLayer.lineWidth = 20
        self.shapeLayer.lineCap = .round
        self.shapeLayer.strokeColor = isClear ? UIColor.clear.cgColor : UIColor.blue.cgColor
        self.shapeLayer.opacity = 0.3
        self.isUserInteractionEnabled = true
    }

    func setupView() {
        self.layer.addSublayer(shapeLayer)
        updateView()
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        if let location = touches.first?.location(in: self){
            previousTouchPoint = location
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)

        if let location = touches.first?.location(in: self){
            path.move(to: location)
            path.addLine(to: previousTouchPoint)
            previousTouchPoint = location
            shapeLayer.path = path.cgPath
        }
    }
}

To cut out a selected area I tried using cropping(to:) method of CGImage. but it's not working just clearing the entire image. You can check my out below

Plain Image Without selection

Selected area [Drawing] With Selection

Resultant image after using cropping(to:) Result

I am not sure if I am doing this correctly. I am open to other ways also.

Nick
  • 1,127
  • 2
  • 15
  • 40

1 Answers1

3

To "remove the background" you'll want to apply your shape layer as a mask.

Add this func to your DrawingImageView class:

func applyMask() -> Void {
    // set shape opacity to 1.0
    shapeLayer.opacity = 1.0
    // use it as a mask
    layer.mask = shapeLayer
}

Then, in your controller, add a button action to call that func. You should see your desired result:

enter image description here

enter image description here

Here's a complete working example... I also added a bool variable and this func in DrawingImageView to allow switching between "drawing" and "masked" mode, so I can go back and add more to the path.


Controller

class RemoveBackgroundViewController: UIViewController {
    
    var theDrawingView: DrawingImageView = DrawingImageView(frame: .zero)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let img = UIImage(named: "musk") else {
            fatalError("Could not load image!!!")
        }
        
        theDrawingView.image = img
        
        let btn = UIButton()
        btn.setTitle("Apply Mask", for: [])
        btn.setTitleColor(.white, for: .normal)
        btn.setTitleColor(.gray, for: .highlighted)
        btn.backgroundColor = .red
        
        [btn, theDrawingView].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview($0)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([

            // center the drawing image view, with 20-pts on each side
            //  sized proportionally to the loaded image
            theDrawingView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            theDrawingView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            theDrawingView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            theDrawingView.heightAnchor.constraint(equalTo: theDrawingView.widthAnchor, multiplier: img.size.height / img.size.width),
            
            // constrain button above the image
            btn.bottomAnchor.constraint(equalTo: theDrawingView.topAnchor, constant: -8.0),
            btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            btn.widthAnchor.constraint(equalToConstant: 160.0),
            
        ])
        
        btn.addTarget(self, action: #selector(self.toggleActivity(_:)), for: .touchUpInside)
        
    }
    
    @objc func toggleActivity(_ sender: Any) {
        guard let btn = sender as? UIButton else { return }
        if theDrawingView.isDrawing {
            theDrawingView.applyMask()
            btn.setTitle("Draw More", for: [])
        } else {
            theDrawingView.drawMore()
            btn.setTitle("Apply Mask", for: [])
        }
    }
}

DrawingImageView (modified)

class DrawingImageView: UIImageView {
    var path = UIBezierPath()
    var previousTouchPoint = CGPoint.zero
    var shapeLayer = CAShapeLayer()
    
    var isDrawing: Bool = true
    
    var isClear: Bool = false {
        didSet {
            updateView()
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        setupView()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    func updateView() {
        self.shapeLayer.shadowOffset = .init(width: 1, height: 1)
        self.shapeLayer.shadowColor = UIColor.black.cgColor
        self.shapeLayer.shadowOpacity = 1
        
        self.shapeLayer.lineWidth = 20
        self.shapeLayer.lineCap = .round
        self.shapeLayer.strokeColor = isClear ? UIColor.clear.cgColor : UIColor.blue.cgColor
        self.shapeLayer.opacity = 0.3
        self.isUserInteractionEnabled = true
    }
    
    func setupView() {
        self.layer.addSublayer(shapeLayer)
        updateView()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        if !isDrawing { return }
        if let location = touches.first?.location(in: self){
            previousTouchPoint = location
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        if !isDrawing { return }
        if let location = touches.first?.location(in: self){
            path.move(to: location)
            path.addLine(to: previousTouchPoint)
            previousTouchPoint = location
            shapeLayer.path = path.cgPath
        }
    }

    func applyMask() -> Void {
        // set shape opacity to 1.0
        shapeLayer.opacity = 1.0
        // use it as a mask
        layer.mask = shapeLayer
        isDrawing = false
    }
    
    func drawMore() -> Void {
        // remove the mask
        layer.mask = nil
        // set opacity back to 0.3
        shapeLayer.opacity = 0.3
        // add shapeLayer back as sublayer
        self.layer.addSublayer(shapeLayer)
        isDrawing = true
    }
}

Obviously, you'll have a lot more to do, but this should get you on your way.

One being next step is to convert / translate your path from the image view frame to the original image size, and then apply the mask to the image so it can be saved out. That should be a fun exercise for you :)

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Hey @DonMag, This is working perfectly thanks. I just want to know if I perform erase functionality on the resultant image then the as I draw it should show the image area in drawing, is it possible with the same shape layer? – Nick Sep 29 '20 at 04:46
  • @Nick - if you remove (or comment out) `if !isDrawing { return }` from `touchesBegan` and `touchesMoved` you'll be able to continue to "draw" additional path segments on the layer while it is active as the mask. – DonMag Sep 29 '20 at 13:41
  • Hey @DonMag I got that but what I am saying assumes the entire image is covered with the blue color shape layer and now I want to perform erase action with the finger so it should work if shape layer opacity is 0. but in my case, it's not working – Nick Sep 29 '20 at 13:51
  • @Nick - do you mean you want to remove part of the path that you have drawn? – DonMag Sep 29 '20 at 14:44
  • @Nick - ok, that's a whole different task. With this approach, you're not "drawing to a surface" ... you're creating a vector path and then letting UIKit fill / stroke the path. To "erase" part of that path, you'd have to find the path segments and delete them. Someone else was working on something similar a while back: https://stackoverflow.com/a/57415125/6257435 ... based on the last comments, it appears he went with PencilKit ... might be worth looking into. – DonMag Sep 29 '20 at 15:06
  • Hey @DonMag, I checked that but its really tedious and not for the runtime drawing. Can I achieve same result (as above) with pencilkit? I really appreciate your help – Nick Sep 30 '20 at 07:00
  • @Nick - I haven't done anything with PencilKit ... and I have no idea what your ultimate goal is. So, not much else I can say at this point. – DonMag Sep 30 '20 at 12:39
  • Hey @DoneMag, I am creating a feature where the user can draw on the image and erase the drawn area based on the drawn area I want to create a cutout. similar to photo cutout feature of picsart app. I am not sure which way is better – Nick Sep 30 '20 at 13:12
  • @Nick - I'm not familiar with the picsart app. Do you want to take what you have so far - the vector mask - and "invert" it? – DonMag Sep 30 '20 at 13:21
  • @DoneMag - Are you available to talk maybe on skype? – Nick Sep 30 '20 at 13:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222312/discussion-between-donmag-and-nick). – DonMag Sep 30 '20 at 13:44