How to Build a Simple WebP Conversion Tool with Python, Qt and OpenCV
Tags: webp, python, qt, opencv
Published: 2021-09-26 02:01:33

"WebP is an image format employing both lossy and lossless compression, and supports animation and alpha transparency. Developed by Google, it is designed to create files that are smaller for the same quality, or of higher quality for the same size, than JPEG, PNG, and GIF image formats" - [Wikipedia](https://en.wikipedia.org/wiki/WebP). Speed is one of the key factors of SEO optimization. Developers tend to use WebP to replace JPEG, PNG, and GIF in order to make web pages SEO friendly. This article demonstrates how to build a simple WebP conversion tool with Python, Qt and OpenCV.

## Installation
- [Python 3.x](https://www.python.org/downloads/)
- OpenCV and Qt (pyside2 or pyside6)
    
    ```bash
    pip install opencv-python pyside2
    ```

## Developing the WebP Conversion Tool

### Step 1: Design the layout and load UI to Python Code
Let's open `Python/Lib/site-packages/PySide2/designer` to design the GUI.

![Design WebP conversion tool](https://www.dynamsoft.com/codepool/img/2021/09/qt-ui-webp-conversion.jpg)

- Label: display the loaded image.
- Horizontal Slider: adjust the quality of the WebP image.
- Push Button: trigger WebP conversion.
- List Widget: append image files to the list.

Once the UI design is done, we convert the `.ui` file to `.py` file using `pyside2-uic`, which is located at `Python/Scripts/`.

```bash
pyside2-uic design.ui -o design.py
```

The next step is to load the `design.py` file and add the following code to the `main.py` file:

```python
import sys
from PySide2.QtGui import QPixmap, QImage, QPainter, QPen, QColor
from PySide2.QtWidgets import QApplication, QMainWindow, QInputDialog
from PySide2.QtCore import QFile, QTimer, QEvent
from PySide2.QtWidgets import *
from design import Ui_MainWindow

import os
import cv2

from PySide2.QtCore import QObject, QThread, Signal

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setAcceptDrops(True)
        
def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
```

Now, running the `main.py` file will show the GUI.

### Step 2: Load image files to list widget and display images
There are two ways to load an image file or a folder:
- Click a button to open the system file dialog. 
- Drag and drop the image file or folder to the application.

To open the system file dialog, we use `QFileDialog`, which provides `getOpenFileName` to pick a file and `getExistingDirectory` to pick a directory.

```python
self.ui.actionOpen_File.triggered.connect(self.openFile)
self.ui.actionOpen_Folder.triggered.connect(self.openFolder)

def openFile(self):
    filename = QFileDialog.getOpenFileName(self, 'Open File',
                                            self._path, "Barcode images (*)")

def openFolder(self):
    directory = QFileDialog.getExistingDirectory(self, 'Open Folder',
                                            self._path, QFileDialog.ShowDirsOnly)

```

To enable drag and drop for file and folder, we must set `setAcceptDrops(True)` for the `MainWindow` and override the `dragEnterEvent` and `dropEvent` methods:

```python
def __init__(self):
    # ...
    self.setAcceptDrops(True)

def dragEnterEvent(self, event):
    event.acceptProposedAction()

def dropEvent(self, event):
    urls = event.mimeData().urls()
    filename = urls[0].toLocalFile()
    if os.path.isdir(filename):
        self.appendFolder(filename)
    else:
        self.appendFile(filename)
    event.acceptProposedAction()
```

As we get the file path, create a new list widget item and add it to the list widget:

```python
item = QListWidgetItem()
item.setText(filename)
self.ui.listWidget.addItem(item)
```

To display the image in the Qt label, we need to convert Mat to QImage:

```python
def showImage(self, filename):
    frame = cv2.imread(filename)
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    image = QImage(frame, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_RGB888)
    pixmap = QPixmap.fromImage(image)
    self._pixmap = self.resizeImage(pixmap)
    self.ui.label.setPixmap(self._pixmap)
    return frame
```

### Step 3: Get the slider value and convert image to WebP
Because the latest OpenCV supports WebP, it is convenient to convert an image to WebP using `cv2.imwrite()`:

```python
quality = int(self.ui.horizontalSlider.value())
frame = cv2.imread(filename)
webp_file = filename.split('.')[0] + '.webp'
cv2.imwrite(webp_file, frame, [cv2.IMWRITE_WEBP_QUALITY, quality])
```

Considering the performance of operating multiple images, we move the WebP conversion code to a worker thread. In addition, showing a progress bar makes the application more responsive.

```python
class Worker(QObject):
    finished = Signal()
    progress = Signal(object)

    def __init__(self, files, quality):
        super(Worker, self).__init__()
        self.files = files
        self.total = len(files)
        self.isRunning = True
        self.quality = quality

    def run(self):
        count = 0
        keys = list(self.files.keys())
        while self.isRunning and len(self.files) > 0:
            filename = keys[count]
            count += 1
            print(filename)
            frame = cv2.imread(filename)
            webp_file = filename.split('.')[0] + '.webp'
            cv2.imwrite(webp_file, frame, [cv2.IMWRITE_WEBP_QUALITY, self.quality])
            self.progress.emit((webp_file, count, self.total))
            self.files.pop(filename)

        self.finished.emit()

def reportProgress(self, data):
    filename, completed, total = data
    self.addImage(filename)
    if not self.isProcessing:
        return

    progress = completed
    self.progress_dialog.setLabelText(str(completed) +"/"+ str(total))
    self.progress_dialog.setValue(progress)
    if completed == total:
        self.onProgressDialogCanceled()
        self.showMessageBox('WebP Conversion', "Done!")

def onProgressDialogCanceled(self):
    self.isProcessing = False
    self.worker.isRunning = False
    self.progress_dialog.cancel()

def runLongTask(self):
    if (len(self._all_images) == 0):
        return
        
    self.isProcessing = True
    self.progress_dialog = QProgressDialog('Progress', 'Cancel', 0, len(self._all_images), self)
    self.progress_dialog.setLabelText('Progress')
    self.progress_dialog.setCancelButtonText('Cancel')
    self.progress_dialog.setRange(0, len(self._all_images))
    self.progress_dialog.setValue(0)
    self.progress_dialog.setMinimumDuration(0)
    self.progress_dialog.show()
    self.progress_dialog.canceled.connect(self.onProgressDialogCanceled)

    self.thread = QThread()
    self.worker = Worker(self._all_images, int(self.ui.label_slider.text()))
    self.worker.moveToThread(self.thread)
    self.thread.started.connect(self.worker.run)
    self.worker.finished.connect(self.thread.quit)
    self.worker.finished.connect(self.worker.deleteLater)
    self.thread.finished.connect(self.thread.deleteLater)
    self.worker.progress.connect(self.reportProgress)
    self.thread.start()
```

### Step 4: Run the application to convert images to WebP

```bash
python main.py
```

![Python WebP conversion tool](https://www.dynamsoft.com/codepool/img/2021/09/qt-webp-conversion-tool.webp)

## Source Code
[https://github.com/yushulx/webp-image-conversion](https://github.com/yushulx/webp-image-conversion)