我正在嘗試創建一個python程序,讓我想象一個風格化的虛構政治地圖,其中每個國家都用特定顏色的像素表示。我正在使用PyQt5創建一些簡單的GUI,讓我打開地圖,縮放和拖動,當你點擊地圖時,它會在窗口的一側顯示一些信息,比如國家名稱和首都。其中一個功能是,一旦單擊某個國家,它的顏色就會變為亮綠色,這樣就可以清楚地選擇哪個國家。我實現這一點的方式對小圖像很好,但對大圖像來說很難,從我單擊一個國家到國家顏色變為綠色的那一刻,時間長達6-7秒。
這是我用來在點擊國家時實現顏色改變的代碼:
def highlight_pixels(self, RGB):
#BLOCK ONE
#Timer 1 starts
start_time = time.perf_counter()
# Copy a numpy array that represent the base image. This is so that every time the highlight_pixels function is called it reverts previous changes
temp_image = self.base_image_array.copy()
# Print time 1
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to initialise image: {elapsed_time:.6f} milliseconds")
#BLOCK TWO
#Timer 2 starts
start_time = time.perf_counter()
# Select pixels that match the target color
mask = (temp_image[:,:,0] == RGB[0]) & (temp_image[:,:,1] == RGB[1]) & (temp_image[:,:,2] == RGB[2])
# Set color of the selected pixels to a bright green.
temp_image[self.ignore_color_mask & mask, :] = (83, 255, 26)
# Print time 2
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to change color: {elapsed_time:.6f} milliseconds")
#BLOCK THREE
#Timer 3 starts
start_time = time.perf_counter()
# Convert array back to image (qimage)
temp_image = qimage2ndarray.array2qimage(temp_image)
# convert the image back to pixmap
self.map_image = QPixmap.fromImage(temp_image)
# update the map scene
self.view.scene().clear()
self.view.scene().addPixmap(self.map_image)
# Print time 3
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to show image: {elapsed_time:.6f} milliseconds")
在一次包含兩個圖像的測試中,一個大圖像,一個小圖像,我得到了這三個塊的時間。很明顯,每次我運行程序時都會有一點變化,但結果總是非常相似:
我嘗試了一些不同的方法。我首先很難嘗試用綠色像素創建一個新圖像,并將其覆蓋在基礎圖像上,而不是對整個圖像進行更改,但我認為這沒有正確實現,因此決定返回。
然后我想我可以通過創建一個包含特定顏色所有像素坐標的字典來加快速度。這在某種程度上起了作用,這意味著在小圖像(200x100像素)上測試時,它顯示出一些改進的跡象:
然而,當嘗試對大圖像(16300x8150像素)使用這種方法時,在嘗試創建字典時,它會耗盡內存。
這是更新的功能:
def highlight_pixels(self, RGB):
#BLOCK ONE
#Timer 1 starts
start_time = time.perf_counter()
# Copy a numpy array that represent the base image. This is so that every time the highlight_pixels function is called it reverts previous changes
temp_image = self.base_image_array.copy()
# Print time 1
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to initialise image: {elapsed_time:.6f} milliseconds")
#BLOCK TWO
#Timer 2 starts
start_time = time.perf_counter()
# Select pixels that match the target color
coordinates = self.color_dict.get(RGB)
# Set their color to green
temp_image[coordinates[:, 0], coordinates[:, 1], :] = (83, 255, 26)
# Print time 2
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to change color: {elapsed_time:.6f} milliseconds")
#BLOCK THREE
#Timer 3 starts
start_time = time.perf_counter()
#convert array back to image (qimage)
temp_image = qimage2ndarray.array2qimage(temp_image)
# convert the image back to pixmap
self.map_image = QPixmap.fromImage(temp_image)
# update the map scene
self.view.scene().clear()
self.view.scene().addPixmap(self.map_image)
# Print time 3
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to show image: {elapsed_time:.6f} milliseconds")
下面是創建我提到的字典的函數:
def create_color_dict(self, image_array):
self.color_dict = {}
# Create an array of indices for each pixel
pixels = np.indices(image_array.shape[:2]).T.reshape(-1, 2)
# Get the color of each pixel
pixel_colors = image_array[pixels[:, 0], pixels[:, 1], :]
# Convert pixel colors to tuples
pixel_colors = tuple(map(tuple, pixel_colors))
# Create dictionary of pixels by color
for color, pixel in zip(pixel_colors, pixels):
if color not in self.color_dict:
self.color_dict[color] = np.array([pixel])
else:
self.color_dict[color] = np.append(self.color_dict[color], [pixel], axis=0)
return self.color_dict
我還嘗試制作一個單獨的程序來創建字典(因為我只需要運行一次該部分,真的),但它顯然也耗盡了內存。
所以我的問題是,如何優化這一點?我嘗試的方法有效嗎?如果是這樣的話,我如何避免內存不足?
我非常樂意提供任何其他信息,包括整個代碼或測試圖像。我是一個絕對的初學者,很難理解如何不讓某個東西正常工作,而是如何讓它更好地工作。
編輯:這是所有代碼,以防有人問,因為我明天才能訪問電腦。請注意,這可能有點令人困惑,因為我對此一無所知。
import sys
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtGui import QImage, QPixmap, QColor, QPainter, QPen, QBrush, qRed, qGreen, qBlue, QFont
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QLabel, QVBoxLayout, QWidget, QGraphicsRectItem, QMainWindow, QDockWidget, QHBoxLayout, QSizePolicy, QGraphicsPixmapItem
import time
import qimage2ndarray
from qimage2ndarray import rgb_view
import numpy as np
class MapViewer(QMainWindow):
def __init__(self, map_path):
super().__init__()
self.setWindowTitle("Map Viewer")
self.setGeometry(100, 100, 800, 600)
self.dragCheck = False #False if not dragging, True if dragging
# Create the QGraphicsView widget and set it as the central widget
self.view = QGraphicsView(self)
self.setCentralWidget(self.view)
self.view.setRenderHint(QPainter.Antialiasing)
self.view.setRenderHint(QPainter.SmoothPixmapTransform)
self.view.setDragMode(QGraphicsView.NoDrag)
self.view.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.view.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
self.view.setOptimizationFlag(QGraphicsView.DontAdjustForAntialiasing, True)
self.view.setOptimizationFlag(QGraphicsView.DontSavePainterState, True)
# Load the map image and set it as the background image
self.map_image = QPixmap(map_path)
#self.original_map_image = self.map_image.copy() # Assign the original image to this variable
#self.map_item = QGraphicsPixmapItem(self.map_image)
self.view.setScene(QGraphicsScene())
self.view.scene().addPixmap(self.map_image)
#self.view.fitInView(self.view.sceneRect(), Qt.KeepAspectRatio)
# convert the pixmap to image
self.base_image = QImage(map_path)
# Convert image into numpy array
self.base_image_array = rgb_view(self.base_image)
# Create a mask with the same shape as the image, filled with True
self.ignore_color_mask = np.ones(self.base_image_array.shape[:2], dtype=bool)
# Set False to pixels that have black color (0, 0, 0) or sea color (172, 208, 239)
self.ignore_color_mask[np.where((self.base_image_array[:,:,0]==0) & (self.base_image_array[:,:,1]==0) & (self.base_image_array[:,:,2]==0)| (self.base_image_array[:,:,0] == 172) & (self.base_image_array[:,:,1] == 208) & (self.base_image_array[:,:,2] == 239))] = False
# Make it so the wheel zooms in and out
self.view.wheelEvent = self.handle_mouse_wheel
# Install an eventFilter to handle the mouse clicks under certain conditions only (to allow panning when pressing control)
self.view.installEventFilter(self)
# Handle ctrl beign pressed or released
self.view.keyPressEvent = self.CtrlPressEvent
self.view.keyReleaseEvent = self.CtrlReleaseEvent
# Display the default country information in the dock widget
RGB = (255, 0, 0)
self.countries = {
(255, 0, 0): {"name": "Red", "rgb": (255, 0, 0), "capital": "Red Capital", "government": "Red Government", "size": "Red Size", "population": "Red Population"},
(0, 255, 0): {"name": "Green", "rgb": (0, 255, 0), "capital": "Green Capital", "government": "Green Government", "size": "Green Size", "population": "Green Population"},
(0, 0, 255): {"name": "Blue", "rgb": (0, 0, 255), "capital": "Blue Capital", "government": "Blue Government", "size": "Blue Size", "population": "Blue Population"}
}
self.display_country_info(RGB)
self.create_color_dict(self.base_image_array)
def create_color_dict(self, image_array):
self.color_dict = {}
# Create an array of indices for each pixel
pixels = np.indices(image_array.shape[:2]).T.reshape(-1, 2)
# Get the color of each pixel
pixel_colors = image_array[pixels[:, 0], pixels[:, 1], :]
# Convert pixel colors to tuples
pixel_colors = tuple(map(tuple, pixel_colors))
# Create dictionary of pixels by color
for color, pixel in zip(pixel_colors, pixels):
if color not in self.color_dict:
self.color_dict[color] = np.array([pixel])
else:
self.color_dict[color] = np.append(self.color_dict[color], [pixel], axis=0)
return self.color_dict
def handle_mouse_wheel(self, event):
if event.angleDelta().y() > 0:
# Scroll up, zoom in
self.view.scale(1.1, 1.1)
else:
# Scroll down, zoom out
self.view.scale(1 / 1.1, 1 / 1.1)
def display_country_info(self, RGB):
# Look up the country information based on the RGB value
country_info = self.countries.get(RGB)
if country_info is None:
# Handle the case where the RGB value is not found in the dictionary
print("Sorry, no country found with that RGB value.")
return
else:
#Remove any existing dock widgets
for dock_widget in self.findChildren(QDockWidget):
self.removeDockWidget(dock_widget)
# Create a QVBoxLayout to hold the labels
layout = QVBoxLayout()
# Create a QLabel for each piece of information and add it to the layout
name_label = QLabel("Name:")
name_label.setStyleSheet("font-weight: bold;")
name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
value_label = QLabel(f"{country_info['name']}")
value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
# create a new layout for the name and value labels
layout_name_value = QHBoxLayout()
# add both labels to the new layout
layout_name_value.addWidget(name_label)
layout_name_value.addWidget(value_label)
# add this new layout to the main layout
layout.addLayout(layout_name_value)
# Create a QLabel for each piece of information and add it to the layout
name_label = QLabel("RGB:")
name_label.setStyleSheet("font-weight: bold;")
name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
value_label = QLabel(f"{country_info['rgb']}")
value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
# create a new layout for the name and value labels
layout_name_value = QHBoxLayout()
# add both labels to the new layout
layout_name_value.addWidget(name_label)
layout_name_value.addWidget(value_label)
# add this new layout to the main layout
layout.addLayout(layout_name_value)
# Create a QLabel for each piece of information and add it to the layout
name_label = QLabel("Capital:")
name_label.setStyleSheet("font-weight: bold;")
name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
value_label = QLabel(f"{country_info['capital']}")
value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
# create a new layout for the name and value labels
layout_name_value = QHBoxLayout()
# add both labels to the new layout
layout_name_value.addWidget(name_label)
layout_name_value.addWidget(value_label)
# add this new layout to the main layout
layout.addLayout(layout_name_value)
# Create a QLabel for each piece of information and add it to the layout
name_label = QLabel("Government:")
name_label.setStyleSheet("font-weight: bold;")
name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
value_label = QLabel(f"{country_info['government']}")
value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
# create a new layout for the name and value labels
layout_name_value = QHBoxLayout()
# add both labels to the new layout
layout_name_value.addWidget(name_label)
layout_name_value.addWidget(value_label)
# add this new layout to the main layout
layout.addLayout(layout_name_value)
# Create a QLabel for each piece of information and add it to the layout
name_label = QLabel("Size:")
name_label.setStyleSheet("font-weight: bold;")
name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
value_label = QLabel(f"{country_info['size']}")
value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
# create a new layout for the name and value labels
layout_name_value = QHBoxLayout()
# add both labels to the new layout
layout_name_value.addWidget(name_label)
layout_name_value.addWidget(value_label)
# add this new layout to the main layout
layout.addLayout(layout_name_value)
# Create a QLabel for each piece of information and add it to the layout
name_label = QLabel("Population:")
name_label.setStyleSheet("font-weight: bold;")
name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
value_label = QLabel(f"{country_info['population']}")
value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
# create a new layout for the name and value labels
layout_name_value = QHBoxLayout()
# add both labels to the new layout
layout_name_value.addWidget(name_label)
layout_name_value.addWidget(value_label)
# add this new layout to the main layout
layout.addLayout(layout_name_value)
# Create a QWidget to hold the layout and add it to the right dock
widget = QWidget()
widget.setLayout(layout)
dock_widget = QDockWidget()
dock_widget.setWindowTitle("Country Informations")
dock_widget.setWidget(widget)
self.addDockWidget(Qt.RightDockWidgetArea, dock_widget)
dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures) # removes close button and full screen button
#dock_widget.setFixedSize(int(self.width()*0.20), self.height())
dock_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def highlight_pixels(self, RGB):
#BLOCK ONE
#Timer 1 starts
start_time = time.perf_counter()
# Copy a numpy array that represent the base image. This is so that every time the highlight_pixels function is called it reverts previous changes
temp_image = self.base_image_array.copy()
# Print time 1
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to initialise image: {elapsed_time:.6f} milliseconds")
#BLOCK TWO
#Timer 2 starts
start_time = time.perf_counter()
# Select pixels that match the target color
coordinates = self.color_dict.get(RGB)
# Set their color to green
temp_image[coordinates[:, 0], coordinates[:, 1], :] = (83, 255, 26)
# Print time 2
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to change color: {elapsed_time:.6f} milliseconds")
#BLOCK THREE
#Timer 3 starts
start_time = time.perf_counter()
#convert array back to image (qimage)
temp_image = qimage2ndarray.array2qimage(temp_image)
# convert the image back to pixmap
self.map_image = QPixmap.fromImage(temp_image)
# update the map scene
self.view.scene().clear()
self.view.scene().addPixmap(self.map_image)
# Print time 3
end_time = time.perf_counter()
elapsed_time = (end_time - start_time) * 1000
print(f"Time taken to show image: {elapsed_time:.6f} milliseconds")
# Set up an event filter to recognize whether it should pan around or print informations.
def eventFilter(self, obj, event):
if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
if self.dragCheck == False:
# Get the position of the mouse click in viewport coordinates
pos = event.pos()
# Convert the viewport coordinates to scene coordinates
scene_pos = self.view.mapToScene(pos)
# Get the pixel at the scene coordinates
pixel = self.map_image.toImage().pixel(int(scene_pos.x()), int(scene_pos.y()))
# Get the red, green, and blue components of the pixel
red = qRed(pixel)
green = qGreen(pixel)
blue = qBlue(pixel)
# Assign the RGB values to the RGB variable
RGB = (red, green, blue)
print("RGB:", RGB) # You can replace this with the call to display_country_info with the RGB variable
self.display_country_info(RGB)
self.highlight_pixels(RGB)
return True
return QMainWindow.eventFilter(self, obj, event)
# Check if Ctrl is beign pressed
def CtrlPressEvent(self, event):
if event.key() == Qt.Key_Control:
self.dragCheck = True
self.view.setDragMode(QGraphicsView.ScrollHandDrag)
#print("drag")
# Check if Ctrl is beign released
def CtrlReleaseEvent(self, event):
if event.key() == Qt.Key_Control:
self.dragCheck = False
self.view.setDragMode(QGraphicsView.NoDrag)
#print("nodrag", self.dragCheck)
if __name__ == "__main__":
app = QApplication(sys.argv)
viewer = MapViewer("map.png")
#viewer = MapViewer("countries_ee.png")
viewer.show()
sys.exit(app.exec_())
由于musicamante的提示,當用戶點擊“國家”顏色時,我能夠獲得更好的方法來更改它們。我實現這一點的方法是首先分解基礎圖像(“政治地圖”),以便將每個“國家”保存為具有透明背景的單個png圖像。然后,我通過為每個“國家”創建一個QGraphicsPixmapItem并將它們添加到正確坐標的視圖中,重新創建了“地圖”,有點像拼圖。此時,由于每個國家都是一個單獨的QGraphicsPixmapItem,因此使用QGraphicsColorizeEffect()可以更容易地更改它們的顏色,該方法會對應用到的項目應用顏色。
這真的很快,因為它不需要像我最初嘗試的那樣,從數組到QImage再到整個圖像的QPixmap的各種轉換(據我理解)。