Visionで輪郭抽出して遊ぶ
これはなに
Visionを使って画像の輪郭抽出して遊んだのでその記録
撮影した画像(キャプチャ)に対してVisionの輪郭抽出をして遊んでみました。
とある勉強会でLTした内容です。
くるるんが可愛すぎるのでくるるんを題材にしていますが、個人的な利用であり、所属する会社、組織とは全く関係ありません。(念の為)
まずは成果物をどうぞ
撮影した画像を切り抜いて、そのあと動かしてます。
なにをしているか
1.画像をCIImage
に変換
1.CIFilter
で切り出しやすい画像に変換
1.VNDetectContuer
で輪郭抽出
1.抽出したパスを加工。外側の輪郭だけ取得
1.元の画像に輪郭を描画
1.VNContour
のnormalizedPath
で内側を塗りつぶした画像と元画像をマスク処理
1.切り抜き完成
1.切り抜いた画像をSKTexture
にしてSpriteKit
のSkSpriteNode
で使えるようにする
輪郭抽出
VNDetectContoursRequestで画像の輪郭をnormalizedPath
(CGPath)で取得できるのでそれを使います
CIFilter
で切り出しやすい画像に変換
輪郭抽出するまえの画像データを加工する必要があります。エッジが際立つので抽出しやすくなります。
画像データを加工ずるためにCIFilter
を使うためCIImage
に変換します。
今回はCIFilter
でグレースケール化、したものを二値化してさらに黒い領域が多い部分は黒く塗りつぶす処理などをかけています。
CIImageにすると画像の回転情報をもたないので画像が横になったりするのでこちらはあとで回転情報を保持して再度渡してあげる必要があります。
CIFilterの使い方は例えば以下のようになります。
// 撮影画像を二値化
let thresholdFilter = CIFilter.colorThreshold()
thresholdFilter.inputImage = grayscaleImage
thresholdFilter.threshold = 0.3
guard let thresholdImage = thresholdFilter.outputImage else { return nil }
VNDetectContuer
で輪郭抽出
切り出しやすい画像に変換したらVNDetectContuer
のrequestHandlerに渡します。Visionのリクエスト処理のイメージは前回の記事(貼る!)とほぼ同じです
self.preProcessImage = preprocessForDetectContour(screenImage: inputImage)
let contourRequest = VNDetectContoursRequest.init()
contourRequest.revision = VNDetectContourRequestRevision1
contourRequest.contrastAdjustment = 1.0
contourRequest.detectsDarkOnLight = true
contourRequest.maximumImageDimension = 524
guard let preProcessImage = self.preProcessImage else { return }
let requestHandler = VNImageRequestHandler.init(ciImage: preProcessImage, options: [:])
do {
try requestHandler.perform([contourRequest])
} catch {
// error
}
CGPathを使って輪郭を描画します。
VNDetectContoursRequest
で取得したVNContoursObservationのtopLevelContours
のVNContourのnormalizedPath: CGPath
を使って輪郭を描画します。
public func drawContours(contours: VNContour, sourceImage: CGImage)
-> UIImage
{
let size = CGSize(width: sourceImage.width, height: sourceImage.height)
let renderer = UIGraphicsImageRenderer(size: size)
let renderedImage = renderer.image { (context) in
let renderingContext = context.cgContext
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height)
renderingContext.concatenate(flipVertical)
renderingContext.draw(
sourceImage,
in: CGRect(x: 0, y: 0, width: size.width, height: size.height)
)
renderingContext.scaleBy(x: size.width, y: size.height)
renderingContext.setLineWidth(100.0 / CGFloat(size.width))
let yellowUIColor = UIColor.yellow
renderingContext.addPath(contours.normalizedPath)
renderingContext.setStrokeColor(yellowUIColor.cgColor)
renderingContext.setFillColor(UIColor.clear.cgColor)
renderingContext.strokePath()
self.path = contours.normalizedPath
self.croppingImage = makeShapeNode(
from: contours.normalizedPath,
captureImage: CIImage(cgImage: sourceImage)
)
}
return renderedImage
}
CGPathを使ってマスク処理
normarizePathとUIGraphicsImageRenderer
を使ってマスク処理用に取得したパスで塗りつぶした画像を作ります。下記が完成イメージ
guard let transPath = normalizedPath.copy(using: &transform) else { return nil }
let pathFillImage = UIGraphicsImageRenderer(
size: CGSize(width: extent.width, height: extent.height),
format: format
)
.image { context in
context.cgContext.setFillColor(UIColor.white.cgColor)
context.cgContext.addPath(transPath)
context.cgContext.fillPath()
}
マスク処理もCIFilter
を使います
multiplyCompositing()
メソッドのinputに撮影した画像(CIImage) を、outputにパスで塗りつぶした画像を使います。
// パスの内側だけキャプチャした画像を切り抜く
let filter = CIFilter.multiplyCompositing()
filter.inputImage = captureCIImage
filter.backgroundImage = maskCIImage
guard let texture = filter.outputImage else { return nil }
SKSpriteNodeに切り出した画像をTextureにして貼る
切り出した画像を SKTexture
で渡して SKSpriteNode
に貼る
Sceneの touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
でSpriteを動かす
import SpriteKit
class KururunSprite: SKSpriteNode {
init(texture: [SKTexture], position: CGPoint) {
super
.init(
texture: texture[0],
color: .clear,
size: CGSize(
width: texture[0].size().width / 90, // この辺のサイズは適当です
height: texture[0].size().height / 90
)
)
physicsBody = SKPhysicsBody(
rectangleOf: CGSize(
width: texture[0].size().width / 90,
height: texture[0].size().height / 90
)
)
setScale(4.0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func move() {
// 速度の更新
physicsBody?.velocity = CGVector(dx: 0, dy: 0)
// 質量を無視して力を加える
physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
physicsBody?.allowsRotation = true
let action = SKAction.rotate(byAngle: Double.pi / 8, duration: 3.0)
run(action)
}
}
SwiftUIでSpriteKitを動かすのでその画面の準備
mport SpriteKit
import SwiftUI
struct SpriteScreenView: View {
var scene: GameScene
var node: CIImage
init(node: CIImage) {
self.node = node
scene = GameScene()
scene.node = node
scene.size = CGSize(width: 828, height: 1792) // 適当です。geometry使いたい
scene.scaleMode = .fill
}
var body: some View {
GeometryReader { geometry in
VStack {
ZStack {
if #available(iOS 14.0, *) {
SpriteView(scene: scene)
.frame(width: geometry.size.width, height: geometry.size.height)
.ignoresSafeArea()
} else {
// なにも用意してない (^θ^)
}
}
}
}
}
}
物理エンジン(SKPhisycsBody)の設定
didMove(to view: SKView)
で物理演算の設定
touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
でSpriteNodeを動かす処理を呼ぶ
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var node: CIImage!
lazy var kururun: KururunSprite = {
let uiimage = self.node.toUIImage(orientation: UIImage.Orientation(rawValue: 90) ?? .up) // .upは適当
let cgImage = uiimage?.cgImage
let texture = SKTexture(cgImage: cgImage!) // forceunwrapは適当
let position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return KururunSprite(texture: [texture], position: position)
}()
override func didMove(to view: SKView) {
// 物理エンジンのグローバル設定
self.physicsBody = SKPhysicsBody.init(edgeLoopFrom: self.frame)
self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -5.0)
self.physicsWorld.contactDelegate = self
self.kururun.position = CGPoint(
x: self.size.width / 2.0,
y: self.size.height / 2.0 + (kururun.size.height + 50.0)
)
self.addChild(kururun)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
kururun.move()
}
}
Author And Source
この問題について(Visionで輪郭抽出して遊ぶ), 我々は、より多くの情報をここで見つけました https://zenn.dev/ohayoukenchan/articles/554059b999f527著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol