Objective-Cでペイントの類似物を書く

この記事では、ユーザーが画像と対話するアプリケーションに役立つかもしれない簡単な写真エディタを作成するための段階的なアルゴリズムを読者に提供したいと思います。 最も基本的な機能が含まれており、小さな調整を行うことができます:特定の領域(ニックネームなど)のワイプ、重要なテキスト(電話、住所、または単なる引用)の強調表示、マップ上の任意の領域の円、または「単語の描画」。 技術的な観点から見ると、このトピックはかなりハックされていますが、プロセスを変更して、より速く簡単にする方法を見つけました-誰かがメモするかもしれません。











どの曲線も、相互接続された一連の線に分解できます。 次に、次の簡単なアルゴリズムを使用して曲線を描画できます。



  1. 曲線をセグメントのシーケンスに分割します
  2. 最初のセグメントの先頭にペンを置きます
  3. 終点まで線を引きます
  4. 次のセグメントに移動
  5. 色、線の太さ、その他の設定を選択します
  6. 画面に線を引きます
  7. シーケンスの各セグメントに対して上記の手順を繰り返します


このアルゴリズムを実装するには、開始点と終了点を含むPaintLineクラスを作成する必要があります。 線の描画は、UIBezierPathコンポーネントを使用して実行されます。



描画アルゴリズムを実装するには、カスタムビューを作成して追加する必要があります。 このコンポーネントは、タッチイベントをキャプチャし、画像を塗りつぶします。



指を最初にタッチすると、最初の行の開始点が設定されます。



- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; NSSet *allTouches = [event allTouches]; if (allTouches.count != 1){ return; } UITouch *touch = [[allTouches allObjects] objectAtIndex:0]; pointTo = [touch locationInView:self]; }
      
      





指の位置を変更すると、ラインのチェーンが作成され、作成された各ラインが表示用のシーケンスに追加されます。



 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; NSSet *allTouches = [event allTouches]; if (allTouches.count != 1){ [linePaint removeAllObjects]; [self setNeedsDisplay]; return; } UITouch *touch = [[allTouches allObjects] objectAtIndex:0]; pointFrom = pointTo; pointTo = [touch locationInView:self]; if (pointTo.y < self.bounds.size.height && pointFrom.y < self.bounds.size.height) { [self addLineFrom:pointFrom to:pointTo]; } }
      
      





ユーザーが画面から指を離すと、曲線がプレビューから画像自体に直接転送されます。



 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; NSSet *allTouches = [event allTouches]; if (allTouches.count != 1){ [linePaint removeAllObjects]; [self setNeedsDisplay]; return; } UITouch *touch = [[allTouches allObjects] objectAtIndex:0]; pointFrom = pointTo; pointTo = [touch locationInView:self]; if (pointTo.y < self.bounds.size.height && pointFrom.y < self.bounds.size.height) { [self addLineFrom:pointFrom to:pointTo]; } imgForPaint = [self getImage:imgForPaint]; dispatch_async(dispatch_get_main_queue(), ^{ if (_imageAfterEndPaint) { _imageAfterEndPaint(imgForPaint); } }); }
      
      





アルゴリズムの結果には、1つの小さな欠点があります。 太い線を選択した場合、セグメントの両端はそれぞれ曲線の始点と終点の方向に垂直になります。 丸い形のツールが描画に選択されていることを考えると、非常に美しく、信頼性が高くありません。



線の代わりに円を描くことができます。 電話の画面上で指を滑らかにスライドさせると良い結果が得られますが、鋭い動きはでこぼこのラインやギャップの出現にさえつながります。



両方の方法の欠点を補うために、単一の線を描くには、図に示すように、塗りつぶされた形状を使用できます。









結果は、端が丸い連続した曲線になります。



 - (UIBezierPath*)getBezuerPathWith:(float)zoom { UIBezierPath *bezierPath = [[UIBezierPath alloc] init]; float width = zoom * _lineWidth / 2.; for (PaintLine *line in linePaint) { CGPoint point1 = CGPointApplyAffineTransform(line.point1, CGAffineTransformMakeScale(zoom, zoom)); CGPoint point2 = CGPointApplyAffineTransform(line.point2, CGAffineTransformMakeScale(zoom, zoom)); [bezierPath moveToPoint:point1]; float alf = atan2(point2.y - point1.y, point2.x - point1.x); [bezierPath addArcWithCenter:point1 radius:width startAngle:alf + M_PI_2 endAngle:alf - M_PI_2 clockwise:YES]; float alf0 = alf - M_PI_2; CGPoint point = CGPointMake(cos(alf0) * width + point2.x, sin(alf0) * width + point2.y); [bezierPath addLineToPoint:point]; [bezierPath addArcWithCenter:point2 radius:width startAngle:alf - M_PI_2 endAngle:alf + M_PI_2 clockwise:YES]; alf0 = alf + M_PI_2; point = CGPointMake(cos(alf0) * width + point1.x, sin(alf0) * width + point1.y); [bezierPath addLineToPoint:point]; } return bezierPath; }
      
      





画像の塗りつぶしに進みます。 次のようにピクセルの色を変更することで実行されます。



  1. 画像をピクセル配列に変換
  2. 必要なすべてのピクセルのテキストを変更する
  3. 目的のピクセルの座標に移動し、ARGB形式で32ビットの色を変更します
  4. 変換された配列から画像を収集する


領域を埋めるために、スタックまたはキューへの呼び出しの数を最適化するクイックメソッドを使用できます。 しかし、ゆっくり注ぐ方法の中間結果を出力すると、美しい注ぐ効果が得られます。 それを実現するには、次の手順を実行する必要があります。



1.画像からピクセルの色を取得し、置換用の色として保存します

2.置換の色が新しいピクセルの色と一致するかどうかを確認します。 そうでない場合:



a)ピクセルをキューに入れる

b)選択したピクセルの色を新しい色に変更します

c)キューが空になるまで繰り返します

d)キューからピクセルを引き出します



3.各隣接ピクセルの色値を確認する手順を確認します

4.境界ピクセルの色が置き換えるピクセルの色と等しい場合:



a)ピクセルの色を新しい色に置き換えます

b)ピクセルをキューの最後に配置する



隣接ピクセルの選択方法に応じて、さまざまな形式の塗りつぶし伝播を取得できます。



1.隣接する4つのポイントを入力します









2.隣接する8つのポイントを入力します









3.隣接する4つと8つのポイント(2〜4、1〜8)で塗りつぶし









これらの方法はすべて、塗りつぶしのポリゴンの広がりにつながります。 さらに、提示されたオプションは、平面上の液体分布の自然なパターンに対応していません。 円の周りに塗りを広げる効果を得るには、アルゴリズムに次の変更を加える必要があります。



1.画像からピクセルの色を取得し、置換用の色として保存します

2.置き換える色が新しいピクセルの色と一致しない場合:



a)ピクセルをキューに入れる

b)選択したピクセルの色を新しい色に変更します

c)円の半径を1に設定します

d)次の半径に移動するために処理する必要があるピクセル数を設定します

e)キューが空になるまで繰り返します

f)キューからピクセルをプルする

g)次の半径に移動するために処理する必要があるピクセル数を1減らす



3.各隣接ピクセルの色値を確認する手順を確認します

4.境界ピクセルの色が置き換えるピクセルの色と等しい場合:



a)ピクセルの色を新しい色に置き換えます

b)ピクセルをキューの最後に配置する



5.次の半径に移動するために処理する必要があるピクセル数が0に減少した場合:



a)新しい値を現在のキューサイズに等しい数に設定します

b)円の半径を1増やします。



置換する色と範囲の新しい色を比較するときに等値を変更することにより、塗りつぶし領域と塗りつぶし境界線の間をスムーズに移行できます。



 [colorQueue addObject:[NSValue valueWithCGPoint:newStartPoint]]; int offset = 4*((w*round(newStartPoint.y))+round(newStartPoint.x)) + 1; memcpy(colorFroUpdate, &data[offset], 3); float limit = 10; isCanPaint = !(abs(newColorArray[0] - data[offset]) < limit && abs(newColorArray[1] - data[offset + 1]) < limit && abs(newColorArray[2] - data[offset + 2]) < limit); NSInteger countPixelICurrentIterrations = 1; int iterrationIndex = 1; while (isCanPaint && colorQueue.count > 0) { CGPoint point = [[colorQueue objectAtIndex:0] CGPointValue]; [colorQueue removeObjectAtIndex:0]; countPixelICurrentIterrations--; offset = 4*((w*round(point.y))+round(point.x)) + 1; memcpy(&data[offset], newColorArray, 3); CGPoint newPoint; int x0 = point.x - 1; int x1 = point.x + 1; int y0 = point.y - 1; int y1 = point.y + 1; for (int x = x0; x <= x1; x++) { for (int y = y0; y <= y1; y++) { float s = sqrtf((x - newStartPoint.x) * (x - newStartPoint.x) + (y - newStartPoint.y) * (y - newStartPoint.y)); if (s < iterrationIndex + 1) { newPoint = CGPointMake(x, y); if (newPoint.x >= 0 && newPoint.x < w && newPoint.y >= 0 && newPoint.y < h) { offset = 4*((w*round(newPoint.y))+round(newPoint.x)) + 1; if (abs(colorFroUpdate[0] - data[offset]) < limit && abs(colorFroUpdate[1] - data[offset + 1]) < limit && abs(colorFroUpdate[2] - data[offset + 2]) < limit) { memcpy(&data[offset], newColorArray, 3); [colorQueue addObject:[NSValue valueWithCGPoint:newPoint]]; } } } } } if (countPixelICurrentIterrations <= 0 && self.updateImageOn) { if (iterrationIndex % 5 == 0) @autoreleasepool { CGImageRef cgImage = CGBitmapContextCreateImage(cgctx); UIImage *resultUIImage = [UIImage imageWithCGImage:cgImage]; self.updateImageOn(resultUIImage); CGImageRelease(cgImage); } countPixelICurrentIterrations = [colorQueue count]; iterrationIndex++; } } }
      
      





描画プロセスを非同期で開始することにより、塗りの広がりの次の効果を確認できます。









そこで、意図した機能を実装すると同時に、アルゴリズムが非常に単純で、一見すると効果的ではないことが、非常に予期しない結果をもたらすことを実証しました。 ご清聴ありがとうございました!



All Articles