1

I want to create a grid of buttons with images. The image on each button is a piece from a bigger image which is cutted into pieces - so the button grid can be seen as tiles of the original image (the code i used to put a image on the button is from here).

To cut the image (image=QPixmap(path_to_image)) into pieces i used the methodimage.copy(x, y, width, height) in a for loop.

At first i thought that the code works fine. A more precise check shows that the image pieces have overlapping parts.

Here my code which was tested with an GIF-Image:

import os
import sys

from PyQt5.QtCore import QSize
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import \
    QWidget, QApplication, \
    QGridLayout, \
    QLabel, QPushButton

def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
    if isinstance(image, str) and os.path.exists(image):
        image = QPixmap(image)
    elif not isinstance(image, QPixmap):
        raise ValueError("image must be str or QPixmap object")

    # Dim of the tile
    tw = int(image.size().width() / cols)
    th = int(image.size().height() / rows)

    # prepare return value
    tiles = {"width": tw, "height": th}
    for r in range(rows):
        for c in range(cols):
            tile = image.copy(c * th, r * tw, tw, th)
            # args: x, y, width, height
            # https://doc.qt.io/qt-5/qpixmap.html#copy-1
            tiles[(r, c)] = tile

    return tiles


def create_pixmapbutton(pixmap, width=0, height=0) -> QPushButton:
    if isinstance(pixmap, QPixmap):
        button = QPushButton()
        if width > 0 and height > 0:
            button.setIconSize(QSize(width, height))
        else:
            button.setIconSize(pixmap.size())
        button.setIcon(QIcon(pixmap))
        return button


def run_viewer(imagepath, rows=4, cols=4):

    # ImageCutter.run_viewer(imagepath)
    app = QApplication(sys.argv)

    image = QPixmap(imagepath)

    tiles = cut_image_into_tiles(image=image, rows=rows, cols=cols)
    tilewidth = tiles["width"]
    tileheight = tiles["height"]
    # get dict tiles (keys:=(row, col) or width or height)

    viewer = QWidget()
    layout = QGridLayout()
    viewer.setLayout(layout)
    viewer.setWindowTitle("ImageCutter Viewer")

    lbl = QLabel()
    lbl.setPixmap(image)
    layout.addWidget(lbl, 0, 0, rows, cols)

    for r in range(rows):
        for c in range(cols):
            btn = create_pixmapbutton(
                tiles[r, c], width=tilewidth, height=tileheight)

            btninfo = "buttonsize={}x{}".format(tilewidth, tileheight)
            btn.setToolTip(btninfo)
            # logger.debug("    create button [{}]".format(btninfo))
            layout.addWidget(btn, r, cols + c)

    viewer.show()
    sys.exit(app.exec_())

I tested the code with run_viewer(path_to_a_gif_image, rows=3, cols=3)

jgsedi
  • 227
  • 4
  • 18

3 Answers3

1

Try it:

import sys
from PyQt5.QtCore import QSize, QRect
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import (QWidget, QApplication, QGridLayout, 
                             QLabel, QPushButton, QSizePolicy)
from PIL import Image


def cut_image_into_tiles(image, r=4, c=4):
    im = Image.open(image)                                                           
    x, y = im.size 
    for i in range(r):
        for j in range(c):
            if i!=r and j!=c:
                im.crop(box=(x/r*i, y/c*j, x/r*(i+1)-1, y/c*(j+1)-1)).\
                save('image{}{}.png'.format(str(i+1), str(j+1)))      


def run_viewer(imagepath, rows=4, cols=4):
    app = QApplication(sys.argv)
    image = QPixmap(imagepath)

    tiles = cut_image_into_tiles(image=imagepath, r=rows, c=cols)

    viewer = QWidget()
    layout = QGridLayout()
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(0)
    viewer.setLayout(layout)
    viewer.setWindowTitle("ImageCutter Viewer")

    lbl = QLabel()
    lbl.setPixmap(image)
    layout.addWidget(lbl, 0, 0, rows, cols)

    for r in range(1, rows+1):
        for c in range(1, cols+1):    
            button = QPushButton()
            button.setStyleSheet('''QPushButton {background-color: yellow; border: 1px solid black;}
                                    QPushButton:pressed {background-color: green;}''')
            image = QPixmap(f"image{c}{r}.png")
            button.setIconSize(image.size())
            button.setIcon(QIcon(image))            
            layout.addWidget(button, r-1, c+cols)            

    viewer.show()
    sys.exit(app.exec_())


run_viewer("linux.gif", rows=3, cols=3)

enter image description here

S. Nick
  • 12,879
  • 8
  • 25
  • 33
  • 1
    hi @S.Nick: that's a nice solution with the backdraw of saving each tile - so on big images and big grids a lot of space and time are needed – jgsedi Aug 16 '19 at 12:04
1

The width of each grid must depend on the width of the image, in your case it depends on th, but th depends on the height which is incorrect, you must change th for tw, the same for the height.

Considering the above, the solution is:

import os
import sys

from PyQt5.QtCore import QSize
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QLabel, QPushButton


def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
    if isinstance(image, str) and os.path.exists(image):
        image = QPixmap(image)
    elif not isinstance(image, QPixmap):
        raise ValueError("image must be str or QPixmap object")

    # Dim of the tile
    tw = int(image.size().width() / cols)
    th = int(image.size().height() / rows)

    # prepare return value
    tiles = {"width": tw, "height": th}
    for r in range(rows):
        for c in range(cols):
            tile = image.copy(c * tw, r * th, tw, th) # <----
            # args: x, y, width, height
            # https://doc.qt.io/qt-5/qpixmap.html#copy-1
            tiles[(r, c)] = tile

    return tiles


def create_pixmapbutton(pixmap, width=0, height=0) -> QPushButton:
    if isinstance(pixmap, QPixmap):
        button = QPushButton()
        if width > 0 and height > 0:
            button.setIconSize(QSize(width, height))
        else:
            button.setIconSize(pixmap.size())
        button.setIcon(QIcon(pixmap))
        button.setContentsMargins(0, 0, 0, 0)
        button.setFixedSize(button.iconSize())
        return button


def run_viewer(imagepath, rows=4, cols=4):

    # ImageCutter.run_viewer(imagepath)
    app = QApplication(sys.argv)

    image = QPixmap(imagepath)

    tiles = cut_image_into_tiles(image=image, rows=rows, cols=cols)
    tilewidth = tiles["width"]
    tileheight = tiles["height"]
    # get dict tiles (keys:=(row, col) or width or height)

    viewer = QWidget()
    layout = QGridLayout(viewer)
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(0)
    viewer.setWindowTitle("ImageCutter Viewer")

    lbl = QLabel()
    lbl.setPixmap(image)
    layout.addWidget(lbl, 0, 0, rows, cols)

    for r in range(rows):
        for c in range(cols):
            btn = create_pixmapbutton(tiles[r, c], width=tilewidth, height=tileheight)

            btninfo = "buttonsize={}x{}".format(tilewidth, tileheight)
            btn.setToolTip(btninfo)
            # logger.debug("    create button [{}]".format(btninfo))
            layout.addWidget(btn, r, cols + c)

    viewer.show()
    viewer.setFixedSize(viewer.sizeHint())
    sys.exit(app.exec_())


run_viewer("linux.gif", 3, 3)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • @eylianesc: "... _it depends on th, but th depends on the height which is incorrect_ ..." Why are the calculated dimension (**th** and **tw**) are incorrect? – jgsedi Aug 16 '19 at 11:58
  • ok my fault (because of my bad englisch)! I interchanged _width_ (_tw_) and _height_ (_th_) in the code. oh man - thanks – jgsedi Aug 16 '19 at 12:12
0

Ok, i discovered that for big images and big grids the images on the buttons on the right can be smaller than the others. The same at the bottom buttons.

An offset at the top and on the left will prevent this.

So the function def cut_image_into_tiles(image, rows=4, cols=4) -> dict: must be adapted to:

def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
    if isinstance(image, str) and os.path.exists(image):
        image = QPixmap(image)
    elif not isinstance(image, QPixmap):
        raise ValueError("image must be str or QPixmap object")

    # Dim of tiled images
    width = image.size().width()
    height = image.size().height()
    tw = int(width / cols)
    th = int(height / rows)
    offset_w = int((width - tw * cols)/2)    # !!!
    offset_h = int((height - th * rows)/2)   # !!!

    # prepare return value
    tiles = {"width": tw, "height": th}
    for r in range(rows):
        for c in range(cols):
            x = c * tw
            y = r * th
            if c == 0:
                x += offset_w    # !!!
            if r == 0:
                y += offset_h    # !!!
            tile = image.copy(x, y, tw, th)
            # args: x, y, width, height
            # https://doc.qt.io/qt-5/qpixmap.html#copy-1
            tiles[(r, c)] = tile

    return tiles

The whole code is now:

def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
    if isinstance(image, str) and os.path.exists(image):
        image = QPixmap(image)
    elif not isinstance(image, QPixmap):
        raise ValueError("image must be str or QPixmap object")

    # Dim of tiled images
    width = image.size().width()
    height = image.size().height()
    tw = int(width / cols)
    th = int(height / rows)
    offset_w = int((width - tw * cols)/2)
    offset_h = int((height - th * rows)/2)

    # prepare return value
    tiles = {"width": tw, "height": th}
    for r in range(rows):
        for c in range(cols):
            x = c * tw
            y = r * th
            if c == 0:
                x += offset_w
            if r == 0:
                y += offset_h
            tile = image.copy(x, y, tw, th)
            # args: x, y, width, height
            # https://doc.qt.io/qt-5/qpixmap.html#copy-1
            tiles[(r, c)] = tile

    return tiles


def create_pixmapbutton(pixmap, width=0, height=0) -> QPushButton:
    if isinstance(pixmap, QPixmap):
        button = QPushButton()
        if width > 0 and height > 0:
            button.setIconSize(QSize(width, height))
        else:
            button.setIconSize(pixmap.size())
        button.setIcon(QIcon(pixmap))
        return button


def run_viewer(imagepath, rows=4, cols=4):

    # ImageCutter.run_viewer(imagepath)
    app = QApplication(sys.argv)

    image = QPixmap(imagepath)

    tiles = cut_image_into_tiles(image=image, rows=rows, cols=cols)
    tilewidth = tiles["width"]
    tileheight = tiles["height"]
    # get dict tiles (keys:=(row, col) or width or height)

    viewer = QWidget()
    layout = QGridLayout()
    viewer.setLayout(layout)
    viewer.setWindowTitle("ImageCutter Viewer")

    lbl = QLabel()
    lbl.setPixmap(image)
    layout.addWidget(lbl, 0, 0, rows, cols)

    for r in range(rows):
        for c in range(cols):
            btn = create_pixmapbutton(
                tiles[r, c], width=tilewidth, height=tileheight)

            btninfo = "buttonsize={}x{}".format(tilewidth, tileheight)
            btn.setToolTip(btninfo)
            # logger.debug("    create button [{}]".format(btninfo))
            layout.addWidget(btn, r, cols + c)

    viewer.show()
    sys.exit(app.exec_())
jgsedi
  • 227
  • 4
  • 18