2018年11月15日木曜日

タッチ座標の色情報を得る

やりたいこと

UIImageViewをタッチしたピクセルの色情報とアルファ値を得る。
たとえば赤いピクセルをタッチしたら
r:255.0 g:0.0 b:0.0 a:255.0
などと出るように。

やり方

タッチ位置に相当するImageViewに貼られたImageのアドレスを参照し、色情報を得る。
ImageViewに表示したImageはたいてい縮小か拡大されてるので、画面のタッチ位置からImage上の位置を計算する。

Image上の位置から画像データのメモリ上のアドレスを計算する。
アドレス計算時は1ピクセルを何バイトで扱っているのかの考慮が必要。(4バイトだった)
Retinaディスプレイの場合、縦横ともデータが2.0倍とかなるので(最近は3.0倍もあるんだっけ?)それも考慮に入れてアドレス計算要。
(注:Retinaで2.0のscaleを得て計算したらかえってうまくいかなかったので、以下のコードでは1.0を代入してある。おそらく画面比率の計算とかを二重にやっちゃってるんじゃないかと思うんだけど、よくわからねぇ…(^^;)ゞ
画面のタッチ位置とオリジナルImage上の位置の変換をしなければ、Retina解像度を考慮に入れたやり方でうまくいくんじゃないかと思う…)

サイトを参考にUIImageを拡張して機能を付けた。

画像の本当の大きさとImageView上に貼られた画像の比率を計算する過程でAVFoundationをimport要。

注意点

CGImageは必ずしもRGBAの順番で並んでいるわけじゃないそうな。
CGImageのCGImageAlphaInfo.rawValueでどういう並びなのか調べられる
以下のコードではRGBAの並びとして書いている。

コード

Swift4.1
import UIKit
import AVFoundation

let pixelDataBytesSize = 4; //1ピクセルのバイト数

extension UIImage {
func getColor(pos: CGPoint) -> (color:UIColor, r:CGFloat, g:CGFloat, b:CGFloat, a:CGFloat) {
let imageData = self.cgImage?.dataProvider?.data //画像データ
let data:UnsafePointer = CFDataGetBytePtr(imageData) //画像データのポインタ(配列扱い)を得るらしい
let scale:CGFloat = 1.0 //UIScreen.main.scale //画面のスケール(Retinaなら多分2.0〜3.0)を得る(コメントアウト中)
//任意のピクセルアドレスを計算
let address:Int = ((Int(self.size.width) * Int(pos.y * scale)) + Int(pos.x * scale)) * pixelDataBytesSize
//RGBαの各値を得る
let r = CGFloat(data[address])
let g = CGFloat(data[address + 1])
let b = CGFloat(data[address + 2])
let a = CGFloat(data[address + 3])
return (UIColor(red: r / 255, green: g / 255, blue: b / 255, alpha: a / 255), r, g, b, a)
}
}

class ViewController: UIViewController {
@IBOutlet weak var myLabel: UILabel!
@IBOutlet weak var myImageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//imageView上の実表示フレーム
let presentedFrame = AVMakeRect(aspectRatio: (myImageView.image?.size)!, insideRect: myImageView.bounds)
let marginX = presentedFrame.origin.x //上部のマージン
let marginY = presentedFrame.origin.y //左のマージン
if let touch = touches.first as UITouch? {
let loc = touch.location(in: myImageView) //タッチ座標
let image = myImageView.image
//画面上のタップ位置とimage上のタップ位置の変換
let tapX = (image?.size.width)! / presentedFrame.size.width * (loc.x - marginX)
let tapY = (image?.size.height)! / presentedFrame.size.height * (loc.y - marginY)
let color = image?.getColor(pos: CGPoint(x: tapX, y: tapY))
myLabel.textColor = color?.color
myLabel.text = "r:\(color!.r.description) g:\(color!.g.description) b:\(color!.b.description) a:\(color!.a.description)"
}
}
}

参考サイト

ImageViewに表示されたImageの、表示部分だけのframeを求める

ただし、AspectFitで表示された場合のみ有効。

ImageViewにImageを表示する際、contentModeの設定次第でImageを拡大/縮小して収めてくれるわけだが、Imageが実際に表示されている部分のframeサイズを得たいことがある。
そんな時に使うのが以下の関数。
ただしImageの縦横比を保ったまま拡大/縮小する .scaleAspectFit に設定された場合のみ。(他の設定にしても同じ結果しか得られなかった)
以下はすでにImageViewに表示中のImageについてframeサイズを得たが、表示前のImageでも大丈夫だろう。

Swift4.1
let presentedFrame = AVMakeRect(aspectRatio: (myImageView.image?.size)!, insideRect: myImageView.bounds)
print("表示サイズ \(presentedFrame)")

あらかじめAVFoundationをimportして使う。

ずっとわからず、比率を自前で計算してやってたんだけど、誤差が出たりとってもめんどうだった。
他のcontentModeでも使える関数ないかしら?

参考サイト:AS blind side/UIImagwViewにAspect Fitで配置されたUIImageの位置・サイズを求める