Code design to avoid instantiating an empty object - python

I am writing some code to register two images. I am using inheritance where the base class is Image and the subclasses are ImageType1 and ImageType2.
class Image:
# Note: I don't have an __init__ fx bc there are no attributes to initialize.
# Is this appropriate?
def sweep_left(self, image)
# loop to find a specific pixel
return row, col # return row and col of pixel
def sweep_right(self, image)
# loop to find a specific pixel
return row, col # return row and col of pixel
def sweep(self, image):
left_row, left_col = self.sweep_left(image)
right_row, right_col = self.sweep_right(image)
return left_row, left_col, right_row, right_col
def register(self, img1, img2):
ref_left_row, ref_left_col, ref_right_row, ref_right_col = self.sweep(img1)
reg_left_row, reg_left_col, reg_right_row, reg_right_col = self.sweep(img2)
registered_img2 = #some calcs with variables above
return registered_img2
class ImageType1(Image):
def __init__(self, image):
self.image = image
def process_image_type_1(self):
# Do some image processing using skimage
processed_img = skimage_functions(image)
return processed_img
class ImageType2(Image):
def __init__(self, image):
self.image = image
def process_image_type_2(self):
# Do some image processing using skimage
processed_img = skimage_functions(image)
return processed_img
The following code runs successfully....
image1 = ImageType1(one_image)
image1_processed = image1.process_image_type_1()
image2 = ImageType2(another_image)
image2_processed = image2.process_image_type_2()
reg = Image()
register_images = reg.register(image1_processed, image2_processed)
However, I don't understand why this code returns an error. Initially I thought it wouldn't even work because there is no obj Image, but after running it seems as though it 'kinda' worked, just needs 1 more argument. Can someone explain the details here?
register_images = Image.register(image1_processed, image2_processed)
TypeError: Image.register() missing 1 required positional argument: 'img2'
How would you design the code to use method 2, e.g. Image.register(image1_processed, image2_processed)?
UPDATE: To hopefully clear some things up removed some generalities and added code snippets.

Related

How to properly remove a row from a QAbstractListModel attached to a QListView without a delay?

I'm trying to build an application with PyQT6 that allows users to browse through a list of images with thumbnails and display the selected image in an image viewer. The application can also add and delete images. Adding images seems to work fine, but when I delete an image from the model the row in the QListView suddenly displays the data from the next row in the list. After a random interval of anywhere between half a second and about five seconds the row will actually be removed, and the list will display the proper file ordering. The fact that this behavior occurs makes me think I'm not removing the item from the model properly, and ideally I'd like the deletion of a row to be instantaneous.
Here is my minimum reproducible example:
import PyQt6 as qt
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
import os
import sys
import traceback
class ImageDataModel(QAbstractListModel):
def __init__(self, images=None):
super(ImageDataModel, self).__init__()
if images is None:
self.images = []
else:
self.images = images
self.thumbnails = []
for img_path in self.images:
icon = QPixmap(img_path).scaledToHeight(20)
self.thumbnails.append(icon)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
img_path = self.images[index.row()]
return img_path
if role == Qt.ItemDataRole.DecorationRole:
thumbnail = self.thumbnails[index.row()]
return thumbnail
def rowCount(self, index):
return len(self.images)
def removeRow(self, index):
self.images.pop(index)
self.thumbnails.pop(index)
class myListView(QListView):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
def currentChanged(self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex) -> None:
if (current.row() >= 0):
self.parent.get_selection(current) # this method simply displays the selected image
return super().currentChanged(current, previous)
class MyMenu(QMainWindow):
def __init__(self):
super().__init__()
self.layout = QHBoxLayout()
self.list = myListView(self)
try:
image_file_list = [x for x in os.listdir('path/to/image/directory') if x.lower().endswith(".png")]
except:
image_file_list = []
image_file_list.sort()
self.model = ImageDataModel(image_file_list)
self.list.setModel(self.model)
self.list.clicked.connect(self.get_selection) # this method simply displays the selected image
self.list.setCurrentIndex(self.model.index(0,0))
self.layout.addWidget(self.list)
self.widget = QWidget()
self.widget.setLayout(self.layout)
self.setCentralWidget(self.widget)
# Deletes the currently displayed image and annotation from the dataset
def delete_image(self):
# Determine what to set the new index to after deletion
if self.list.currentIndex().row() != 0:
new_index = self.list.currentIndex().row() - 1
else:
new_index = 0
# Attempt to remove the row and delete the file
try:
self.list.model().removeRow(self.list.currentIndex().row())
os.remove(self.img_path)
# Set index row to the image immediately preceding the deleted image
index = self.model.createIndex(new_index, 0)
self.list.setCurrentIndex(index)
except:
traceback.print_exc()
# Replaced display code for brevity
def get_selection(self, item):
print(item.row())
# Handles keypresses
def keyPressEvent(self, e) -> None:
global model_enabled
if (e.key() == Qt.Key.Key_Escape):
app.quit()
if (e.key() == Qt.Key.Key_Delete):
self.delete_image()
def main():
app = QApplication(sys.argv)
window = MyMenu()
window.show()
app.exec()
main()
Any change in the size, order/layout and data of a model should always be done using the proper function calls so that the views linked to the model get proper notifications about those changes.
For size and layout changes, it's important to always call the begin* and end* functions, which allows the view to be notified about the upcoming change, so they can keep a persistent list of the current items (including selection) and restore it when the change is completed.
Row removal is achieved using beginRemoveRows() and endRemoveRows().
In your case:
def removeRow(self, index):
self.beginRemoveRows(QModelIndex(), index, index)
self.images.pop(index)
self.thumbnails.pop(index)
self.endRemoveRows()
return True # <- the function expects a bool in return
Note that the correct way to implement row removal is done by implementing removeRows(), not removeRow() (singular), which internally calls removeRows anyway. So, you can leave the existing removeRow() call, do not override removeRow() and implement removeRows() instead.
def removeRows(self, row, count, parent=QModelIndex()):
if row + count >= len(self.images) or count < 1:
return False
self.beginRemoveRows(parent, row, row + count - 1)
del self.images[row:row+count]
del self.thumbnails[row:row+count]
self.endRemoveRows()
return True
A similar concept should always be done when adding new items after a view is linked to the model; in that case, implement insertRows() and there you'll call beginInsertRows() insert the new data and finally call endInsertRows().
Note that your code will throw an exception if the images is None, as it doesn't create the thumbnails object.

Issue using images in Python Tkinter

I am creating a Blackjack GUI game with Tkinter and I'm running into an issue where the deal button clears the image of the old card from the screen when a new one is added. My educated guess is the card_image inside the deal() function is overwriting itself when I use the function again. If this is the case why is this and what's the best fix? Thanks.
import random
from tkinter import *
from PIL import Image, ImageTk
root =Tk()
root.title('21 Blackjack')
root.iconbitmap('images/21_cards.ico')
root.geometry('1280x750')
root.configure(bg='green')
cards = []
suits = ['hearts', 'clubs', 'diamonds', 'spades']
face_cards = ['ace', 'jack', 'queen', 'king']
extension = 'png'
for y in suits:
for x in range(2, 11):
name = 'images/{}-{}.{}'.format(str(x), y, extension)
cards.append(name)
for x in face_cards:
name = 'images/{}-{}.{}'.format(str(x), y, extension)
cards.append(name)
print(cards)
print(len(cards))
random.shuffle(cards)
print(cards[0])
hand = []
def deal():
global card_image, card_label, hand
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label = Label(root, image=card_image, relief="raised").pack(side="left")
hand += cards[:1]
cards.pop(0)
print(hand)
deal_button = Button(root, text="deal", command=deal).pack()
root.mainloop()
Instead of
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label = Label(root, image=card_image, relief="raised").pack(side="left")
Do:
card_label = Label(root, image=card_image, relief="raised").pack(side="left")
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label.image = card_image #Keeps reference to the image so it's not garbage collected
Do note when you want use the photo use the variable card_image
As people have pointed out I needed to add card_label.image = card_image to the function and remove card_image & card_label global to stop the image being removed. But for some reason Python didn't like having the image being packed before I did this.
The function now looks like this.
global hand
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label = Label(root, image=card_image, relief="raised")
card_label.image = card_image
card_label.pack(side="left")
hand += cards[:1]
cards.pop(0)
print(hand)
You should use an image pool. Lucky for you, I have one right here
import os
from glob import glob
from PIL import Image, ImageTk
from typing import List, Tuple, Union, Dict, Type
from dataclasses import dataclass
#dataclass
class Image_dc:
image :Image.Image
rotate :int = 0
photo :ImageTk.PhotoImage = None
def size(self) -> Tuple[int]:
if not self.photo is None:
return self.photo.width(), self.photo.height()
return self.image.width, self.image.height
class ImagePool(object):
##__> PRIVATE INTERFACE <__##
__PATHS = dict()
__IMAGES = dict()
#staticmethod
def __name(path) -> str:
return os.path.basename(os.path.splitext(path)[0])
#staticmethod
def __unique(path:str, prefix:str='') -> str:
name = f'{prefix}{ImagePool.__name(path)}'
if name in ImagePool.names():
sol = 'using a prefix' if not prefix else f'changing your prefix ({prefix})'
msg = ("WARNING:\n"
f"{name} was not loaded due to a same-name conflict.\n"
f"You may want to consider {sol}.\n\n")
print(msg)
return None
return name
#staticmethod
def __request(name:str) -> Image_dc:
if name in ImagePool.__PATHS:
path = ImagePool.paths(name)
if os.path.isfile(path):
if name not in ImagePool.__IMAGES:
ImagePool.__IMAGES[name] = Image_dc(Image.open(path))
return ImagePool.__IMAGES[name]
else:
raise ValueError(f'ImagePool::__request - Path Error:\n\tpath is not a valid file\n\t{path}')
raise NameError(f'ImagePool::__request - Name Error:\n\t"{name}" does not exist')
return None
#staticmethod
def __size(iw:int, ih:int, w:int=None, h:int=None, scale:float=1.0) -> Tuple[int]:
if not w is None and not h is None:
if iw>ih:
ih = ih*(w/iw)
r = h/ih if (ih/h) > 1 else 1
iw, ih = w*r, ih*r
else:
iw = iw*(h/ih)
r = w/iw if (iw/w) > 1 else 1
iw, ih = iw*r, h*r
return int(iw*scale), int(ih*scale)
##__> PUBLIC INTERFACE <__##
#staticmethod
def names(prefix:str='') -> List[str]:
names = [*ImagePool.__PATHS]
return names if not prefix else list(filter(lambda name, pre=prefix: name.startswith(pre), names))
#staticmethod
def paths(name:str=None) -> Union[Dict, str]:
if name is None:
return ImagePool.__PATHS
if name in ImagePool.__PATHS:
return ImagePool.__PATHS[name]
raise NameError(f'ImagePool::paths - Name Error:\n\tname "{name}" does not exist')
#staticmethod
def images(name:str=None, prefix:str='') -> Union[Dict, Image.Image]:
if name is None:
return {name:ImagePool.__request(name).image for name in self.names(prefix)}
return ImagePool.__request(name).image
#staticmethod
def photos(name:str=None, prefix:str='') -> Union[Dict, ImageTk.PhotoImage]:
if name is None:
return {name:ImagePool.__request(name).photo for name in self.names(prefix)}
return ImagePool.__request(name).photo
#staticmethod
def append_file(path:str, prefix:str='') -> Type:
if not os.path.isfile(path):
raise ValueError(f'ImagePool::append_file - Value Error:\n\tpath is not valid\n\t{path}')
name = ImagePool.__unique(path, prefix)
if name:
ImagePool.__PATHS[name] = path
return ImagePool
#staticmethod
def append_directory(directory:str, filters=['*.png', '*.jpg'], prefix:str='') -> Type:
if not os.path.isdir(directory):
raise ValueError(f'ImagePool::append_directory - Value Error:\n\tdirectory is not valid\n\t{directory}')
filters = filters if isinstance(filters, (List, Tuple)) else [filters]
for filter in filters:
for path in glob(f'{directory}/{filter}'):
ImagePool.append_file(path, prefix)
return ImagePool
#staticmethod
def photo(name:str, width:int=None, height:int=None, scale:float=1.00, rotate:int=None) -> ImageTk.PhotoImage:
image_t = ImagePool.__request(name)
size = ImagePool.__size(*image_t.size(), width, height, scale)
rotate = image_t.rotate if rotate is None else rotate
#only resize if the new size or rotation is different than the current photo size or rotation
#however, a small margin for error must be considered for the size
diff = tuple(map(lambda i, j: i-j, image_t.size(), size))
if (diff > (1, 1)) or (diff < (-1, -1)) or (image_t.rotate != rotate):
image_t.rotate = rotate
image_t.photo = ImageTk.PhotoImage(image_t.image.resize(size, Image.LANCZOS).rotate(rotate))
return image_t.photo
Using that file you can do a lot of things very easily. You can put all your card images in a folder and get them all with:
ImagePool.append_directory(path_to_folder)
You can then get any card and force it to fit in it's parent (no-matter-what) with:
somelabel['image'] = ImagePool.photo(image_name, allotted_width, allotted_height)
or just:
somelabel['image'] = ImagePool.photo(image_name)
you can get a list of every name in the pool with
deck = ImagePool.names()
or grab it while you also append a directory
deck = ImagePool.append_directory(path_to_folder).names()
That is very helpful to you, because you can simply shuffle and pop that list as the official "deck" in your game. Like this:
somecard['image'] = ImagePool.photo(deck.pop(0))
Assuming you will have more than just card graphics, but do not want to get all the images when creating a deck, there is a solution for that, as well.
first supply a prefix when appending the cards image directory, and then use that prefix when calling names(). As long as only card images were in the cards image directory, only card names will be returned.
deck = ImagePool.append_directory(path_to_folder, prefix='cards_').names('cards_')
notes:
allotted area is only allotted and in no way should be assumed to represent the final width and/or height of the actual image. The image will do whatever it has to to fit in the allotted space without losing it's original height and width ratio. If you don't supply allotted width and height the image will be it's full size.
All image and photo references are automatically maintained in ImagePool. There is no reason to store your own references
The entire class is static so references to all the images and photos can be accessed anywhere without an ImagePool instance. In other words, you never need to do this: images = ImagePool() and therefore never need to figure out how to get images into some class or other document.
No images actually load until you start requesting them, and then it happens automatically. This is a good thing. You have 52 cards, but if you only use ex: 6 in the first game ~ only 6 will fully load. Eventually all the cards will get played and be fully loaded, and you didn't have some huge burp in your game where you tried to fully create 52 cards all at once.
The ImagePool class has more features, but the ones explained here are all you should need for your game.

How do I execute functions in a particular order when defined in a class?

I'm trying to build a function (defined inside a class) that calls on other functions (also defined in the class) in a particular order.
To be specific, I have created a class of image transformations (using imgaug), where all of the image augmentation parameters are passed as arguments in the constructor (see code below). I have then defined a couple of functions that take a raw image as an input, and apply the transformations thereon.
What I'd like to do is to be able to apply these transformation functions in a specific order. I think I'm close, but I keep getting the same errors. I'm using classes because I'd like to only have to specify the parameters once, and then use a list or tuple to specify the ordering of the functions, and ultimately have the option to make the ordering random.
EDIT: To add context to my question - the end goal here is to create a function that will randomly augment image data using various augmentation techniques applied in a random order, using random parameters (from within a specified range).
Here is my code:
import numpy as np
import imgaug as ia
from imgaug import augmenters as iaa
image = my_image_to_transform
class random_augmentation:
def __init__(self,rotate_range=None,e_sev=None,):
if rotate_range is None:
self.rotate_range = None
elif (type(rotate_range) is tuple) or (type(rotate_range) is list) and len(rotate_range) == 2:
self.rotate_range = rotate_range
else:
self.rotate_range = None
if e_sev is None:
self.e_sev = None
else:
self.e_sev = e_sev
def aug_rotate(self,img):
if self.rotate_range is not None:
rotate = iaa.Affine(rotate=self.rotate_range)
rotated_img = rotate(image=img)
return rotated_img
else:
pass
def aug_elastic(self,img):
if self.e_sev is not None:
elastic = iaa.imgcorruptlike.ElasticTransform(severity=self.e_sev)
elastic_img = elastic(image=img)
return elastic_img
else:
pass
f_list = [aug_rotate,aug_elastic]
def random_augment(img,order):
for i in order:
function = f_list[i]
outp = function(img)
img = outp
return outp
r_aug = random_augmentation(rotate_range=(-30,30),e_sev=3)
augmented_img = r_aug.random_augment(image,[0,1])
The error I get is the following:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-49-2a880e8381b8> in <module>
----> 1 augmented_img = r_aug.random_augment(image,[0,1])
TypeError: random_augment() takes 2 positional arguments but 3 were given
I'm not that experienced with classes, so I'm sure I'm doing something wrong somewhere but for the life of me I can't figure it out!
Cheers!
EDIT: Typos
Try this:
def random_augment(self, img, order):
for i in order:
function = [self.aug_rotate, self.aug_elastic][i]
outp = function(img)
return outp
I annotated the parts in the code where you should make the corrections suggested in the comments:
import numpy as np
import imgaug as ia
from imgaug import augmenters as iaa
image = my_image_to_transform
class random_augmentation:
def __init__(self, rotate_range=None, e_sev=None, ):
if rotate_range is None:
self.rotate_range = None
elif (type(rotate_range) is tuple) or (type(rotate_range) is list) and len(rotate_range) == 2:
self.rotate_range = rotate_range
else:
self.rotate_range = None
if e_sev is None:
self.e_sev = None
else:
self.e_sev = e_sev
self.f_list = [self.aug_rotate, self.aug_elastic] # Added self.f_list
def aug_rotate(self, img):
if self.rotate_range is not None:
rotate = iaa.Affine(rotate=self.rotate_range)
rotated_img = rotate(image=img)
return rotated_img
else:
pass
def aug_elastic(self, img):
if self.e_sev is not None:
elastic = iaa.imgcorruptlike.ElasticTransform(severity=self.e_sev)
elastic_img = elastic(image=img)
return elastic_img
else:
pass
def random_augment(self, img, order): # Added self
for i in order:
function = self.f_list[i] # Use self.f_list
outp = function(img)
img = outp
return outp
r_aug = random_augmentation(rotate_range=(-30, 30), e_sev=3)
augmented_img = r_aug.random_augment(image, [0, 1])

Property dependency and update in python

I have a class which should transform the image retrieved based on a path received(url or os image).
Not sure how is the right way to implement this to follow best principles, like SOLID, or should i even care about it?! in my case.
In particular, whenever i update the 'path' attribute the code should automatically update also the
'img' attribute based on the given path.
class TransfImage(path):
def __init__(self,path):
self._path = path
#self._img = img_from_url() ?! would be better to use here the method
#property
def path(self):
return self._path
#path.setter
def path(self,my_path):
if valid_os_file_image(my_path):
self._path = my_path
self.img = img_from_os_file(my_path) #not sure if is the proper way to set the image here
elif valid_url_image(my_path):
self._path = my_path
self.img = img_from_url(my_path) #not sure if is the proper way to set the image here
#property
def img(self):
return self._img
#img.setter
def img(self,my_img):
self._img = my_img
def img_from_os_file(self,my_os_file):
return cv.imread(my_os_file)
def img_from_url(self, url_path):
try:
req = urllib.request.urlopen(url_path)
except ValueError:
raise ValueError('not a valid url')
try:
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
img = cv.imdecode(arr, -1)
except ValueError as e:
print('could not read img from url', e)
else:
return img
def im_show(self):
cv.imshow(self.img)
t = TransfImage('lena.jpg')
t.im_show() #should show lena.jpg
t.path = 'cards.jpg'
t.im_show() #should show 'cards.jpg
There is no visible reason in your code to make img a property, Apart from this I see no problem (except for a few calls to methods missing a self prefix).

Python CodeSkulptor Pause Drawing from Inside For Loop

I want to build some visualizations for searching algorithms (BFS, A* etc.) within a grid.
My solution should show each step of the algorithm using CodeSkulptor simplegui (or the offline version using SimpleGUICS2Pygame.)
I've made a version which highlights all the cells visited by changing their color, but I've run into trouble trying to make the path display step-by-step with a time delay between each step.
I've extracted the essence of the problem and created a minimal example representing it in the code below, also run-able online here: http://www.codeskulptor.org/#user47_jB2CYfNrH2_2.py
What I want is during the change_colors() function, for there to be a delay between each iteration.
CodeSkulptor doesn't have time.sleep() available, and I don't think it would help anyway.
CodeSkulptor does have timers available, which might be one solution, although I can't see how to use one in this instance.
Code below:
import time
try:
import simplegui
except ImportError:
import SimpleGUICS2Pygame.simpleguics2pygame as simplegui
simplegui.Frame._hide_status = True
TITLE = "TEST"
FRAME_WIDTH = 400
FRAME_HEIGHT = 400
DELAY = 10
class Square:
"""This class represents a simple Square object."""
def __init__(self, size, pos, pen_size=2, pen_color="red", fill_color="blue"):
"""Constructor - create an instance of Square."""
self._size = size
self._pos = pos
self._pen_size = pen_size
self._pen_color = pen_color
self._fill_color = fill_color
def set_color(self, color):
self._fill_color = color
def get_color(self):
return self._fill_color
def is_in(self, pos):
"""
Determine whether coordinates are within the area of this Square.
"""
return self._pos[0] < pos[0] < self._pos[0] + self._size and self._pos[1] < pos[1] < self._pos[1] + self._size
def draw(self, canvas):
"""
calls canvas.draw_image() to display self on canvas.
"""
points = [(self._pos[0], self._pos[1]), (self._pos[0] + self._size, self._pos[1]),
(self._pos[0] + self._size, self._pos[1] + self._size), (self._pos[0], self._pos[1] + self._size)]
canvas.draw_polygon(points, self._pen_size, self._pen_color, self._fill_color)
def __str__(self):
return "Square: {}".format(self._pos)
def draw(canvas):
for square in squares:
square.draw(canvas)
def change_colors():
for square in squares:
# time.sleep(1) # Not implemented in CodeSkulptor and would'nt work anyway
square.set_color("green")
frame = simplegui.create_frame(TITLE, FRAME_WIDTH, FRAME_HEIGHT)
frame.set_draw_handler(draw)
width = 20
squares = []
for i in range(10):
squares.append(Square(width, (i * width, 0)))
change_colors()
frame.start()
Any help appreciated.
Yes, you need to use a timer. Something like this:
I = 0
def change_next_color():
if I < len(squares):
squares[I].set_color("green")
global I
I += 1
timer = simplegui.create_timer(1000, change_next_color)
timer.start()
http://www.codeskulptor.org/#user47_udyXzppCdw2OqdI.py
I also replaced
simplegui.Frame._hide_status = True
by simplegui.Frame._hide_controlpanel = True
https://simpleguics2pygame.readthedocs.io/en/latest/simpleguics2pygame/frame.html#SimpleGUICS2Pygame.simpleguics2pygame.frame.Frame._hide_controlpanel
See also _keep_timers option of SimpleGUICS2Pygame to help you:
https://simpleguics2pygame.readthedocs.io/en/latest/simpleguics2pygame/frame.html#SimpleGUICS2Pygame.simpleguics2pygame.frame.Frame._keep_timers
Possible improvements:
Find a better solution that don't use a global counter.
Stop timer when all work is finished.

Categories

Resources