Vector Halftone Generator is a Python-based application that transforms raster images into artistic vector halftones. With an intuitive graphical user interface built using PyQt5, this tool enables designers and digital artists to create unique halftone effects that can be easily exported as SVG files for further editing in graphic design software like Adobe Illustrator.

Overview
The application offers a wide range of customizable options:
- Dot Shape: Choose from “Circle”, “Square”, or “Star” (fancy dot). The “Star” option adds a creative twist to traditional halftone dots.
- Grid Size: Adjusts the size of the grid cells which determines the overall density of dots.
- Dot Scale (%): Controls the scale of the dots relative to the grid cell size.
- Rotation Angle: Rotates the halftone pattern to achieve various artistic effects.
- Jitter (pixels): Adds randomized positional offsets to each dot, creating a more organic look.
- Threshold (0-255): Sets the brightness threshold; only areas where the image’s brightness is below this threshold will have a dot applied.
- Colored Option: When enabled, each dot will be filled with the color sampled from the original image rather than a fixed black color.
- Optimized Export for Illustrator: This option applies additional settings during export to ensure compatibility and flexibility when editing the SVG in Adobe Illustrator.
Usage
Running the Application
- Desktop Executable:
You can run the standalone executable provided. Simply double-click the executable (e.g.,VectorHalftoneGenerator.exe
). This version includes all necessary dependencies and will run on any compatible Windows system without requiring a separate Python installation. - From Source:
Ensure that you have Python 3.11 or higher installed along with the following dependencies:
- PyQt5
- Pillow
- numpy
- svgwrite Install dependencies using:
pip install PyQt5 pillow numpy svgwrite
Then, navigate to the project directory and run:
python main.py
How to Use the Application
- Loading an Image
Click the Load Image button to select a raster image (PNG, JPG, JPEG, BMP) from your computer. - Previewing the Halftone
Checking the Preview Halftone option will automatically update the rendered preview as you adjust the settings:
- Select your preferred Dot Shape (Circle, Square, Star).
- Adjust the Grid Size, Dot Scale, Rotation Angle, Jitter, and Threshold using the respective controls.
- Enable the Colored checkbox if you wish to use the original image’s colors for the dots.
- Exporting the Halftone as SVG
Once satisfied with the preview, click Export as SVG. Choose a destination to save your vector file. If the Optimized Export for Illustrator option is enabled, the SVG will include additional settings to facilitate editing in Adobe Illustrator.
ui_main.py
# ui_main.py
import numpy as np
import random
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import (
QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog,
QComboBox, QCheckBox, QSpinBox, QGroupBox
)
from PyQt5.QtCore import Qt
from halftone_engine import generate_halftone_preview, generate_svg
def pil_to_pixmap(pil_image):
rgb_image = pil_image.convert("RGB")
data = np.array(rgb_image)
height, width, channel = data.shape
bytes_per_line = 3 * width
qimage = QImage(data.data, width, height, bytes_per_line, QImage.Format_RGB888)
return QPixmap.fromImage(qimage)
class HalftoneUI(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Raster to Vector Halftone Generator by Mehmet Sensoy")
self.setMinimumSize(900, 600)
self.image_path = None
self.image_label = QLabel("No image loaded")
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet("background-color: #eee; border: 1px solid #ccc;")
self.load_button = QPushButton("Load Image")
self.export_button = QPushButton("Export as SVG")
self.preview_checkbox = QCheckBox("Preview Halftone")
# Updated dot shape options: "Circle", "Square", "Star"
self.dot_shape_combo = QComboBox()
self.dot_shape_combo.addItems(["Circle", "Square", "Star"])
self.grid_size_spin = QSpinBox()
self.grid_size_spin.setRange(2, 100)
self.grid_size_spin.setValue(10)
self.dot_scale_spin = QSpinBox()
self.dot_scale_spin.setRange(1, 300)
self.dot_scale_spin.setValue(100)
self.angle_spin = QSpinBox()
self.angle_spin.setRange(0, 180)
self.angle_spin.setValue(0)
self.jitter_spin = QSpinBox()
self.jitter_spin.setRange(0, 20)
self.jitter_spin.setValue(0)
self.threshold_spin = QSpinBox()
self.threshold_spin.setRange(0, 255)
self.threshold_spin.setValue(128)
# New "Colored" option to use original image colors
self.colored_checkbox = QCheckBox("Colored")
self.illustrator_checkbox = QCheckBox("Optimized export for Illustrator")
self._init_layout()
self._connect_signals()
def _init_layout(self):
layout = QHBoxLayout()
control_panel = QVBoxLayout()
controls = QGroupBox("Settings")
controls_layout = QVBoxLayout()
controls_layout.addWidget(QLabel("Dot Shape:"))
controls_layout.addWidget(self.dot_shape_combo)
controls_layout.addWidget(QLabel("Grid Size:"))
controls_layout.addWidget(self.grid_size_spin)
controls_layout.addWidget(QLabel("Dot Scale (%):"))
controls_layout.addWidget(self.dot_scale_spin)
controls_layout.addWidget(QLabel("Rotation Angle:"))
controls_layout.addWidget(self.angle_spin)
controls_layout.addWidget(QLabel("Jitter (pixels):"))
controls_layout.addWidget(self.jitter_spin)
controls_layout.addWidget(QLabel("Threshold (0-255):"))
controls_layout.addWidget(self.threshold_spin)
controls_layout.addWidget(self.colored_checkbox)
controls_layout.addWidget(self.illustrator_checkbox)
controls_layout.addWidget(self.preview_checkbox)
controls_layout.addWidget(self.load_button)
controls_layout.addWidget(self.export_button)
controls.setLayout(controls_layout)
control_panel.addWidget(controls)
control_panel.addStretch()
layout.addLayout(control_panel, 1)
layout.addWidget(self.image_label, 3)
self.setLayout(layout)
def _connect_signals(self):
self.load_button.clicked.connect(self.load_image)
self.export_button.clicked.connect(self.export_svg)
self.preview_checkbox.stateChanged.connect(self.toggle_preview)
# Auto-update preview when settings change
self.grid_size_spin.valueChanged.connect(self.update_preview_if_checked)
self.dot_scale_spin.valueChanged.connect(self.update_preview_if_checked)
self.angle_spin.valueChanged.connect(self.update_preview_if_checked)
self.dot_shape_combo.currentIndexChanged.connect(self.update_preview_if_checked)
self.jitter_spin.valueChanged.connect(self.update_preview_if_checked)
self.threshold_spin.valueChanged.connect(self.update_preview_if_checked)
self.colored_checkbox.stateChanged.connect(self.update_preview_if_checked)
def load_image(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Open Image", "", "Images (*.png *.jpg *.jpeg *.bmp)")
if file_path:
self.image_path = file_path
self.update_image_display()
self.preview_checkbox.setChecked(False)
def update_image_display(self):
if self.image_path:
pixmap = QPixmap(self.image_path)
self.image_label.setPixmap(pixmap.scaled(
self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
))
def toggle_preview(self):
if self.preview_checkbox.isChecked():
self.update_preview()
else:
self.update_image_display()
def update_preview_if_checked(self):
if self.preview_checkbox.isChecked():
self.update_preview()
def update_preview(self):
if not self.image_path:
return
shape = self.dot_shape_combo.currentText().lower()
grid_size = self.grid_size_spin.value()
scale = self.dot_scale_spin.value() / 100.0
angle = self.angle_spin.value()
jitter = self.jitter_spin.value()
threshold = self.threshold_spin.value()
colored = self.colored_checkbox.isChecked()
preview_img = generate_halftone_preview(
self.image_path, grid_size, scale, shape, angle,
jitter=jitter,
threshold=threshold,
colored=colored
)
pixmap = pil_to_pixmap(preview_img)
self.image_label.setPixmap(pixmap.scaled(
self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
))
def export_svg(self):
if not self.image_path:
return
save_path, _ = QFileDialog.getSaveFileName(self, "Save SVG", "", "SVG files (*.svg)")
if save_path:
shape = self.dot_shape_combo.currentText().lower()
grid_size = self.grid_size_spin.value()
scale = self.dot_scale_spin.value() / 100.0
angle = self.angle_spin.value()
jitter = self.jitter_spin.value()
threshold = self.threshold_spin.value()
colored = self.colored_checkbox.isChecked()
illustrator_optimized = self.illustrator_checkbox.isChecked()
generate_svg(
self.image_path, save_path,
grid_size=grid_size,
scale=scale,
shape=shape,
angle=angle,
jitter=jitter,
threshold=threshold,
colored=colored,
illustrator_optimized=illustrator_optimized
)
print(f"✅ SVG saved to {save_path}")
if __name__ == "__main__":
# For testing outside main.py
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = HalftoneUI()
window.show()
sys.exit(app.exec_())
main.py
# main.py
import sys
from PyQt5.QtWidgets import QApplication
from ui_main import HalftoneUI
def main():
app = QApplication(sys.argv)
window = HalftoneUI()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
halftone_engine.py
# halftone_engine.py
from PIL import Image, ImageDraw
import numpy as np
import random
import svgwrite
import math
def _draw_star(draw, cx, cy, outer_radius, inner_radius, fill):
# Compute points for a 5-pointed star
points = []
for i in range(10):
angle = math.radians(i * 36)
r = outer_radius if i % 2 == 0 else inner_radius
x = cx + r * math.cos(angle)
y = cy + r * math.sin(angle)
points.append((x, y))
draw.polygon(points, fill=fill)
def _create_star_path(cx, cy, outer, inner):
# Create an SVG path for a 5-pointed star centered at (cx,cy)
points = []
for i in range(10):
angle = math.radians(i * 36)
r = outer if i % 2 == 0 else inner
x = cx + r * math.cos(angle)
y = cy + r * math.sin(angle)
points.append((x, y))
# Build a path string. Start at the first point then draw lines to the rest and close.
path = f"M {points[0][0]},{points[0][1]} "
for pt in points[1:]:
path += f"L {pt[0]},{pt[1]} "
path += "Z"
return path
def _rgb_to_hex(rgb):
return '#%02x%02x%02x' % rgb
def generate_halftone_preview(image_path, grid_size=10, scale=1.0, shape='circle', angle=0,
jitter=0, threshold=128, colored=False):
"""Returns a Pillow Image with a halftone effect drawn as a raster preview.
Options:
threshold: only draws dots if the normalized brightness is below the threshold.
colored: if True, uses the color from the original image for each dot.
"""
# Load original image for color sampling if needed
if colored:
orig_img = Image.open(image_path).convert("RGB")
img_gray = orig_img.convert("L")
else:
img_gray = Image.open(image_path).convert("L")
width, height = img_gray.size
output = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(output)
pixels = np.array(img_gray)
norm_threshold = threshold / 255.0
for y in range(0, height, grid_size):
for x in range(0, width, grid_size):
# Center of the cell with random jitter
cx = x + grid_size // 2 + random.uniform(-jitter, jitter)
cy = y + grid_size // 2 + random.uniform(-jitter, jitter)
if cx >= width or cy >= height:
continue
brightness = pixels[int(cy) % height, int(cx) % width] / 255.0
if brightness > norm_threshold:
continue
radius = grid_size * (1.0 - brightness) * scale * 0.5
fill_color = "black"
if colored:
# Sample the color from the original image
rgb = orig_img.getpixel((int(cx), int(cy)))
fill_color = _rgb_to_hex(rgb)
if shape == 'circle':
draw.ellipse([
(cx - radius, cy - radius),
(cx + radius, cy + radius)
], fill=fill_color)
elif shape == 'square':
draw.rectangle([
(cx - radius, cy - radius),
(cx + radius, cy + radius)
], fill=fill_color)
elif shape == 'star':
outer = radius
inner = radius / 2
_draw_star(draw, cx, cy, outer, inner, fill=fill_color)
return output
def generate_svg(image_path, svg_path, grid_size=10, scale=1.0, shape='circle', angle=0,
jitter=0, threshold=128, colored=False, illustrator_optimized=False):
"""Generate vector halftone and save as SVG.
Options:
threshold: only draws dots if the normalized brightness is below the threshold.
colored: if True, uses the color from the original image for each dot.
"""
# Load original image for color sampling if needed
if colored:
orig_img = Image.open(image_path).convert("RGB")
img_gray = orig_img.convert("L")
else:
img_gray = Image.open(image_path).convert("L")
width, height = img_gray.size
pixels = np.array(img_gray)
dwg = svgwrite.Drawing(svg_path, size=(f"{width}px", f"{height}px"))
if illustrator_optimized:
dwg.attribs['version'] = "1.1"
dwg.attribs['baseProfile'] = "full"
norm_threshold = threshold / 255.0
for y in range(0, height, grid_size):
for x in range(0, width, grid_size):
base_cx = x + grid_size // 2
base_cy = y + grid_size // 2
cx = base_cx + random.uniform(-jitter, jitter)
cy = base_cy + random.uniform(-jitter, jitter)
if cx >= width or cy >= height:
continue
brightness = pixels[int(cy) % height, int(cx) % height] / 255.0
if brightness > norm_threshold:
continue
radius = grid_size * (1.0 - brightness) * scale * 0.5
fill_color = "black"
if colored:
rgb = orig_img.getpixel((int(cx), int(cy)))
fill_color = _rgb_to_hex(rgb)
if shape == 'circle':
dwg.add(dwg.circle(center=(cx, cy), r=radius,
fill=fill_color))
elif shape == 'square':
dwg.add(dwg.rect(insert=(cx - radius, cy - radius),
size=(radius * 2, radius * 2), fill=fill_color))
elif shape == 'star':
star_path = _create_star_path(cx, cy, radius, radius / 2)
dwg.add(dwg.path(d=star_path, fill=fill_color))
dwg.save()
You can download compiled exe from:
Download “Raster_to_Vector_Halftone_Generator” Raster_to_Vector_Halftone_Generator.zip – Downloaded 0 times – 55.20 MB