読者です 読者をやめる 読者になる 読者になる

PILのImage.transformの使い方 (PERSPECTIVE)

PILのImage.transformの使い方 (QUAD, MESH) - umejanのブログのつづき。

Image.PERSPECTIVEの変形

遠近感のある形に変形する。

Image.PERSPECTIVEのdata指定方法

data : 8 tupples (a, b, c, d, e, f, g, h)

(x0, y0) = ((ax+by+c)/(gx+hy+1), (dx+ey+f)/(gx+hy+1))

で元の画像のピクセルで見るとのこと。


自力でa,b,c,d,e,f,hを計算するのは難しい。

変換元座標から変換先座標に変換する a,b,c,d,e,f を求める処理がネットにあったのでそれを利用する。 ( python - How does perspective transformation work in PIL? - Stack Overflow ) ※中身わからず。方程式を解いている?


遠近感のある画像から正面から見たように変換する場合は、変換前と変換後の4座標は決められる。

しかし、正面の画像を遠近感のある画像に変換する場合は、本来であれば変換後の4点を計算する必要があるだろう。

そこで、サンプルでは、視野変換、透視投影変換で計算した座標を求める処理も用意した。 詳しくはサンプル参照のこと。

視野変換は、 http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090902 を参考にした。

透視投影変換は、 http://marupeke296.com/DXG_No70_perspective.html を参考にした。

Image.PERSPECTIVEのサンプル

直接座標を指定して変形したサンプルと
視野変換、透視投影変換で計算して変換後の座標を求めて変形したサンプル。

# -*- coding: utf-8 -*-
from __future__ import print_function
from PIL import Image
from PIL import ImageDraw
import numpy as np
import math
import argparse

parser = argparse.ArgumentParser(description="Image.transformのサンプル")

parser.add_argument("--output","-o", required=True, type=str
                   , help="出力ファイル")

args = parser.parse_args()

# 変換元の画像をロード
def load_image2() :
  #im = Image.open("xxxx.png")

  # サンプルとして、82x66の画像を作成。グレースケール256階調
  # 16ドットごとに格子を描画
  im = Image.new("L", (82, 65), color=0)
  draw = ImageDraw.Draw(im)
  for i in range(6) :
    draw.line([(i*16, 0), (i*16, 65)], fill=255, width=2)
  for i in range(5) :
    draw.line([(0, i*16), (82, i*16)], fill=255, width=2)
  
  # 四角を左上の格子の中に描画
  draw.rectangle([6, 6, 12, 12], fill=255)
  
  del draw
  #im.save("moto2.png")

  return im


im = load_image2()

#
# Image.PERSPECTIVE
# 
# data : 8 tupples (a, b, c, d, e, f, g, h)
# 
# (x0, y0) =
#   ((ax+by+c)/(gx+hy+1), (dx+ey+f)/(gx+hy+1))
# 
# (1) 変換元から変換先に変換するパラメータ取得処理
# 変換元座標から変換先座標に変換する a,b,c,d,e,f を求める処理が
# ネットにあったのでそれを利用する。
#
# ただ、長方形を遠近感のある四角形に変形するには、
# 事前に変形後の座標を求める必要がある。
# 四角形の4点はでたらめに指定しても、
# 正しく遠近感のある四角形にならない気がする。
#
# (2) 変換先座標を求める処理
# 3次元(x,y,z)でのカメラの位置、透視投影スクリーンの位置、注目点等を与えて、
# カメラの位置を移動させることで変換先座標を求めることにする。
#
# 変換前のカメラの位置  :
#     (0, 0, 0)
# 変換前の画像の位置 :
#     カメラの位置から真正面にあるものとし、距離はnZ。z座標は、-nZ。
#     左上、左下、右下、右上のそれぞれの (x, y)は、下記とする。
#       (-width/2, -height/2)
#       ( width/2, -height/2)
#       ( width/2,  height/2)
#       (-width/2,  height/2)
# 変換前の注目点(視線とスクリーンの交差する座標) :
#     画像中央の位置とする。(0, 0, -nZ)
# 変換前の透視投影スクリーン :
#     変換前の画像と一致しているものとする。
#     べたっとくっついてる状態で枠も画像サイズと同じ。
# 
# 上記の設定で、以下のカメラの移動を行い、座標変換(x1,y1,z1系)を行う。
# - カメラ位置の移動。(0, 0, 0) -> (ex, ey, ez)。
# - カメラの上方向のベクトルを指定。(ux, uy, uz)。通常、(0, 1, 0)。
# - 目標点の移動。(tx, ty, tz)。画像中央にする場合は、(0, 0, -nZ)。
# (補足) この変換を視野変換というらしい。
# (参考) : http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090902
# 
# 視野変換後、カメラ位置から目標点(z軸)方向に透視投影スクリーンがあるものとして、
# その透視投影スクリーンに投影された画像の4隅の座標を取得する。(x2,y2,z2系)
# なお、描画空間の奥行が必要だが適当に以下にしておく。
#   nZ + max(width,height) * 2 + カメラ位置のz移動量としておく。
#
# (補足) この変換を透視投影変換というらしい。
# (参考) : http://marupeke296.com/DXG_No70_perspective.html
#
# この透視投影変換で得られた座標を(1)の変換後の座標として使用する。
#


#######################################################
# 関数
#######################################################

##############################################
# a b c d e f g の係数を取得する関数
# pa : 変換先の座標系
# pb : 現在の座標系
# コピー元:
# http://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil
##############################################
def find_coeffs(pa, pb):
    matrix = []
    for p1, p2 in zip(pa, pb):
        matrix.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]])
        matrix.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]])

    A = np.matrix(matrix, dtype=np.float)
    B = np.array(pb).reshape(8)

    res = np.dot(np.linalg.inv(A.T * A) * A.T, B)
    ret = np.array(res).reshape(8)
    wk = []
    for v in ret :
      wk.append(round(v, 3))
    #print("coeffs", wk)
    (alpha,beta) = (wk[6]+1, wk[7]+1)
    (x0,y0) = (wk[2], wk[5])
    (x1,y1) = (round((wk[0]+x0)/alpha,3), round((wk[3]+y0)/alpha,3))
    (x2,y2) = (round((wk[1]+x0)/beta,3), round((wk[4]+y0)/beta,3))
    #print("alpha,beta", wk[6]+1,wk[7]+1, "x0,y0", wk[2],wk[5])
    #print("x1,y1", x1, y1, "x2,y2", x2,y2)
    #print("pa", pa)
    #print("pb", pb)
    
    return ret

# 視野変換
def getSiyaHenkanMatrix(ev, tv, uv) :
  k1 = np.sqrt(np.power(ev[0]-tv[0],2) +
               np.power(ev[1]-tv[1],2) +
               np.power(ev[2]-tv[2],2))
  
  zv = ((ev[0]-tv[0])/k1, (ev[1]-tv[1])/k1, (ev[2]-tv[2])/k1)
  #print("zv", zv)
  
  wv = np.cross(uv, zv)
  k2 = np.sqrt(np.power(wv[0], 2) +
               np.power(wv[1], 2) +
               np.power(wv[2], 2))
  xv = wv / k2
  yv = np.cross(zv, xv)
  
  A = np.matrix([
    [xv[0], xv[1], xv[2], -ev[0]*xv[0]-ev[1]*xv[1]-ev[2]*xv[2]],
    [yv[0], yv[1], yv[2], -ev[0]*yv[0]-ev[1]*yv[1]-ev[2]*yv[2]],
    [zv[0], zv[1], zv[2], -ev[0]*zv[0]-ev[1]*zv[1]-ev[2]*zv[2]],
    [0.0  , 0.0  , 0.0  , 1.0                                 ]
  ], dtype=float)
  return A.T

# 透視投影変換
#  (x y z 1)*M = (x' y' z' 1)
#  P = (x'/z y'/z z' 1)
def getTosiHenkanMatrix(size, nZ, ev) :
  (w, h) = (float(size[0]), float(size[1]))
  fZ = float(nZ) + max(w, h) * 2.0 + float(ev[2])
  cot_h_theta = 2.0 * float(nZ) / w / (h/w)
  print("cot_h_theta:", cot_h_theta, "nZ:", nZ, "fZ:", fZ)
  B = np.matrix([
    [h/w * cot_h_theta, 0.0        , 0.0        , 0.0],
    [0.0              , cot_h_theta, 0.0        , 0.0],
    [0.0              , 0.0        , -1/(fZ-nZ) , 0.0],
    [0.0              , 0.0        , -nZ/(fZ-nZ), 1.0]
  ])
  return B


#######################################################
# サンプルのメイン
#######################################################

#----------------------------------
# 1. 直接指定する場合
#----------------------------------
# 右端を起点に左に視線を向ける
(w, h) = im.size

# 魔法の関数を呼ぶ。
coeffs = find_coeffs(
        [(0, 0), (w/2, w/4), (w/2, h - w/4), (0, h)],
        [(0, 0), (w  ,  0 ), (w  , h)      , (0, h)])

print("coeffs:", coeffs)

subnm = "directly"
im2 = im.transform(im.size, Image.PERSPECTIVE, coeffs, Image.BILINEAR)
filenm = args.output + "_" + subnm + ".png"
print("filenm:", filenm)
im2.save(filenm)


#----------------------------------
# 2. 視野変換、透視投影変換を行う場合
#----------------------------------
#------------------------------------
# 視野変換、透視投影変換のパラメータ
# nZ : 透視投影スクリーンまでの距離
# ev : カメラの移動ベクトル
# tv : 注目点の移動ベクトル
# uv : カメラの上ベクトル
#------------------------------------
nZ = im.size[0]/2
ev = (-im.size[0]/2, 0,  im.size[0]/4)

tv = (im.size[0]/5, 0, -nZ)
uv = (0, 1, 0)

#---------------------

params = (nZ, ev[0],ev[1],ev[2],tv[0],tv[1],tv[2],uv[0],uv[1],uv[2])

A = getSiyaHenkanMatrix(ev, tv, uv)
print("A:",A)

B = getTosiHenkanMatrix(im.size, nZ, ev)
print("B:",B)

(w, h) = (float(im.size[0]), float(im.size[1]))
P = np.matrix([
  [-w/2, -h/2, -nZ, 1],
  [ w/2, -h/2, -nZ, 1],
  [ w/2,  h/2, -nZ, 1],
  [-w/2,  h/2, -nZ, 1]
], dtype=float)

Pv = P * A
Pwk = P * A * B
#print("P:",P)
#print("Pwk:",Pwk)
#print("P * A", P * A)

Pn = []
for i in range(Pwk.shape[0]) :
  Pn.append((int(round(Pwk[i, 0]/(-Pv[i,2])*w/2 + w/2)),
             int(round(Pwk[i, 1]/(-Pv[i,2])*h/2 + h/2))))
  
  #print("p1:", Pwk[i, :])
  #print(" p1x/z:", Pwk[i, 0]/(-Pv[i,2]), "p1y/z:", Pwk[i,1]/(-Pv[i,2]))
  #print(" p1x' :", Pwk[i, 0]/(-Pv[i,2])*w/2,
  #       "p1y' :", Pwk[i, 1]/(-Pv[i,2])*h/2)


print("Pn:", Pn)

# 魔法の関数を呼ぶ。
coeffs = find_coeffs(
        [Pn[0], Pn[1], Pn[2], Pn[3]],
        [(0, 0), (w, 0), (w, h), (0, h)]
)

#print("coeffs:", coeffs)

subnm = "bysitenido"
im2 = im.transform(im.size, Image.PERSPECTIVE, coeffs, Image.BICUBIC)
filenm = args.output + "_" + subnm + "_" + "_".join(map(str, params)) + ".png"
print("filenm:", filenm)
im2.save(filenm)

変換元の画像

f:id:umejan:20160427223126p:plain ※ふちが白枠。

変換後の画像

  • 直接変形
    f:id:umejan:20160427223204p:plain

  • 視野変換、透視投影変換して変形
    f:id:umejan:20160427223222p:plain

コメント

このImage.PERSPECTIVEにはまる。

最初、変換後の座標を適当に決めてa,b,c,d,e,f,g,hを求めようとしたが、 何度も解を間違え、あげくには検証するための計算も間違っていたという情けないことに。

行列の計算もあやしく、行列の計算で大きな勘違いもあった。

あらためて自分が数学が苦手であることを認識させられた。勉強すれば何とかなる部分があるにしても、集中力の足りなさを実感する。

それにしても、このa,b,c,d,e,f,g,hを計算する処理をネットにあげた人に感謝である。 これがなければあきらめていた。

関連

PILのImage.transformの使い方 (EXTENT) - umejanのブログ
PILのImage.transformの使い方 (AFFINE) - umejanのブログ
PILのImage.transformの使い方 (QUAD, MESH) - umejanのブログ

PILのImage.transformの使い方 (QUAD, MESH)

PILのImage.transformの使い方 (AFFINE) - umejanのブログのつづき。

Image.QUADの変形

ランベルトの四角形の変形?

wikipediaの"平行線公準"に記載のあった四角形だと思う。

Image.QUADのdata指定方法

8 tupples (x0, y0, x1, y1, x2, y2, x3, y3)

四角形の左上、左下、右下、右上の角を指定する。

Image.QUADのサンプル

Image.MESHのサンプルに含めた。

Image.MESHの変形

Image.QUADを連続して実行する。

たぶん、格子状に画像を繰り返すのに使うと思う。

ほかの使い道は浮かばず。

Image.MESHのdata指定方法

出力域のサイズとQUADのdataと同じパラメータのリスト。

こんな感じで指定する。

[
  [(width1, height1, (x1_0, y1_0, x1_1, y1_1, x1_2, y1_2, x1_3, y1_3)],
  [(width2, height2, (x2_0, y2_0, x2_1, y2_1, x2_2, y2_2, x2_3, y2_3)],
  ...
]

Image.QUADとImage.MESHのサンプル

画像の右端を引き伸ばしたサンプル(QUAD)と
画像の右端を縮めたサンプル(QUAD)と
画像を格子状に4つ繰り返したサンプル(MESH)。

# -*- coding: utf-8 -*-
from __future__ import print_function
from PIL import Image
from PIL import ImageDraw
import numpy as np
import argparse

parser = argparse.ArgumentParser(description="Image.transformのサンプル")

parser.add_argument("--output","-o", required=True, type=str
                   , help="出力ファイル")

args = parser.parse_args()

# 変換元の画像をロード
def load_image() :
  #im = Image.open("xxxx.png")

  # サンプルとして、41x33の画像を作成。グレースケール256階調
  # 8ドットごとに格子を描画
  im = Image.new("L", (41, 33), color=0)
  draw = ImageDraw.Draw(im)
  for i in range(6) :
    draw.line([(i*8, 0), (i*8, 32)], fill=255, width=1)
  for i in range(5) :
    draw.line([(0, i*8), (40, i*8)], fill=255, width=1)
  
  # 四角を左上の格子の中に描画
  draw.rectangle([3, 3, 6, 6], fill=255)
  
  del draw

  return im


im = load_image()


#
# Image.QUAD
# 
# data : 8 tupples (x0, y0, x1, y1, x2, y2, x3, y3)
# 左上、左下、右下、右上の角を指定
# 
# ランベルトの四角形
# 
subnm = "quad"
data = (0, 0, 0, 33, 41, 33, 32, 10) # ランベルトの四角形?
im2 = im.transform(im.size, Image.QUAD, data, Image.BILINEAR)

filenm = args.output + "_" + subnm + "_" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)

im2.save(filenm)

subnm = "quad2"
data = (0, 0, 0, 33, 41, 33, 51, -10) # ランベルトの四角形の逆パターン
im2 = im.transform(im.size, Image.QUAD, data, Image.BILINEAR)

filenm = args.output + "_" + subnm + "_" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)

im2.save(filenm)

#
# Image.MESH
# 
# data : 以下のリスト
#  出力域のboxと
#  4隅(x1, y1, x2, y2, x3, y3, x4, y4)
#  例)
#   [
#    ( 0, 0, 20, 33), (0, 0, 0, 33, 41, 33, 32, 10),
#    (21, 0, 41, 33), (0, 0, 0, 33, 28, 20, 41, 0)
#   ]
# 
# QUADの処理を繰り返す。
# 使い方としては、格子状に画像を繰り返したりするくらいか。
#

subnm = "mesh"
print("im.size",im.size)
data = [
    [( 0,  0, 20, 16), (0, 0, 0, 33, 41, 33, 41, 0)],
    [(21,  0, 41, 16), (0, 0, 0, 33, 41, 33, 41, 0)],
    [( 0, 17, 20, 33), (0, 0, 0, 33, 41, 33, 41, 0)],
    #[(21, 17, 41, 33), (0, 0, 0, 33, 41, 33, 32, 10)] 変形して適用することも可能
    [(21, 17, 41, 33), (0, 0, 0, 33, 41, 33, 41, 0)]
]

#im2 = im.transform(im.size, Image.MESH, data, Image.BILINEAR)
im2 = im.transform(im.size, Image.MESH, data, Image.NEAREST)

filenm = args.output + "_" + subnm + "_" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)

im2.save(filenm)

変換元の画像

f:id:umejan:20160424202553p:plain ※ふちが白枠。

変換後の画像

  • 画像の右端を引き伸ばしたサンプル(QUAD)
    f:id:umejan:20160426185038p:plain
  • 画像の右端を縮めたサンプル(QUAD)
    f:id:umejan:20160426185051p:plain
  • 画像を格子状に4つ繰り返したサンプル(MESH)
    f:id:umejan:20160426185105p:plain

関連

PILのImage.transformの使い方 (EXTENT) - umejanのブログ
PILのImage.transformの使い方 (AFFINE) - umejanのブログ
PILのImage.transformの使い方 (PERSPECTIVE) - umejanのブログ

PILのImage.transformの使い方 (AFFINE)

PILのImage.transformの使い方 (EXTENT) - umejanのブログのつづき。

Image.AFFINEの変形

拡大縮小、回転、移動、平行四辺形などに変換できる。

ネットでAFFINE変換を検索するとどんな変換かわかる。

Image.AFFINEのdata指定方法

6 tupples(a, b, c, d, e, f)

以下の要領で変換する。

x0 = ax + by + c
y0 = dx + ey + f

(x,y)は変換先の座標。(x0,y0)は変換元の座標。

で、以下の行列を使った式で表現できる。

{
  \begin{pmatrix}
   x0 \\
   y0 \\
   1
 \end{pmatrix}
    = 
    \begin{pmatrix}
      a & b & c \\
      d & e & f \\
      0 & 0 & 1
    \end{pmatrix}
  \begin{pmatrix}x \\ y \\ 1 \end{pmatrix}
}

元の画像から見る場合は、この行列の逆行列を求めればいい。

Image.AFFINEのサンプル

平行四辺形に変形するサンプルと
拡大された画像を必要に応じて縮小して画像サイズに収めるサンプル。

# -*- coding: utf-8 -*-
from __future__ import print_function
from PIL import Image
from PIL import ImageDraw
import numpy as np
import math
import argparse

parser = argparse.ArgumentParser(description="Image.transformのサンプル")

parser.add_argument("--output","-o", required=True, type=str
                   , help="出力ファイル")

args = parser.parse_args()

# 変換元の画像をロード
def load_image() :
  #im = Image.open("xxxx.png")

  # サンプルとして、41x33の画像を作成。グレースケール256階調
  # 8ドットごとに格子を描画
  im = Image.new("L", (41, 33), color=0)
  draw = ImageDraw.Draw(im)
  for i in range(6) :
    draw.line([(i*8, 0), (i*8, 32)], fill=255, width=1)
  for i in range(5) :
    draw.line([(0, i*8), (40, i*8)], fill=255, width=1)
  
  # 四角を左上の格子の中に描画
  draw.rectangle([3, 3, 6, 6], fill=255)
  
  del draw
  return im


im = load_image()


#
# Image.AFFINE
# 
# data : 6 tupples(a, b, c, d, e, f)
# 
# AFFINE変換
# 座標(ax + by + c, dx + ey + f)で変換
#   f(x) = Ax + t (Aは行列。x, tはベクトル)
#   写像先平面から見た元の平面への行列かな?
#
# たぶん、下記のイメージ。
# (x0) = ( a b )(xnew) + (c)
# (y0)   ( d e )(ynew)   (f)
# 
# 元の座標系から見た場合だと
# 写像先への座標は
# (xnew) = ( a0 b0 )(x0) + (c0)
# (ynew)   ( d0 e0 )(y0)   (f0)
# 
# detA0 = det(A^(-1)) = a0*e0 - b0*d0 とすると # (行列式)
#
# AはA^(-1)なので、
# a = 1/detA0 * d0
# b = 1/detA0 * b0 * (-1)
# d = 1/detA0 * d0 * (-1)
# e = 1/detA0 * a0
# 
# (c, f)^Tは-A^(-1)*tなので
# c = 1/detA0 * ( -e0*c0 + b0*f0 )
# f = 1/detA0 * (  d0*c0 - a0*f0 )
# 
# 上記はnumpyで簡単に計算可能
# 
# なお、[[a, b, c],[d,e,f],[0,0,1]]の行列で上記の計算が可能。
# こんなイメージ
# (x0) = ( a b c )(xnew)
# (y0)   ( d e f )(ynew)
# (1 )   ( 0 0 1 )(1)   
# 

#ptn1 縦軸が斜め (b<0の時左回転。b>0の時右回転)
subnm = "tate_naname"
data = (1.25, -0.25, 0, 0, 1, 0)
im2 = im.transform(im.size, Image.AFFINE, data, Image.BILINEAR)

filenm = args.output + "-" + subnm + "-" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)
im2.save(filenm)

#ptn1 横軸が斜め (d<0の時右回転。d>0の時左回転)
subnm = "yoko_naname"
data = (1, 0, 0, -0.25, 1.25, 0)
im2 = im.transform(im.size, Image.AFFINE, data, Image.BILINEAR)

filenm = args.output + "-" + subnm + "-" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)
im2.save(filenm)

#ptn3 縦横軸が斜め
subnm = "tateyoko_naname"
data = (1.3, -0.3, 0, -0.3, 1.3, 0)
im2 = im.transform(im.size, Image.AFFINE, data, Image.BILINEAR)

filenm = args.output + "-" + subnm + "-" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)
im2.save(filenm)


#
# AFFINE変換(2)
# 元の画像をはみ出さないようにする場合
# -> 元の画像サイズからはみ出したら、
#    それに応じて縮小する
#
# 画像の4隅がはみ出していたら、
# 必要に応じて移動、縮小すればいいはず。
# 
# 以下の方針で対応することにする場合
#  - マイナス座標であれば、+方向に移動させる。
#  - 移動させたあと、x軸、y軸のMaxを調べ、
#    元のx軸、y軸のMaxと比較する。
#    大きければ、x軸、y軸の大きい方(元のxy比を考慮)を
#    規準に作成する画像サイズを決定する。
#  - 画像サイズが元と異なる場合は、元の画像サイズにリサイズする。
#
# detAがゼロの場合どうするか。
# -> よくわからないので元の画像サイズのままとする。
#     そもそも、点か線にしかならないので、
#     与えられたパラメータが悪いと考えておくことにする。
# 

subnm = "hamidasi_nasi"
#data = (1.5, -0.3, 0, -0.3, 1.3, 0)
data = (0.8, -0.3, 0, -0.3, 1.3, 0)

(a, b, c, d, e, f) = data
detA = (float)(a * e - b * d)
print("detA:", detA)

if detA == 0.0 :
  raise Exception, "このパラメータは処理できません。"

#
#a0 =   e / detA 
#b0 = - b / detA 
#d0 = - d / detA 
#e0 =   a / detA 
#
#c0 = ( -e*c + b*f ) / detA
#f0 = (  d*c - a*f ) / detA
#
#
#A0 = np.matrix([[a0, b0, c0], [d0, e0, f0], [0, 0, 1]])
#print("A0:", A0)

# 変換後の点を取得したいので、下記の行列で計算させる。
A = np.matrix([[a, b, c], [d, e, f], [0, 0, 1]])
invA = np.linalg.inv(A) # 変換元から変換先の行列がほしいので。
print("invA:", invA)

# 画像の4隅の点
Points = np.matrix([
  [0         , 0         , 1],
  [im.size[0], 0         , 1],
  [0         , im.size[1], 1],
  [im.size[0], im.size[1], 1]
]).T
print("Points:", Points)

# invA x 画像の4隅の点 で変換後の4点を計算。
PointsNew = invA * Points

print("PointsNew:", PointsNew)
p_min = PointsNew.min(1) # x軸、y軸のそれぞれのminを取得。min(0)だとx,yのminを取得する意味になる。
p_max = PointsNew.max(1) # x軸、y軸のそれぞれのmaxを取得。
print("p_min:", p_min)
print("p_max:", p_max)
x_min = math.floor(p_min[0])
x_max = math.ceil(p_max[0])
y_min = math.floor(p_min[1])
y_max = math.ceil(p_max[1])
print("x(min,max):", x_min, x_max, "y(min,max)", y_min, y_max)

x_shift = 0
y_shift = 0

if x_min < 0 :
  x_shift = -x_min
if y_min < 0 :
  y_shift = -y_min

hamidashi = 0
if x_max + x_shift > im.size[0] :
  hamidashi = 1
if y_max + y_shift > im.size[1] :
  hamidashi = 1

y_p_x = im.size[1] / float(im.size[0])

size_wk = [im.size[0], im.size[1]]

if hamidashi == 1 :
  if (x_max + x_shift) > float(y_max + y_shift) / y_p_x :
    size_wk[0] = int(x_max + x_shift)
    size_wk[1] = int(math.ceil(im.size[1]*(x_max + x_shift)/im.size[0]))
  else :
    size_wk[0] = int(math.ceil(im.size[0]*(y_max + y_shift)/im.size[1]))
    size_wk[1] = int(y_max + y_shift)

print("size_wk:", size_wk)
print("x_shift,y_shift:", x_shift, y_shift)

# 移動
invA_Add = np.matrix([[0, 0, x_shift], [0, 0, y_shift], [0, 0, 0]])
B = invA + invA_Add
invB = np.linalg.inv(B)
print("invB", invB)

im2 = im.transform((size_wk[0], size_wk[1]), Image.AFFINE,
                   (a,b,invB[0,2],d,e,invB[1,2]), Image.BILINEAR)

if im2.size[0] == im.size[0] and im2.size[1] == im.size[1] :
  filenm = args.output + "_" + subnm + "_" + "_".join(map(str, data)) + ".png"
  im2.save(filenm)
else :
  filenm = args.output + "-t_" + subnm + "_" + "_".join(map(str, data)) + ".png"
  im3 = im2.resize(im.size)
  im3.save(filenm)


変換元の画像

f:id:umejan:20160424202553p:plain ※ふちが白枠。

変換後の画像

  • 縦軸が斜め
    f:id:umejan:20160425123232p:plain
  • 横軸が斜め
    f:id:umejan:20160425123243p:plain
  • 縦横軸が斜め
    f:id:umejan:20160425123253p:plain

関連

PILのImage.transformの使い方 (EXTENT) - umejanのブログ

PILのImage.transformの使い方 (QUAD, MESH) - umejanのブログ

PILのImage.transformの使い方 (PERSPECTIVE) - umejanのブログ


PILのImage.transformの使い方 (EXTENT)

先日、pythonで画像を変形させてみたいと思った。まあ、バッチで処理したかったので。

調べてみたらImage.transformという関数があるのを知るが、 使ってみようとしたところ、どんなパラメータを指定したらいいのかわからず途方にくれる。(泣)

ネットで調べたりして、なんとか使えるレベルまでになったので Image.transform の使い方を整理してみた。

とりあえず、今回は概要とEXTENTの使い方。

Image.transformの概要

reference

Image Module — Pillow (PIL Fork) 3.1.1 documentation

自分の環境では、Python2.7.6でPillowが2.3.0だが、ここで問題ないと思われるので。

Pillowに限らず、Python周りのバージョンとドキュメントの関連性の知識が足りずこれでいいのか判断できない。あとでPythonのドキュメントを見ておかないと。

変形方法の種類 (method)

methodというパラメータで指定するが、以下の5つがある。

  • PIL.Image.EXTENT
    矩形を切り取って拡大。

  • PIL.Image.AFFINE
    AFFINE変換。移動、拡大、縮小、回転、反転ができる。

  • PIL.Image.PERSPECTIVE
    遠近感のある画像に変換。

  • PIL.Image.QUAD
    ランベルトの四角形っていうやつの変形と思われる。 端がよれて"くる"ってなってる紙とかの写真を四角形に補正するために使われるのだと思う。

  • PIL.Image.MESH
    QUADを連続して行うためのもの。

変形のパラメータ (data)

dataで変形のパラメータを指定する。

methodの変形方法に合わせて指定する必要がある。

ここがpillowのリファレンスに記載がなく困った。

ネットで検索したところ以下がヒット。これを参考にした。

The Image Module

描画時の補間の方法。filter。 (resample)

変形するときに描画する点を埋め合わせないといけないケースがあるが、 その方法を指定する。
以下の3つがある。

  • PIL.Image.NEAREST
    デフォルト。となりの点を見て補間。2値画像や"P"モードの場合、これしか指定できない。 "P"モードとはパレットを使うモードらしいが、よくわからず。

  • PIL.Image.BILINEAR
    2x2で見て補間。

  • PIL.Image.BICUBIC
    4x4で見て補間。

Image.EXTENT の変形

Image.EXTENTのdata指定方法。

4 tupples (x0, y0, x1, y1)

Image.EXTENTのサンプル

# -*- coding: utf-8 -*-
from __future__ import print_function
from PIL import Image
from PIL import ImageDraw
import numpy as np
import argparse

parser = argparse.ArgumentParser(description="Image.transformのサンプル")

parser.add_argument("--output","-o", required=True, type=str
                   , help="出力ファイル")

args = parser.parse_args()

# 変換元の画像をロード
def load_image() :
  #im = Image.open("xxxx.png")

  # サンプルとして、41x33の画像を作成。グレースケール256階調
  # 8ドットごとに格子を描画
  im = Image.new("L", (41, 33), color=0)
  draw = ImageDraw.Draw(im)
  for i in range(6) :
    draw.line([(i*8, 0), (i*8, 32)], fill=255, width=1)
  for i in range(5) :
    draw.line([(0, i*8), (40, i*8)], fill=255, width=1)
  
  # 四角を左上の格子の中に描画
  draw.rectangle([3, 3, 6, 6], fill=255)
  
  del draw

  return im


im = load_image()

#
# Image.EXTENT
# 
# data : 4 tupples (x0, y0, x1, y1)
# 
# dataで矩形を指定して切り取る。
# 
subnm = "nearest"
data = (0, 5, 20, 20)
im2 = im.transform(im.size, Image.EXTENT, data, Image.NEAREST)

filenm = args.output + "-" + subnm + "-" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)
im2.save(filenm)

subnm = "bilinear"
data = (0, 5, 20, 20)
im2 = im.transform(im.size, Image.EXTENT, data, Image.BILINEAR)

filenm = args.output + "-" + subnm + "-" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)
im2.save(filenm)

subnm = "bicubic"
data = (0, 5, 20, 20)
im2 = im.transform(im.size, Image.EXTENT, data, Image.BICUBIC)

filenm = args.output + "-" + subnm + "-" + "_".join(map(str, data)) + ".png"
print("filenm:", filenm)
im2.save(filenm)

変換元の画像

f:id:umejan:20160424202553p:plain ※ふちが白枠。

変換後の画像

  • resample=Image.NEAREST
    f:id:umejan:20160424202632p:plain

  • resample=Image.BILINEAR
    f:id:umejan:20160424202654p:plain

  • resample=Image.BICUBIC
    f:id:umejan:20160424202621p:plain

コメント

EXTENTはそのままなので簡単。

関連

PILのImage.transformの使い方 (AFFINE) - umejanのブログ

PILのImage.transformの使い方 (QUAD, MESH) - umejanのブログ

PILのImage.transformの使い方 (PERSPECTIVE) - umejanのブログ


OpenCV3.1.0のインストールメモ (Ubuntu 14.04 LTS)

先日、あるpythonのサンプルでcv2をimportしていたのでubuntu14.04にインストールした。 そのときのメモ。

OpenCV3.1.0のインストール

このページ(Ubuntu14.04にpython用にOpenCV3.0.0をインストール - Qiita)を参考に実施した。

zip入手

cd ~/work/download
wget http://downloads.sourceforge.net/project/opencvlibrary/opencv-unix/3.1.0/opencv-3.1.0.zip
unzip opencv-3.1.0.zip

requirementのインストール

sudo apt-get install build-essential libgtk2.0-dev libjpeg-dev libtiff4-dev libjasper-dev libopenexr-dev cmake python-dev python-numpy python-tk libtbb-dev libeigen3-dev yasm libfaac-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev libx264-dev libqt4-dev libqt4-opengl-dev sphinx-common texlive-latex-extra libv4l-dev libdc1394-22-dev libavcodec-dev libavformat-dev libswscale-dev default-jdk ant libvtk5-qt4-dev

cmake

mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D WITH_TBB=ON -D BUILD_NEW_PYTHON_SUPPORT=ON -D WITH_V4L=ON -D WITH_FFMPEG=OFF -D BUILD_opencv_python2=ON ..

make -j4
sudo make install
sudo checkinstall
sudo sh -c 'echo "/usr/local/lib" > /etc/ld.so.conf.d/opencv.conf'
sudo ldconfig
echo "OpenCV ready to be used"

# cv2.soが /usr/local/lib/python2.7/site-packages/ にないので、
sudo cp ~/work/download/opencv-3.1.0/build/lib/cv2.so /usr/local/lib/python2.7/site-packages/

コメント

結局、ubuntuのcommnityページ(OpenCV - Community Help Wiki)の内容をそのままやればいいみたい。

CUDAとCaffeのインストール (Ubuntu 14.04 LTS)

CUDAとCaffeをインストールしたときの手順。

CUDA

※CUDAは、apt-getでなく、本家サイトからdebファイルを取得してやったほうがいいらしい。

ここ(CUDA 7.5 Downloads | NVIDIA Developer)から、debファイルをダウンロードする。

cuDNNについては、インストールするのをやめた。 ダウンロードサイトに、Organizationを入力する箇所があったので。。。 個人で使用する場合は、individualとか入れればいいのかな。

インストール

sudo dpkg -i cuda-repo-ubuntu1404-7-5-local_7.5-18_amd64.deb
sudo apt-get update
sudo apt-get install cuda

再起動

lsmod | grep nvidia

動作確認

cp -r /usr/local/cuda/samples ~
cd ~/samples
make

bin/x86_64/linux/release/deviceQuery

参考サイト

Ubuntu 14.04.3 LTS に Chainer をインストールする - 不確定特異点

Caffe

本家サイトのInstallation(Caffe | Installation)を参考にインストール。

Caffeにインストールが必要なものをapt-getでインストール

General dependencies

sudo apt-get install libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libhdf5-serial-dev protobuf-compiler
sudo apt-get install --no-install-recommends libboost-all-dev

BLAS (Install ATLAS)

sudo apt-get install libatlas-base-dev

Remaining dependencies, 14.04

sudo apt-get install libgflags-dev libgoogle-glog-dev liblmdb-dev

Caffe用のフォルダを作成とCaffeのダウンロード

「~/caffe_ws」に作成することにした。

mkdir caffe_ws
cd caffe_ws
git clone https://github.com/BVLC/caffe.git

PyCaffeに必要なもののインストール

Fortranが必要らしいので。gfortranを入れる。

sudo apt-get install gfortran

VirtualEnvでcaffe用のpython環境を作成

「~/caffe_ws/python」で作成することにした。

mkdir ~/caffe_ws/python
virtualenv --system-site-packages ~/caffe_ws/python
source ~/caffe_ws/python/bin/activate  # If using bash

簡単に使えるように、.bash_aliases に 「source .../activate」を登録しておく。

alias s_caffe="source ~/caffe_ws/python/bin/activate"

pipでpycaffeで必要なものをインストール

cd ~/caffe_ws/caffe/python
for req in $(cat requirements.txt); do pip install $req; done

deactivate

Caffeのコンパイル

cd ~/caffe_ws/caffe
cp Makefile.config.example Makefile.config

Makefile.configの編集

環境に合わせて修正する。 CUDAあり、CUDNNなしだと、ほぼ変更不要のようだ。

numpyが~/caffe_ws/caffe配下にあるので、64行目あたりを変更。

#PYTHON_INCLUDE := /usr/include/python2.7 \
#       /usr/lib/python2.7/dist-packages/numpy/core/include
# この部分を環境にあわせて変更

PYTHON_INCLUDE := /usr/include/python2.7 \
        /home/自分のアカウント/caffe_ws/python/lib/python2.7/site-packages/numpy/core/include

OpenCV3.1を入れてたので、「OPENCV_VERSION := 3」を有効にした。

make

make all -j4 #CPUのコア数で調整
make test
make runtest

pycaffeのインストール

s_caffe

cd ~/caffe_ws/caffe
make pycaffe
make distribute

export PYTHONPATH=~/caffe_ws/caffe/python:$PYTHONPATH

pycaffeのテスト

$> python
    
import caffe

pycaffe.cy でワーニングが出てるけど、そのうち対応するらしい。

caffeでMNIST

export CAFFE_ROOT=~/caffe_ws/caffe
    
cd $CAFFE_ROOT
./data/mnist/get_mnist.sh
./examples/mnist/create_mnist.sh

./examples/mnist/train_lenet.sh

train_lenet.shの実行時間は、だいたい3分20秒くらい。

参考サイト

素人がCaffeを使ってDeepLearningしてみた(導入編) - Qiita

コメント

chainerでCUDAを利用する場合、chainerインストール前にCUDAをインストールする必要があるとのこと。 インストールは先日のブログどおり。

chainerのMNIST exampleをgpuありで実行したところ、1分50秒くらい!(CPU Core i5, GPU:GPX750Ti)
gpuなしだと、2時間半くらいで、前回のノートPCのVirtualBox上では、1日ちょっとだったのでgpu必須でしょう。
gpuなしに比べて80倍くらい速いけど、なにか間違ってないか不安。
それと、AWSgpuインスタンスだと5倍くらいとどこかで書いてあった気がするけど、結構パフォーマンスが違うみたい。

それから、tensorflowについては、GPU利用する場合にCUDAとcuDNNのどちらも必要とのことでインストールしなかった。

関連

TensorflowとChainerのインストールの覚え書き - umejanのブログ

Windows10のPCにUbuntu14.04 LTSを入れる(デュアルブート)

機械学習するにはNVIDIAGPUが必要だと実感したので、 衝動買いに近い感覚でミニタワーのPCを購入してしまった。

ということで、Windows10のPCにUbuntu 14.04 LTSをインストールしてデュアルブートできるようにしたときのメモを残しておく。

購入PC

ドスパラのMagnate MXというモデル。

CPU : Core i5-6400
Memory : 16GB
GPU : GPU NVIDIA GeForce GPX750 Ti 2GB

事前準備

回復ドライブの作成

参考サイト:

Windows 10にしても“回復ドライブ”さえあれば以前のOSに戻せます - 週刊アスキー

  1. コントロールパネル→システムとセキュリティ→ファイル履歴 を開く。
  2. 左下の関連項目に「回復」のリンクがあるのでクリック。
  3. 「高度な回復ツール」の画面が開くので、一番上の「回復ドライブの作成」をクリック。
  4. その後、ガイダンスに従い操作。

USB Diskが必要で8GB以上の容量が必要とのこと。PCによっては16GB以上とかもあるらしい。

使ってないSDカードがあったのでこれに入れた。

Ubuntu用の領域確保(ドライブ領域の縮小)

  1. 管理ツール→コンピュータの管理 を開く。
  2. 左のメニューの 記憶域→ディスク管理をクリック。
  3. Cドライブのところを右クリックすると、「ボリュームの縮小」があるのでこれをクリック。
  4. 縮小できるサイズを入力できる画面が表示されて、最大の値が初期表示されている。これでよければ縮小ボタンをクリック。

250GBのSSDで行ったが、Windows10でアップデートなどいくつか作業したせいか、確保できた領域は100GBちょっとだけだった。 見たところそんなに使ってなかったのに。

Ubuntuのインストールメディア作成

適当なUSBメモリがなかったので、

Ubuntu 14.04 LTS 日本語 Remix リリース | Ubuntu Japanese Team

から、isoファイルをダウンロードしてDVDに焼く。

Ubuntu 14.04 LTSのインストール

参考サイト:Ubuntu14.04 + Windows10 のデュアルブート環境を構築する - 俺とプログラミング

上記サイトを参考に実施。

  1. 高速スタートアップを無効にする。
  2. UbuntuインストーラDVDをセットする。
  3. Windowsを落として、PCを起動する。
  4. BIOSメニューに入る。
  5. DVDから起動するようにする。

  6. Ubuntuインストーラが動き、 起動メニューのところで、 「e」キーを押す。(起動オプションを変える)
    そのまま起動してしまうと、画面が真っ黒でなにもできない。

  7. 起動オプションが表示されるので、 「quiet splash」→「nomodeset」に変更する。
    参考サイト:Ubuntu日本語フォーラム / ubuntuインストール時の黒画面について

  8. インストーラを起動。

  9. インストールの種類でそれ以外を選択する。
  10. パーティション作成。基本パーティションでよさそうなのでこれで。 スワップはメモリの1から2倍を指定するらしいので、1.5倍で確保。

  11. 再起動後も、「e」キーで起動オプションを「quiet splash」→「nomodeset」に変更して起動。

  12. sudo apt-get update

  13. 以下のファイルを作成して、Nouveauを無効化。
    sudo vi /etc/modprobe.d/blacklist-nouveau.conf
    blacklist nouveau
    options nouveau modeset=0
    参考サイト:CUDA6.5/Ubuntu14.04 - labs.beatcraft.com

  14. sudo update-initramfs -u
    念のため再起動して画面が表示されることを確認。

  15. NVIDIA driverのインストール
    よくわからないが、apt-cacheで新しいものをインストール。
    sudo apt-get install nvidia-352
    念のため再起動して確かめた。これでいいようだ。

コメント

やっぱりビデオドライバ周りでまごついてしまった。 最新のUbuntuでやれば簡単にインストールできるかも。

それと、Ubuntu 14.04 LTS 日本語 Remixのインストーラで、その他インストールだとLVMの入れ方がわからなかった。 なので、素直にWindows10を別ドライブに移してから、まっさらのディスクにインストールしたほうがよさそう。

関連

Ubuntu 14.04 LTSインストールの覚え書き - umejanのブログ