iPhone smooth sketch drawing algorithm
Asked Answered
R

6

64

I am working on a sketching app on the iPhone. I got it working but not pretty as seen here enter image description here

And I am looking for any suggestion to smooth the drawing Basically, what I did is when user places a finger on the screen I called

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 

then I collect a single touch in an array with

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

and when the user lefts a finger from the screen, I called

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

then I draw all the points in the array using

NSMutableArray *points = [collectedArray points];   

CGPoint firstPoint;
[[points objectAtIndex:0] getValue:&firstPoint];

CGContextMoveToPoint(context, firstPoint.x, firstPoint.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);

for (int i=1; i < [points count]; i++) {
    NSValue *value = [points objectAtIndex:i];
    CGPoint point;
    [value getValue:&point];    
    CGContextAddLineToPoint(context, point.x, point.y);

} 

CGContextStrokePath(context);
UIGraphicsPushContext(context);

And now I want to improve the drawing tobe more like "Sketch Book" App enter image description here

I think there is something to do with signal processing algorithm to rearrange all the points in the array but I am not sure. Any Help would be much appreciated.

Thankz in advance :)

Raglan answered 22/2, 2011 at 9:56 Comment(2)
GooD question with GooD snaps.Mirthamirthful
Very interesting, but I'm curious about the focus on line segments rather than points. From what I can tell, most professional graphics software (Photoshop, PaintTool SAI, GIMP, etc) apply bitmap-based brushes at evenly spaced intervals.Infraction
N
22

The easiest way to smooth a curve like this is to use a Bezier curve instead of straight line segments. For the math behind this, see this article (pointed to in this answer), which describes how to calculate the curves required to smooth a curve that passes through multiple points.

I believe that the Core Plot framework now has the ability to smooth the curves of plots, so you could look at the code used there to implement this kind of smoothing.

There's no magic to any of this, as these smoothing routines are fast and relatively easy to implement.

Navarrette answered 23/2, 2011 at 18:30 Comment(2)
I would like to point out that the bezier curve article in the link and the code shared therein is NOT for interactively sketched data. This kind of curve fitting algorithm requires that you know all of the points before running the algorithm.Wilmawilmar
I'm a little late to the party, but maybe OP could do what he's done with the lines, then after the finger has been lifted he'll have all the points to be able to draw the Bézier curve, so he could just replace the old layer with the new curve layer (I know that drawing freehand in photoshop does something similar. Until the user is done drawing the curves are sort of pointy, but then round off afterwards)Jehad
L
59
CGPoint midPoint(CGPoint p1, CGPoint p2)
{

    return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);

}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch = [touches anyObject];

    previousPoint1 = [touch previousLocationInView:self];
    previousPoint2 = [touch previousLocationInView:self];
    currentPoint = [touch locationInView:self];

}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch = [touches anyObject];

    previousPoint2 = previousPoint1;
    previousPoint1 = [touch previousLocationInView:self];
    currentPoint = [touch locationInView:self];


    // calculate mid point
    CGPoint mid1 = midPoint(previousPoint1, previousPoint2); 
    CGPoint mid2 = midPoint(currentPoint, previousPoint1);

    UIGraphicsBeginImageContext(self.imageView.frame.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.imageView.image drawInRect:CGRectMake(0, 0, self.imageView.frame.size.width, self.imageView.frame.size.height)];

    CGContextMoveToPoint(context, mid1.x, mid1.y);
    // Use QuadCurve is the key
    CGContextAddQuadCurveToPoint(context, previousPoint1.x, previousPoint1.y, mid2.x, mid2.y); 

    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextSetLineWidth(context, 2.0);
    CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
    CGContextStrokePath(context);

    self.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

}
Loreenlorelei answered 31/3, 2011 at 14:20 Comment(2)
a sample project based on this solution here: github.com/brightredchilli/Free-SketchingPrefecture
This worked really well for me too (I used the algorithm in an HTML5 canvas).Deadandalive
N
22

The easiest way to smooth a curve like this is to use a Bezier curve instead of straight line segments. For the math behind this, see this article (pointed to in this answer), which describes how to calculate the curves required to smooth a curve that passes through multiple points.

I believe that the Core Plot framework now has the ability to smooth the curves of plots, so you could look at the code used there to implement this kind of smoothing.

There's no magic to any of this, as these smoothing routines are fast and relatively easy to implement.

Navarrette answered 23/2, 2011 at 18:30 Comment(2)
I would like to point out that the bezier curve article in the link and the code shared therein is NOT for interactively sketched data. This kind of curve fitting algorithm requires that you know all of the points before running the algorithm.Wilmawilmar
I'm a little late to the party, but maybe OP could do what he's done with the lines, then after the finger has been lifted he'll have all the points to be able to draw the Bézier curve, so he could just replace the old layer with the new curve layer (I know that drawing freehand in photoshop does something similar. Until the user is done drawing the curves are sort of pointy, but then round off afterwards)Jehad
C
14

I really love the topic. Thanks for all the implementations, espesially Krzysztof Zabłocki and Yu-Sen Han. I have modified the version of Yu-Sen Han in order to change line thickness depending on the speed of panning (in fact the distance between last touches). Also I've implemented dot drawing (for touchBegan and touchEnded locations being close to each other) Here is the result: enter image description here

To define the line thickness I've chosen such a function of distance:

(Don't ask me why... I just though it suits well, but I'm sure you can find a better one)

enter image description here

CGFloat dist = distance(previousPoint1, currentPoint);
CGFloat newWidth = 4*(atan(-dist/15+1) + M_PI/2)+2;

One more hint. To be sure the thickness is changing smoothly, I've bounded it depending on the thickness of the previous segment and a custom coef:

self.lineWidth = MAX(MIN(newWidth,lastWidth*WIDTH_RANGE_COEF),lastWidth/WIDTH_RANGE_COEF);
Consult answered 24/10, 2012 at 21:51 Comment(2)
Hi Alex! i am working on a similar kind of app, would you be kind enough to share the code, that how and where you modified the smooth line code by Yu-Sen Han or if you have made any branch to it! Thanks.Salish
This response refers to two other responses that don't exist: one from "Krzysztof Zabłocki" and one from "Yu-Sen Han". Presumably those responses provided answers to the question, but this one does not. A search found a blog post by Krysztof Zablocki, I could find nothing from Yu-Sen Han: merowing.info/2012/04/…Linalinacre
M
5

I translated kyoji's answer into Swift, as a reusable subclass of UIImageView. The subclass TouchDrawImageView allows the user to draw on an image view with her finger.

Once you've added this TouchDrawImageView class to your project, make sure to open your storyboard and

  1. select TouchDrawImageView as the "Custom Class" of your image view
  2. check "User Interaction Enabled" property of your image view

Here's the code of TouchDrawImageView.swift:

import UIKit

class TouchDrawImageView: UIImageView {

    var previousPoint1 = CGPoint()

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        previousPoint1 = touch.previousLocation(in: self)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let previousPoint2 = previousPoint1
        previousPoint1 = touch.previousLocation(in: self)
        let currentPoint = touch.location(in: self)


        // calculate mid point
        let mid1 = midPoint(p1: previousPoint1, p2: previousPoint2)
        let mid2 = midPoint(p1: currentPoint, p2: previousPoint1)

        UIGraphicsBeginImageContext(self.frame.size)
        guard let context = UIGraphicsGetCurrentContext() else { return }
        if let image = self.image {
            image.draw(in: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
        }

        context.move(to: mid1)
        context.addQuadCurve(to: mid2, control: previousPoint1)

        context.setLineCap(.round)
        context.setLineWidth(2.0)
        context.setStrokeColor(red: 1.0, green: 0, blue: 0, alpha: 1.0)
        context.strokePath()

        self.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }

    func midPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
        return CGPoint(x: (p1.x + p2.x) / 2.0, y: (p1.y + p2.y) / 2.0)
    }
}
Malloy answered 23/9, 2017 at 20:40 Comment(1)
How to do undo and redo from previouspoint 1 and previouspoint2Guyton
R
3

Thankz for the input.I update my quest here because I need the space for it.

I look up both corePlot and Bezier curve solutions that you suggested with little success.

For the corePlot I am able to get the graph plot from an array of int but can't find anything related to curve smoothing.BTW Here I am using CPScatterPlot with some random number.

enter image description here

as for Bezier curve, My quest lead me to here It is something to do with Spline implementation in iOS

  CatmullRomSpline *myC = [[CatmullRomSpline alloc] initAtPoint:CGPointMake(1.0, 1.0)];
  [myC addPoint:CGPointMake(1.0, 1.5)];
  [myC addPoint:CGPointMake(1.0, 1.15)];
  [myC addPoint:CGPointMake(1.0, 1.25)];
  [myC addPoint:CGPointMake(1.0, 1.23)];
  [myC addPoint:CGPointMake(1.0, 1.24)];
  [myC addPoint:CGPointMake(1.0, 1.26)];
  NSLog(@"xxppxx %@",[myC asPointArray]);
  NSLog(@"xxppxx2 %@",myC.curves);

and the result I get is:

  2011-02-24 14:45:53.915 DVA[10041:40b] xxppxx (
  "NSPoint: {1, 1}",
  "NSPoint: {1, 1.26}"
   )

  2011-02-24 14:45:53.942 DVA[10041:40b] xxppxx2 (
  "QuadraticBezierCurve: 0x59eea70"
  )

I am not really sure how to go from there. So I am stuck on that front as well :(

I did look up GLPaint, as a last resource. It uses OpenGLES and use a "soft dot" sprite to plot the points in the array. I know it's more like avoiding the problem rather than fixing it. But I guess I'l share my findings here anyway.

The Black is GLPaint and the white one is the old method. And the last one is the drawing from "Sketch Book" app just to compare

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

I am still trying to get this done right, any further suggestion are most welcome.

Raglan answered 24/2, 2011 at 7:58 Comment(1)
I have been trying for weeks, have you got anything? I ran into the the same problem with Spline implementation. If you have anything please let me know. Thanks...Wildfire
P
1

To get rid of the silly dot in the GLPaint code.

Change in

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

this function

//Ändrat av OLLE
/*
// Convert touch point from UIView referential to OpenGL one (upside-down flip)
if (firstTouch) {
    firstTouch = NO;
    previousLocation = [touch previousLocationInView:self];
    previousLocation.y = bounds.size.height - previousLocation.y;
} else {
    location = [touch locationInView:self];
    location.y = bounds.size.height - location.y;
    previousLocation = [touch previousLocationInView:self];
    previousLocation.y = bounds.size.height - previousLocation.y;
}
 */
location = [touch locationInView:self];
location.y = bounds.size.height - location.y;
previousLocation = [touch previousLocationInView:self];
previousLocation.y = bounds.size.height - previousLocation.y;

//Ändrat av OLLE//

I know that this isn't the solution for our problem, but it's something.

Pewit answered 24/2, 2011 at 8:44 Comment(1)
Thankz for sharing Olle, It did make some strange dot disappear. Let's hope we'll find our solution soon :)Raglan

© 2022 - 2024 — McMap. All rights reserved.