はじめに
PySimpleGUIとOpenCVを使用してジグソーパズルを作成しました。
ダウンロードと実行方法
ダウンロード
ソースは、https://github.com/tostos5963/PySimpleGUI-JigsawPuzzle で公開していますのでダウンロードしてください。
Windowsの場合
pip install pysimplegui
python JigsawPuzzle.py
Linux(ubuntu)の場合
pip3 install pysimplegui
python3 JigswaPuzzle.py
以下のエラーが出た場合
ModuleNotFoundError: No module named 'tkinter'
python3-tk をインストールしてください
sudo apt-get install python3-tk
各ピース用のマスク画像作成
処理を簡単にするためにピース境界は以下のパターンのみとしました。
上下左右の各辺のパターンは、フラット、凹、凸の3パターンがあるので3の4乗=81パターンですが、向かい合う2辺がフラットになることはないので64パターンの画像ファイルをimages\jigsaw に置いています。
ファイル名は、上右下左の順にパターンに従って、F(Flat)、C(conCave 凹)、V(conVex 凸)のいずれかをつけています。以下の場合 FVVC.pngとなります。(チェック模様は透過)
主なクラスの処理
class MaskImage():マスク画像作成
マスク画像作成する。
def __init__(self, resize_x, resize_y, scale_x, scale_y) -> None:
self.keylist = ['CCCC', 'CCCF', 'CCCV', 'CCFC', 'CCFF', 'CCFV', 'CCVC', 'CCVF',
'CCVV', 'CFCC', 'CFCV', 'CFFC', 'CFFV', 'CFVC', 'CFVV', 'CVCC',
'CVCF', 'CVCV', 'CVFC', 'CVFF', 'CVFV', 'CVVC', 'CVVF', 'CVVV',
'FCCC', 'FCCF', 'FCCV', 'FCVC', 'FCVF', 'FCVV', 'FFCC', 'FFCV',
'FFVC', 'FFVV', 'FVCC', 'FVCF', 'FVCV', 'FVVC', 'FVVF', 'FVVV',
'VCCC', 'VCCF', 'VCCV', 'VCFC', 'VCFF', 'VCFV', 'VCVC', 'VCVF',
'VCVV', 'VFCC', 'VFCV', 'VFFC', 'VFFV', 'VFVC', 'VFVV', 'VVCC',
'VVCF', 'VVCV', 'VVFC', 'VVFF', 'VVFV', 'VVVC', 'VVVF', 'VVVV']
self.mask_info_tbl = {}
for key in self.keylist:
p_fname = f'images/jigsaw/{key}.png'
img_mask = cv2.imread(p_fname, cv2.IMREAD_COLOR)
sx = int(img_mask.shape[1] * resize_x * scale_x)
sy = int(img_mask.shape[0] * resize_y * scale_y)
img_mask_small = cv2.resize(img_mask, dsize=(sx, sy))
mask_h, mask_w, ch = img_mask_small.shape
mask_gray = cv2.cvtColor(img_mask_small, cv2.COLOR_BGR2GRAY)
mask_mono = cv2.threshold(mask_gray, 128, 255, cv2.THRESH_BINARY_INV)[1]
mask_bin = cv2.threshold(mask_gray, 128, 255, cv2.THRESH_BINARY)[1]
contours, hierarchy = cv2.findContours(mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
img_mask_mono = cv2.merge((mask_mono, mask_mono, mask_mono, mask_mono))
img_mask_mono_not = cv2.bitwise_not(img_mask_mono)
self.mask_info_tbl[key] = MaskImageInfo(img_mask_mono_not, contours, mask_w, mask_h)
ジグソーパズルの画像に合わせてリサイズ
リサイズした画像をグレースケールに変換
グレースケール画像を2値化、反転画像作成
2値化画像から輪郭検出。この輪郭はジグソーパズルのピースの輪郭を表示するときに使用する。(赤枠の画像は輪郭がわかるように作成したデバッグ用画像)
2値化画像をRGB+アルファチャンネル画像に変換。アルファチャンネルは透過度用のチャネル。
反転してマスク画像作成
class PieceNode(PieceObject):1ピース
ピース1つ(他のピースと接続されていないもの)の処理
def is_hit(self, pos)
ピース(透過部分を除く)がクリックされたか。
def is_hit(self, pos):
x, y = pos
if self.x <= x < (self.x + self.img_piece.shape[1]):
if self.y <= y < (self.y + self.img_piece.shape[0]):
color = self.img_piece[(y - self.y, x - self.x)]
if color[3] == 255:
return True
return False
def has_imgid(self, imgid):
imgid (PySimpleGUI の DrawImage の戻り値)が一致するか。
def has_imgid(self, imgid):
if self.img_id == imgid:
return True
else:
return False
def move(self, draw, dx, dy):
ピースをドラッグしたときに移動する。
def move(self, draw, dx, dy):
self.x += dx
self.y += dy
draw.MoveFigure(self.img_id, dx, dy)
def to_front(self, draw):
ピースを前面に移動する。
def to_front(self, draw):
draw.BringFigureToFront(self.img_id)
def adjacent_piece(self, piece):
ピースが接近したときに位置を自動調整する。
def adjacent_piece(self, piece):
if self.row == piece.row:
if (piece.y < (self.y - WITHIN_TOLERANCE)) or ((self.y + WITHIN_TOLERANCE) < piece.y):
return (False, 0, 0)
adj_y = self.y - (piece.last_y - self.last_y)
if (self.col - 1) == piece.col:
# [piece][self]
adj_x = self.x - (self.last_x - piece.last_x)
if (piece.x < (adj_x - WITHIN_TOLERANCE)) or ((adj_x + WITHIN_TOLERANCE) < piece.x):
return (False, 0, 0)
else:
return (True, adj_x - piece.x, adj_y - piece.y)
if (self.col + 1) == piece.col:
# [self][piece]
adj_x = self.x + (piece.last_x - self.last_x)
if (piece.x < (adj_x - WITHIN_TOLERANCE)) or ((adj_x + WITHIN_TOLERANCE) < piece.x):
return (False, 0, 0)
else:
return (True, adj_x - piece.x, adj_y - piece.y)
if self.col == piece.col:
if (piece.x < (self.x - WITHIN_TOLERANCE)) or ((self.x + WITHIN_TOLERANCE) < piece.x):
return (False, 0, 0)
adj_x = self.x - (piece.last_x - self.last_x)
if (self.row - 1) == piece.row:
# [piece]
# [self]
adj_y = self.y - (self.last_y - piece.last_y)
if (piece.y < (adj_y - WITHIN_TOLERANCE)) or ((adj_y + WITHIN_TOLERANCE) < piece.y):
return (False, 0, 0)
else:
return (True, adj_x - piece.x, adj_y - piece.y)
if (self.row + 1) == piece.row:
# [self]
# [piece]
adj_y = self.y + (piece.last_y - self.last_y)
if (piece.y < (adj_y - WITHIN_TOLERANCE)) or ((adj_y + WITHIN_TOLERANCE) < piece.y):
return (False, 0, 0)
else:
return (True, adj_x - piece.x, adj_y - piece.y)
return (False, 0, 0)
class PieceComposite(PieceObject)):複数ピース
接続されているピースを処理するクラス
def is_hit(self, pos):
ピース(透過部分を除く)がクリックされたか。
def is_hit(self, pos):
hit = False
for piece in self.pieces:
if piece.is_hit(pos):
hit = True
break
return hit
def has_imgid(self, imgid):
imgid (PySimpleGUI の DrawImage の戻り値)が一致するか。
def has_imgid(self, imgid):
for piece in self.pieces:
if piece.has_imgid(imgid):
return True
return False
def add(self, piece):
ピースを追加する。
def add(self, piece):
self.pieces.append(piece)
def move(self, draw, dx, dy):
ドラッグしたときに接続されているピースを全て移動する。
def move(self, draw, dx, dy):
for piece in self.pieces:
piece.move(draw, dx, dy)
def to_front(self, draw):
接続されているピース全て前面に移動する。
def to_front(self, draw):
for piece in self.pieces:
piece.to_front(draw)
def connect(self, draw, pieces2) -> bool:
ピースを接続する。
def connect(self, draw, pieces2) -> bool:
can_connect, dx, dy = (False, 0, 0)
for p1 in self.pieces:
for p2 in pieces2.pieces:
can_connect, dx, dy = p1.adjacent_piece(p2)
if can_connect:
break
if can_connect:
break
if can_connect:
for p2 in pieces2.pieces:
p2.move(draw, dx, dy)
self.add(p2)
return True
return False
class Jigsaw():ジグソーパズル
ジグソーパズル
def add(self, img_id, piece):
ピースを追加
def add(self, img_id, piece):
self.piece_tbl[img_id] = piece
def click_down(self, pos):
マウス押下時の処理。クリック位置にピースがあればドラッグ開始する。
def click_down(self, pos):
self.x1, self.y1 = pos
if self.first_click:
self.first_click = False
self.x0, self.y0 = (self.x1, self.y1)
ids = self.draw.GetFiguresAtLocation((self.x1, self.y1))
is_hit = False
if len(ids) >= 1:
for imgid in reversed(ids):
is_hit = False
for id in self.piece_tbl:
if self.piece_tbl[id].has_imgid(imgid):
if self.piece_tbl[id].is_hit((self.x1, self.y1)):
self.drag_img_id = id
try:
self.drag_img_ids.remove(id)
except Exception:
pass
self.drag_img_ids.append(id)
self.piece_tbl[id].to_front(self.draw)
is_hit = True
break
if is_hit:
break
if is_hit == False:
self.drag_img_id = 0
else:
if self.drag_img_id in self.piece_tbl:
dx, dy = self.x1 - self.x0, self.y1 - self.y0
self.x0, self.y0 = (self.x1, self.y1)
self.piece_tbl[self.drag_img_id].move(self.draw, dx, dy)
def click_up(self):
マウスボタンを離した時の処理。ドラッグ終了。接続可能なら接続する。
def click_up(self):
self.first_click = True
if len(self.drag_img_ids) <= 0:
return
if len(self.drag_img_ids) >= 3:
self.drag_img_ids.pop(0)
if len(self.drag_img_ids) <= 1:
img_id_prev = 0
img_id_now = self.drag_img_ids[0]
else:
img_id_prev = self.drag_img_ids[0]
img_id_now = self.drag_img_ids[1]
if img_id_prev != 0 and img_id_prev != img_id_now:
if self.piece_tbl[img_id_prev].connect(self.draw, self.piece_tbl[img_id_now]):
del self.piece_tbl[img_id_now]
self.drag_img_ids.remove(img_id_now)
return
max_pieces_img_id = 0
num_max = 0
for img_id in self.piece_tbl:
if num_max < self.piece_tbl[img_id].num_pieces():
max_pieces_img_id = img_id
num_max = self.piece_tbl[img_id].num_pieces()
if max_pieces_img_id != 0 and max_pieces_img_id != img_id_now:
if self.piece_tbl[max_pieces_img_id].connect(self.draw, self.piece_tbl[img_id_now]):
del self.piece_tbl[img_id_now]
self.drag_img_ids.remove(img_id_now)
class Board():メイン
def click_down(self, pos):
マウス押下時の処理。
def click_down(self, pos):
if self.jigsaw != None:
self.jigsaw.click_down(pos)
def click_up(self):
マウスボタンを離した時の処理。完成していたら「complete」を表示する。
def click_up(self):
if self.jigsaw != None:
self.jigsaw.click_up()
if self.is_complte != True:
if self.jigsaw.num_blocks() <= 1:
self.is_complte = True
sg.popup('complete!', keep_on_top=True)
def onclick_start(self):
スタートボタン押下時の処理。
選択された画像をウィンドウサイズに合わせてリサイズします。
try:
org_img = cv2.imread(img_file)
img_pic_h, img_pic_w, _ = org_img.shape
sx = float(self.disp_image_width / img_pic_w)
sy = float(self.disp_image_height / img_pic_h)
if sx < sy:
self.img_pic = cv2.resize(org_img, dsize=None, fx=sx, fy=sx)
else:
self.img_pic = cv2.resize(org_img, dsize=None, fx=sy, fy=sy)
img_pic_h, img_pic_w, _ = self.img_pic.shape
resize_x = img_pic_w / float(IMAGE_WIDTH)
resize_y = img_pic_h / float(IMAGE_HEIGHT)
except Exception as err:
print(err)
return
選択されたピース数により縦横の数を決定します。
if num_pieces == 24:
num_cols = 6
num_rows = 4
elif num_pieces == 60:
num_cols = 10
num_rows = 6
elif num_pieces == 144:
num_cols = 16
num_rows = 9
elif num_pieces == 240:
num_cols = 20
num_rows = 12
各ピースの座標を求めます。
x_step = int(IMAGE_WIDTH / num_cols)
x_1st = int(x_step * 0.722)
x_list = [0, x_1st]
for x in range(2, num_cols):
x_list.append(x_1st + x_step * (x - 1))
y_step = int(IMAGE_HEIGHT / num_rows)
y_1st = int(y_step * 0.722)
y_list = [0, y_1st]
for y in range(2, num_rows):
y_list.append(y_1st + y_step * (y - 1))
各ピースの上下左右の凹凸をランダムに決定し、マスク画像を使用して各ピース画像を作成します。
u_list = {} # UP
r_list = {} # RIGHT
d_list = {} # DOWN
l_list = {} # LEFT
for row in range(num_rows):
for col in range(num_cols):
# F: Flat, C: conCave 凹 V: conVex 凸
rval = random.randint(1, 100) % 2
if rval == 0:
d_list[(row, col)] = 'C'
else:
d_list[(row, col)] = 'V'
if row == 0:
u_list[(row, col)] = 'F'
else:
if d_list[(row - 1, col)] == 'C':
u_list[(row, col)] = 'V'
else:
u_list[(row, col)] = 'C'
if row == (num_rows - 1):
d_list[(row, col)] = 'F'
rval = random.randint(1, 100) % 2
if rval == 0:
r_list[(row, col)] = 'C'
else:
r_list[(row, col)] = 'V'
if col == 0:
l_list[(row, col)] = 'F'
else:
if r_list[(row, col - 1)] == 'C':
l_list[(row, col)] = 'V'
else:
l_list[(row, col)] = 'C'
if col == (num_cols - 1):
r_list[(row, col)] = 'F'
mask_key = u_list[(row, col)] + r_list[(row, col)] + d_list[(row, col)] + l_list[(row, col)]
mask_info = self.mask.mask_info_tbl[mask_key]
img_mask_mono_not = mask_info.img
mask_w = mask_info.w
mask_h = mask_info.h
x = int(x_list[col] * resize_x)
y = int(y_list[row] * resize_y)
img_crop = self.img_pic[y:y+mask_h, x:x+mask_w]
img_contours = img_crop.copy()
cv2.drawContours(img_contours, mask_info.contours, -1, color=(255, 255, 255, 255), thickness=1)
img_crop_mask = cv2.bitwise_and(img_contours, img_mask_mono_not)
id_img = self.draw.DrawImage(data=cvimg2pngimg(img_crop_mask), location=(x, y))
self.jigsaw.add(id_img, PieceComposite(PieceNode(img_crop_mask, id_img, x, y, row, col)))
move_x = random.randint(10, self.graph_w - int(mask_w * 1.2))
move_y = random.randint(10, self.graph_h - int(mask_h * 1.2))
self.jigsaw.piece_tbl[id_img].move(self.draw, move_x - x, move_y - y)
矩形抜き出し
輪郭表示
ピース抜き出し
コメント