ラベル 画像 の投稿を表示しています。 すべての投稿を表示
ラベル 画像 の投稿を表示しています。 すべての投稿を表示

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の位置・サイズを求める

2018年7月8日日曜日

特定の色を透明にする

やりたいこと

画像の特定の色(例えばグレーの部分とか)だけを透明にしたい。
(別な画像と合成する時に使うのだ)

やり方

let maskColor:[CGFloat] = [188, 188, 188, 188, 188, 188]
let maskedImage = cgImage?.copy(maskingColorComponents: maskColor)


Core Graphicsの機能を使うので、画像をCGImageに変換しておく。(例では変数cgImageとして用意)

maskColorの配列が、透明化したい色を範囲として指定するための引数。はっきりはわかってないんだけど、要素0から順に、Rの下限、上限、Gの下限、上限、Bの下限、上限を0〜255の値で指定するようだ。
一色だけなら全部同じ値にすればいいし(CIFilterのポスタリゼーションで作った3階調のグレー色は188だった)、色の範囲で指定するならそれぞれの下限値、上限値を変えればいい。

実際に透過処理をしてるのがCGImageのcopyメソッド。処理が終わったCGImageを返す。
合成処理をするならCGImageのままかCIImageがいい。というか、macOSでNSImageに変換したら透明じゃなくなってしまった。

リファレンス見ると最後にCGImageをreleaseしろって書いてあるんで、そうした方がいいみたい。

注意

元画像にαチャンネルがあるとnilが返ってきてしまうので、あらかじめα情報を取り除く作業をしておかないといけない。そうしないと予期せぬエラーでアプリが落ちてしまう。

UIGraphicsの作り直しのOptionで第2引数のopaqueをtrueにすればOK。

UIGraphicsBeginImageContextWithOptions(srcImage.size, true, 0.0)
srcImage.draw(in: CGRect(origin: CGPoint.zero, size: srcImage.size))
let remakedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

2018年6月9日土曜日

点線の描画

Draw図形を描くときに点線で描く方法があるんだね。意外と簡単だった。

UIBezierPathの場合

UIViewのdraw( )の中で、以下のように書く。

//MARK:丸
let oval = UIBezierPath(ovalIn: CGRect(x: 100, y: 100, width: 200, height: 200))
UIColor.black.setFill() //stroke色
oval.lineWidth = 線幅
oval.setLineDash([10.0, 5.0, 5.0, 5.0], count: 4, phase: 0.0)

oval.stroke() //枠線描画

setLineDashがそれで、引数はUnsafePointerがどーとかわかりにくいんだけど、要は以下の3つだ。

  1. 点線の長さ、余白の長さの順に配列で指定
  2. 配列の要素数
  3. 配列の要素の使用開始位置
oval.setLineDash(UnsafePointer:<CGFloat>?, count: Int, phase: CGFloat)

CAShapeLayerの場合

こっちも似たようなもんだろう。

let layer = CALayer()
     
let lineDashPatterns: [[NSNumber]?]  = [nil, [2,3], [10, 5, 5, 5]]
   
for (index, lineDashPattern) in lineDashPatterns.enumerated() {
   
    let shapeLayer = CAShapeLayer()
    shapeLayer.strokeColor = UIColor.black.cgColor
    shapeLayer.lineWidth = 5
    shapeLayer.lineDashPattern = lineDashPattern
   
    let path = CGMutablePath()
    let y = CGFloat(index * 50)
    path.addLines(between: [CGPoint(x: 0, y: y),
                            CGPoint(x: 640, y: y)])
   
    shapeLayer.path = path
   
    layer.addSublayer(shapeLayer)
}

参考サイト

2018年3月8日木曜日

画像の作り直しでJPEGがPNGになっちゃった

Retinaディスプレイの画面をキャプチャし、それをツイッターに投稿するアプリを作ってるが、画像容量がでかくなるので縮小したい。

画面キャプチャだけ

iPhoneだとRetina解像度でちょうどいいのだが、iPadだとファイルサイズが大きくなるためツイートをはねられることがしばしばある。5MB制限らしいので、それをオーバーしてしまうようだ。
うまく投稿できても15秒くらいかかって遅い。投稿できた画像は元と同じくJPEG。

UIGraphicsBeginImageContextWithOptions(realSize, false, 2.0) //Contextを作成(Retina対応で2.0)
let ctx = UIGraphicsGetCurrentContext() //現在のContextを得る
imageView.layer.render(in: UIGraphicsGetCurrentContext()!) //Context部分に表示されている画像その他をレンダリング(ここが画面キャプチャ)
var image = UIGraphicsGetImageFromCurrentImageContext() //UIImageを作る

UIGraphicsEndImageContext()

縦横ピクセルを制限

iPadのRetina解像度のまま投稿すると縦横の長さも大きすぎるので、はみ出してる画像は最大1000ピクセルに縮小させるようコードを書いた。
いっぺんに縮小するのでなく、一度キャプチャして出来た画像を再度GraphicContextを使ってリサイズしている。
Retina解像度でキャプチャしているので、image.sizeで得られるwidth、heightは1/2になっているので注意。最終的に得られる画像はその倍になるのでややこしい。
1000pxの画像を得たいときに500pxに縮小すると最終的に1000pxになるというもの。

//画像縦横サイズ制限
//maxPixelSizeは例えば500とか入ってる
//縦横の最大を1000pxに抑えたいが、2倍精細なRetina解像度なので、image.sizeで得られる解像度はその半分になるため
var newSize = image?.size
if image!.size.width > maxPixelSize || image!.size.height > maxPixelSize {
    if image!.size.width >= image!.size.height {
        let rate = maxPixelSize / image!.size.width //縦横比
        newSize = CGSize(width: maxPixelSize, height: image!.size.height * rate)
    } else {
        let rate = maxPixelSize / image!.size.height //縦横比
        newSize = CGSize(width: image!.size.width * rate, height: maxPixelSize)
    }
        //UIImage作り直し開始。なぜかPNGになってしまう
    UIGraphicsBeginImageContext(newSize!)
    image!.draw(in: CGRect(x: 0, y: 0, width: newSize!.width, height: newSize!.height))
    image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    ⭐️
}

そうしたらなぜか画像がPNGになってしまった。縮小してるから投稿できないことはないが、1MBオーバーで縮小したありがたみが薄い。

キャプチャ時の解像度を非Retinaにするとサイズ制限しなくてもJPEGになってくれるけど、今度はiPhoneでのキャプチャ解像度が低いため、画像が小さすぎてしまう。

しょうがないので、一度JPEGのData型に直し、さらにそれをUIImageに戻して解決した。

上記コードの⭐️に挿入
//0.7は圧縮率
let imageData = UIImageJPEGRepresentation(image!, 0.7)
image = UIImage(data: imageData!)

  1. Retina解像度でContextを作ってキャプチャ
  2. UIImageとして変数に
  3. 縦横を同比率で縮小するためにwidthとheightを計算
  4. 計算したwidth、heightでContextを作り、2.のUIImageをdrawすることで画像縮小(このままだとPNGになる)
  5. UIImageをJPEG化してDataに直す(UIImageのままフォーマット変換できないから)
  6. DataをUIImageに戻して終了

原因はわからないが、UIImageって内部的にJPEGとかPNGとかの情報も持ってるのね?

2018年1月12日金曜日

Layerに描いた図形を全部消す

やりたいこと

UIViewのLayerに描いたドロー図形を全部クリアしたい。

やり方

Layerをインスタンス変数として持っておき、それをremoveFromSuperLayer()してやればいい。


let shapeLayer = CAShapeLayer()
    

func drawFigure() -> Void {
    let path = UIBezierPath()
    
    path.move(to: CGPoint(x: 384, y: 512))
    path.addLine(to: CGPoint(x: 600, y: 600))
    path.addLine(to: CGPoint(x: 600, y: 200))
    
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.fillColor = UIColor.blue.cgColor
    shapeLayer.path = path.cgPath
    self.view.layer.addSublayer(shapeLayer)
}
    
@IBAction func deleteFigure(_ sender: Any) {
    self.shapeLayer.removeFromSuperlayer()

}

注意点

インスタンス変数として持っておくところが肝。単に
self.view.layer.removeFromSuperlayer()
とやると関係ないLayerがなくなっちゃうから画面が真っ黒になっちゃう。
viewのどのLayerを削除するのか指示するために必要なのだな。

もしかしたら変数として持ってなくても消せるやり方あるのか知らんけどさ。

2017年11月5日日曜日

画像のメタデータを解析

画像のメタデータ、つまりEXIFの撮影データや、GPSの位置情報などを取り出す。

実際のメタデータはCIImageのpropertiesプロパティで取れる。
辞書の中の一部に配列が入れ子になってる。
let ciImage = CIImage(contentsOf: fileURL)
let metadata:[String:Any] = ciImage!.properties

辞書の中身は以下のよう。
{ }が辞書、( )が配列。
ISOSpeedRatingsのように要素1個の配列に入ってる場合もあるので注意。
辞書のKeyはColorModelとかの文字列でもいいが、kCGImagePropertyColorModelなどの定数があるのでそれを使うといい。ただし、CFStringなのでStringにキャストして使う。
“ ”で囲まれた数値は最初にNSNumberにキャストすると取り出せる場合が多い。
値がなくてnilの場合もあるので、Optionalとして、nilチェックもする。

辞書のトップレベル

辞書のトップレベルならそのままkeyで取れる。
let colorModel = metadata[kCGImagePropertyColorModel as String] as! String

辞書の2ndレベル

2ndレベル以降だと、一度入れ子になった辞書や配列を読み出さないといけない。
以下ではExifの中のExposureTime(シャッター速度)を取り出す。
let exifDict = metadata[kCGImagePropertyExifDictionary as String] as? [String:Any]
if let m = exifDict?[kCGImagePropertyExifExposureTime as String] as? NSNumber {
    let exposureTime = m.floatValue
}

つづいてISOSpeedRatings(ISO感度)を取り出す。
辞書の中の要素一個の配列に入ってる。
if let ISORateArr = exifDict?[kCGImagePropertyExifISOSpeedRatings as String] as? [Any] {
    if let rate = ISORateArr[0] as? NSNumber {
        let ISORate = exifDict[kCGImagePropertyExifISOSpeedRatings as String] = rate.stringValue
    }
}

メタデータの中身の例

{
    ColorModel = RGB;
    DPIHeight = 72;
    DPIWidth = 72;
    Depth = 8;
    Orientation = 1;
    PixelHeight = 2448;
    PixelWidth = 3264;
    ProfileName = "sRGB IEC61966-2.1";
    "{Exif}" =     {
        ApertureValue = "2.52606882168926";
        BrightnessValue = "2.661020629750272";
        ColorSpace = 1;
        ComponentsConfiguration =         (
            1,
            2,
            3,
            0
        );
        CustomRendered = 2;
        DateTimeDigitized = "2016:03:09 15:28:25";
        DateTimeOriginal = "2016:03:09 15:28:25";
        ExifVersion =         (
            2,
            2,
            1
        );
        ExposureBiasValue = 0;
        ExposureMode = 0;
        ExposureProgram = 2;
        ExposureTime = "0.0303030303030303";
        FNumber = "2.4";
        Flash = 32;
        FlashPixVersion =         (
            1,
            0
        );
        FocalLenIn35mmFilm = 31;
        FocalLength = "3.3";
        ISOSpeedRatings =         (
            160
        );
        LensMake = Apple;
        LensModel = "iPad Air 2 back camera 3.3mm f/2.4";
        LensSpecification =         (
            "3.3",
            "3.3",
            "2.4",
            "2.4"
        );
        MeteringMode = 5;
        PixelXDimension = 3264;
        PixelYDimension = 2448;
        SceneCaptureType = 0;
        SceneType = 1;
        SensingMethod = 2;
        ShutterSpeedValue = "5.058989898989899";
        SubjectArea =         (
            1631,
            1223,
            1795,
            1077
        );
        SubsecTimeDigitized = 996;
        SubsecTimeOriginal = 996;
        WhiteBalance = 0;
    };
    "{GPS}" =     {
        Altitude = "143.7687861271676";
        AltitudeRef = 0;
        DateStamp = "2016:03:09";
        DestBearing = "127.0993788819876";
        DestBearingRef = T;
        HPositioningError = 5;
        ImgDirection = "12x.xxx3788819876";
        ImgDirectionRef = T;
        Latitude = "36.588055";
        LatitudeRef = N;
        Longitude = "139.xxx3666666667";
        LongitudeRef = E;
        Speed = 0;
        SpeedRef = K;
        TimeStamp = "06:28:25";
    };
    "{MakerApple}" =     {
        1 = 4;
        10 = 2;
        14 = 1;
        2 = <37007700 73003500 34004c00 5c006c00 67007c00 75008500 a300b100 b000e400 6e006500 68003600 38005f00 62006c00 66007700 70007b00 8200b600 e200cd00 5800b900 9d009400 5f004300 52005e00 5c008100 8300b100 d000cf00 de000701 56006800 a8009700 7e005300 4c006600 7c008e00 9300ea00 1601f100 e900e300 53006100 83007c00 7e00c300 ca00b800 86008c00 9200f200 ff000101 d500b100 5e006200 6b00be00 4a016d01 7d018d01 5001cd00 a200c900 f800df00 e000af00 71009600 fd004d01 4b014401 4f014f01 6a01a201 44013a01 16012501 37019000 6f015801 0f01fc00 f800f000 f7002101 3f016a01 9001bb01 9b018101 4401f700 d500a200 e700d700 dc00e500 f500e800 18013b01 60019001 6a013001 12010b01 5f008000 9d00b200 9d009b00 b900c800 e7002a01 87017201 3401fa00 c800cb00 5c008000 91009400 85009900 9600af00 be005601 59012b01 e300d800 18012f01 6e008a00 7c007500 72008200 9100a500 e7006701 3a01c400 e9007601 8c018e01 41007100 63005c00 5f006c00 83009100 3a014f01 dc00fb00 9e019d01 aa01ad01 59005300 4f005500 52005d00 72000c01 53010b01 1701b001 a701a201 9b019c01 44005100 51005000 5200a400 0a014701 23010c01 af01b801 9f019c01 9701a601 36003800 48006700 d000f300 08014301 19018701 ad01b101 9c019901 93018a01>;
        20 = 3;
        3 =         {
            epoch = 0;
            flags = 1;
            timescale = 1000000000;
            value = 817736307596166;
        };
        4 = 1;
        5 = 214;
        6 = 220;
        7 = 1;
        8 =         (
            "-0.9269282",
            "0.0151294",
            "-0.3894437"
        );
    };
    "{TIFF}" =     {
        DateTime = "2016:03:09 15:28:25";
        Make = Apple;
        Model = "iPad Air 2";
        Orientation = 1;
        ResolutionUnit = 2;
        Software = "9.2.1";
        XResolution = 72;
        YResolution = 72;
    };

}

2017年7月12日水曜日

Documentsフォルダ内の画像ファイル読み書き

アプリ内にあるDocumentsフォルダに対して、画像ファイルを読み書きする方法。

Swift3ではURL(Swift2.xまではたしかNSURL)型で保存先情報を持つのだが、そのURLをそのまま引数に書いてやってもうまくいかない。
myURLに入っているなら、myURL.pathというプロパティで指定してやる。
引数はString型のようなのでmyURL.descriptionとやってみたがダメだった。

実機で試した場合のpathとdescriptio

ファイルpath
 /var/mobile/Containers/Data/Application/47DCD124-F9A4-4953-9B81-696DA790CCED/Documents/20170711055328142.jpg

ファイルdescription
 file:///var/mobile/Containers/Data/Application/47DCD124-F9A4-4953-9B81-696DA790CCED/Documents/20170711055328142.jpg

頭のfile:///の有無だけのようだ。

書き込み

//Documentsディレクトリのpathを得る(返り値はArrayで、index0がそれ)
let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
//StringappendingPathComponentがないのでURLに変換
let fileURL = URL(fileURLWithPath: docPath).appendingPathComponent(fileName)
//JPGに変換
let imageData = UIImageJPEGRepresentation(savePhoto, 1.0)
//画像書き込み(URLのpathを引数に)
//write(to:)はエラーを投げる関数なので、do-catch文が必要
do {
   //do-catchを使ってるので書込みエラーが起きるとcatchに移ってくれる
     try imageData?.write(to: fileURL, options: .atomic)
   //書き込み成功時の処理
   (省略)
   } catch let error {
   //書き込み失敗時の処理
     print("画像保存失敗 \(error)")
   }

読み込み

エラー処理なんかはしてない。

//Documentsディレクトリのpathを得る
let filePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
//その後にファイル名を追加
//(String型のfileWithPathのメソッドがSwiftにないので、一度URL型に変更)
let fileURL = URL(fileURLWithPath: filePath).appendingPathComponent("ファイル名")
//.pathプロパティを引数に画像読み込み
let uiImage = UIImage(contentsOfFile: fileURL.path)

2017年6月7日水曜日

画面キャプチャ方法

//余計なUIをキャプチャしないように隠す
hogeButton.isHidden = true
hogeLabel.isHidden = true
CATransaction.flush() //画面更新
        
//viewと同じサイズのコンテキスト(オフスクリーンバッファ)を作成
let rect = self.view.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
//スクリーンをコンテキストに描画 スクリーンキャプチャ
self.view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)

//スクリーンがキャプチャされたコンテキストからUIImageを作成
let capturedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
//コンテキストの扱いを終了
UIGraphicsEndImageContext()
        
//隠したUIを表示させる
hogeButton.isHidden = false
hogeLabel.isHidden = false

アニメの途中が表示されない問題

Context(オフスクリーンバッファみたいなもん)を作ってそこに画像をdrawしたりrenderしたりしてたんだけど、UIViewアニメの途中をキャプろうとしても、なぜかアニメ中のオブジェクトが実際に表示されている位置でキャプチャされてくれない現象が。

詳しいことはわからんのだが、アニメのメソッドで指定してる最後の移動位置でキャプチャされちゃうみたい。
つまり、AからBの位置に5秒間で移動するアニメを実行中、3秒目でキャプチャしてもなぜかキャプチャ画像だけはBの位置に来てる。(画面上ではまだ途中を動いてるというのに)

renderやdrawじゃなくdrawHierarchy

それを回避するため、drawHierarchy()メソッドを使うのがいいようだ。
ただし、afterScreenUpdatesがtrueの場合は同じような問題が発生するので、falseにしなければいけない。

直前のhiddenが効かない新たな問題

でもfalseにすると、写したくないUI部品をキャプチャ動作の直前でhiddenしても、hiddenされなくて写り込んでしまう。
UI部品をhiddenした直後にsetNeedsDisplay()とかやってもダメ。これは画面更新の予約だけで、実際に更新されるのはOS次第なんだそうな。

よくわからんがflush()で解決

そこで調べたところ、部品をhiddenした直後に CATransaction.flush() をしたところうまくいった。
一瞬部品がhiddenされ、スクリーンキャプチャされた後にまた部品が表示される。
CATransactionはCALayerのアニメを使うためのクラスだそうで、flush()はRunLoop終わりで自動的に呼ばれるものを強制的に(?)実行させるものらしい。
flashじゃなくてflushな。顔を赤らめるとか、興奮、感情の芽生え、トイレの貯水槽とかの意味がある。なんだよそれ!?w
正直言ってUIViewのメソッドじゃないのにうまくいくのは納得いかないんだけど、結果オーライでいいや。

2017年5月18日木曜日

画像サイズ変更

画像サイズを縮小したり拡大したりする方法。とりあえず縮小で説明。

要は、UIGraphicsImageContext(オフスクリーンバッファだな)を縮小(拡大)したサイズで作り、そこに元の画像をdrawすればいい。

以下でimageには512*512の画像が入っているとする。それを256*256のContextにdrawすると、Contextの縦横に合わせて画像が縮小されるわけですな。


//オフスクリーンのコンテキスト作成
UIGraphicsBeginImageContextWithOptions(CGSize(width: 256, height: 256), false, 0.0) 
//コンテキストに画像を描画
image.draw(in: CGRect(x: 0, y: 0, width: 256, height: 256))
//コンテキストから画像を得る
myImageView.image = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()


drawする際にCGRectの各値を変更すれば、描画位置を変えたり、縦横を縮小(拡大)して描画することができる。

単純な拡大も、例えば100*100の画像を256*256のコンテキストに描画すれば拡大されるっちゅうわけ。

画像のクロップ(切り抜き)

画像の一部をクロップする方法。(とりあえず矩形で)

たとえば1024*1024の画像があったとして、そのx:100, y:100, width:200, height:200 の部分をクロップしたい場合、
UIImageのcgImage.cropping(to:)でクロップしたい範囲を指定してやれば、クロップした画像がCGImageとして得られるので、それを再びUIImageに変換してやればいい。
か〜んた〜ん♪


func cropImage(image:UIImage, cropRect:CGRect) -> UIImage {
    let cropRef = image.cgImage!.cropping(to: cropRect) //クロップ
    //UIImageに戻す
    let cropImage = UIImage(cgImage: cropRef!, scale: image.scale, orientation: image.imageOrientation)
    
    return cropImage
}

クロップ範囲に画像より大きい範囲を指定した場合

クロップはおこなわれない。
余白がつくとかはないよ。

解像度の違いに注意

画像によって(たとえばスクリーンキャプチャしたもの)はiPadとiPhoneで解像度に違いがあるため、クロップ範囲をそれに合わせないといけない。

iPadの768*1024の画面をキャプチャしたものはそのまま768*1024の等倍サイズだが、iPhoneの場合、320*568の画面をキャプチャしたものは画像サイズこそ320*568だが、2倍の解像度になっているので、内部的には640*1136になっている。

その解像度の比率はUIImageのscaleプロパティで得られるので、クロップ範囲のwidthとheightにそれぞれかけてやればいい。
キャプチャした画像のscaleプロパティは、iPadは1.0、iPhoneなら2.0になってるはずだ。
キャプチャしたものじゃなくても当然解像度に違いがある画像ならこのプロパティを使って計算すればいいと思う。

let cropRect = CGRect(x: 0, y: 0, width: image.size.width * image.scale, height: image.size.height * image.scale)
image = self.cropImage(image: image, cropRect: cropRect)

スクリーンショット

スクリーンショットの撮りかた。
GraphicsContext(要はメモリ上に作る描画領域=オフスクリーンバッファだな)を作り、そこに画面に表示されてるviewのlayer(bitmapの画像がある…はず)をrenderしてやることでスクリーンショットが撮れるわけですな。

今回はviewの部分だけのキャプチャーなので、画面一番上に時計とかを表示してるステータスバーなんかはキャプチャーされない。
普通にスクリーンショットを撮った時みたいに、そこまで含めたい場合はまた今度調べる。


    func getScreenShot() -> UIImage {
        //viewと同じサイズのコンテキスト(オフスクリーンバッファ)を作成
        let rect = self.view.bounds
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
        //現在のviewの中身(スクリーン)をコンテキストに描画
        //viewの中身なのでステータスバーなどは含まれないようだ
        let context: CGContext = UIGraphicsGetCurrentContext()!
        self.view.layer.render(in: context)
        //スクリーンがキャプチャされたコンテキストからUIImageを作成
        let capturedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        //コンテキストの扱いを終了
        UIGraphicsEndImageContext()
        
        return capturedImage

    }


上にボタンとかを重ねてて、それをキャプチャーしたくない場合は、直前でhiddenして直後にまた表示すればいい。

Retinaディスプレイの場合、たとえばiPadの768*1024のスクリーンショットでも、倍の1536*2048の画像ができるので、それをさらに加工したりするときは注意。


このUIGraphicsBeginImageContext() 〜 UIGraphicsEndImageContext()で囲んだ間でオフスクリーンを操作するやり方はいろいろ使い道がありそうだ。

参考サイト