csabau / BTracking

Other
0 stars 0 forks source link

draw Path between two specific joints #5

Open csabau opened 2 years ago

mhamilt commented 2 years ago

do #2 first

csabau commented 2 years ago

I've got a Skeleton with the latest update!

I've moved the DrawLine class in the BodyTrack2D file so that I can take advantage of how the data is being processed there, as the actual values CGPoints values are in that file. Then I noticed I don't need the actual CGPoints themselves as there is a function that already does something based on the TwoDBodyJoint array of joints - angleFrom2Joints. This takes in 2 joints of type TwoDBodyJoint and although it does call private func angleBetween2Points to work out the angles, I don't need that part.

So I've replicated that function and modified to suit my needs - taking 2 joints as arguments and using the DrawLine class to draw a line between them. Called this function lineBetween2Joints. I've simply passed on the screen position of the joints from the argument into my DrawLine class.

I then called this in the ARSUIView2D file within the ARSessionDelegate to make sure it always updates with the latest position between the joints so that the line is accurate. I did however encounter the issue that the lines kept on drawing on the screen and never removing, filling my screen with lines. The way the circles are attached are by providing an updated location each view's location via updateTrackedViews, because the views were stored in trackedViews. That makes it easier to reposition the centre of a UIView. In lines are different because I don't care about the centre, but the two joints it's connecting. So I've created a similar variable to track every bone - trackedBones. With that I was able to create a removeBone function that I call in the ARSUIView2D file, every frame, before drawing the line. This leaves only the most up to date line on the screen and gives the impression that the line is moving with the joints.

I then created a dictionary for all the lines I wanted to create so that this whole process is a bit more automated. This bonesToSHow contains a dictionary of String keys and TwoDBodyJoint array of values. The key allows me to not only identify what bone needs to be created, but to assign it a name in the trackedBones dictionary as well, which allows me to remove it. The array of values makes it easy to pass the values to the DrawLine class.

Now I have the feeling you might think that this is a quite an unorthodox solution to the problem...but it does the job. Do you think I should be worrying about improvements for performance? Am I drawing and removing too many UIViews?

I know the code needs tidying up, but I feel like I've made MASSIVE progress today.

mhamilt commented 2 years ago

Do you think I should be worrying about improvements for performance? Am I drawing and removing too many UIViews?

Whatever you can get to work is as good a solution as any other. It is a lot harder to improve nothing than it is something.

What you might be taking the wrong approach with is the drawing and inheritance of UIView. It would probably be worthwhile quickly casting your eye over the docs. For instance, you'll want to do your drawing in the draw method. You can then create some function to both update the start and end points and also update redraw.

The core animation layer part might actually throw things a little:

https://github.com/csabau/BTracking/blob/8b4c3964e0a78e789a4a3d4d17516e54d2394a69/Sources/BodyTracking/BodyTracker2D.swift#L384-L394

typically you'd want to add the circles and the paths, otherwise you'll get some weird looking animation.

This is pretty thrown together but here are two potential ways, on keeping the other discarding the CALayer

In both cases all you'd need to do is trackedBones[bone]?.update(newStartPoint, newEndPoint) instead of removing and re-adding the bones

With CALayer

//---------------------------- Add Line between points------------

class DrawLine: UIView{
    //--------------------------------------------------------------------------
    var path: UIBezierPath!
    var strokeColour: CGColor = #colorLiteral(red: 0.670588235294118, green: 0.898039215686275, blue: 0.12156862745098, alpha: 1)
    var fillColour: CGColor = #colorLiteral(red: 0.250980392156863, green: 0.250980392156863, blue: 0.250980392156863, alpha: 0.0)
    var startPoint: CGPoint?
    var endPoint: CGPoint?
    //--------------------------------------------------------------------------
    init(frame: CGRect, start: CGPoint, end: CGPoint)
    {
        self.startPoint = start
        self.endPoint = end
        super.init(frame: frame)
    }
    //--------------------------------------------------------------------------
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    //--------------------------------------------------------------------------
    /// create a simple straight line
    /// - Parameters:
    ///   - start:
    ///   - end:
    func createLine(start: CGPoint, end: CGPoint) {
        path = UIBezierPath()

        path.move(to: start)
        path.addLine(to: end)
    }
    //--------------------------------------------------------------------------
    /// Create the CAShapeLayer Object because it's more versatile and can define stroke width.
    /// We assign the path of the BezierPath from above
    func simpleShapeLayer() {
        self.createLine(start: startPoint!, end: endPoint!)

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = self.path.cgPath

        shapeLayer.fillColor = fillColour
        shapeLayer.strokeColor = strokeColour
        shapeLayer.lineWidth = 4.0

        self.layer.addSublayer(shapeLayer)
    }
    //--------------------------------------------------------------------------
    /// This is where any drawing _should_ go
    /// - Parameter rect: this is the bounding rectangle of the view
    override func draw(_ rect: CGRect) {
        simpleShapeLayer()
    }
    //--------------------------------------------------------------------------
    /// update the points of the line and set the view to redraw
    /// - Parameters:
    ///   - start: new start point
    ///   - end: new end point
    func update(start: CGPoint, end: CGPoint)
    {
        self.startPoint = start
        self.endPoint = end
        setNeedsDisplay();
    }
    //--------------------------------------------------------------------------
}

Without CALayer

class DrawLine: UIView{
    //--------------------------------------------------------------------------
    var path: UIBezierPath!
    var strokeColour: CGColor = #colorLiteral(red: 0.670588235294118, green: 0.898039215686275, blue: 0.12156862745098, alpha: 1)
    var fillColour: CGColor = #colorLiteral(red: 0.250980392156863, green: 0.250980392156863, blue: 0.250980392156863, alpha: 0.0)
    var startPoint: CGPoint!
    var endPoint: CGPoint!
    //--------------------------------------------------------------------------
    init(frame: CGRect, start: CGPoint, end: CGPoint)
    {
        self.startPoint = start
        self.endPoint = end
        super.init(frame: frame)
    }
    //--------------------------------------------------------------------------
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    //--------------------------------------------------------------------------
    /// This is where any drawing _should_ go
    /// - Parameter rect: this is the bounding rectangle of the view
    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        context?.move(to: self.startPoint)
        context?.addLine(to: self.endPoint)
        context?.setStrokeColor(strokeColour)
        context?.setFillColor(fillColour)
        context?.setLineWidth(2)
        context?.strokePath()
    }
    //--------------------------------------------------------------------------
    /// update the points of the line and set the view to redraw
    /// - Parameters:
    ///   - start: new start point
    ///   - end: new end point
    func update(start: CGPoint, end: CGPoint)
    {
        self.startPoint = start
        self.endPoint = end
        setNeedsDisplay();
    }
    //--------------------------------------------------------------------------
}
mhamilt commented 2 years ago

Realised the above might also duplicate lines depending on the relationship to the ARView

In which case, removing and re-adding a view is probably the easiest option. You could make a little more self contained by creating 2 classes

  1. LineView which simply draws a line
  2. BoneView which adds a LineView as a subview and provides an update function to deal with clearing and redrawing the view.

Ultimately you'll want to end up with something like a Skeleton2DView object that collects this together

class LineView: UIView{
    //--------------------------------------------------------------------------
    var path: UIBezierPath!
    var strokeColour: CGColor = #colorLiteral(red: 0.670588235294118, green: 0.898039215686275, blue: 0.12156862745098, alpha: 1)
    var fillColour: CGColor = #colorLiteral(red: 0.250980392156863, green: 0.250980392156863, blue: 0.250980392156863, alpha: 0.0)
    var startPoint: CGPoint!
    var endPoint: CGPoint!
    //--------------------------------------------------------------------------
    init(frame: CGRect, start: CGPoint, end: CGPoint)
    {
        self.startPoint = start
        self.endPoint = end
        super.init(frame: frame)
    }
    //--------------------------------------------------------------------------
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    //--------------------------------------------------------------------------
    /// This is where any drawing _should_ go
    /// - Parameter rect: this is the bounding rectangle of the view
    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        context?.move(to: self.startPoint)
        context?.addLine(to: self.endPoint)
        context?.setStrokeColor(strokeColour)
        context?.setFillColor(fillColour)
        context?.setLineWidth(2)
        context?.strokePath()
    }

}

class BoneView: UIView{
    //--------------------------------------------------------------------------
    var startPoint: CGPoint!
    var endPoint: CGPoint!
    //--------------------------------------------------------------------------
    init(frame: CGRect, start: CGPoint, end: CGPoint)
    {
        self.startPoint = start
        self.endPoint = end
        super.init(frame: frame)
    }
    //--------------------------------------------------------------------------
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    //--------------------------------------------------------------------------
    /// This is where any drawing _should_ go
    /// - Parameter rect: this is the bounding rectangle of the view
    override func draw(_ rect: CGRect) {
        self.addSubview(LineView(frame: rect,
                                 start: self.startPoint,
                                 end: self.endPoint))
    }
    //--------------------------------------------------------------------------
    /// update the points of the line and set the view to redraw
    /// - Parameters:
    ///   - start: new start point
    ///   - end: new end point
    func update(start: CGPoint, end: CGPoint)
    {
        for subview in self.subviews{
            subview.removeFromSuperview()
        }
        self.startPoint = start
        self.endPoint = end

        setNeedsDisplay();
    }
    //--------------------------------------------------------------------------
}
csabau commented 2 years ago

That looks great thanks Matt.

I will look into improving the way things get drawn if I have any time left before the submission, if not I will likely want to improve it after anyway. For now I will focus on the logic for the actual form check for my submission.