feat: add 3 upscale methods - Sharp Pixels, Smooth HQ4x-style, Photorealistic
- NEAREST: Sharp pixel edges (best for clean pixel art) - HQ4x-style: Integer scale first, then smooth to target with edge enhancement - LANCZOS: Very smooth (best for photorealistic textures) - Added upscale method selector to UI - Each method uses UnsharpMask for enhanced crispness
This commit is contained in:
parent
da52b99a36
commit
0034cc9453
|
|
@ -279,7 +279,7 @@ class TGAConverter:
|
||||||
|
|
||||||
def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None,
|
def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None,
|
||||||
canvas_size: Optional[Tuple[int, int]] = None,
|
canvas_size: Optional[Tuple[int, int]] = None,
|
||||||
upscale: bool = False) -> Optional[Path]:
|
upscale: bool = False, upscale_method: str = 'nearest') -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Convert a TGA file to PNG with optional canvas sizing.
|
Convert a TGA file to PNG with optional canvas sizing.
|
||||||
|
|
||||||
|
|
@ -312,7 +312,7 @@ class TGAConverter:
|
||||||
|
|
||||||
# Apply canvas sizing if requested
|
# Apply canvas sizing if requested
|
||||||
if canvas_size:
|
if canvas_size:
|
||||||
image = self._apply_canvas(image, canvas_size, upscale)
|
image = self._apply_canvas(image, canvas_size, upscale, upscale_method)
|
||||||
|
|
||||||
# Determine output path and format
|
# Determine output path and format
|
||||||
if output_name is None:
|
if output_name is None:
|
||||||
|
|
@ -347,7 +347,7 @@ class TGAConverter:
|
||||||
|
|
||||||
# Apply canvas sizing if requested
|
# Apply canvas sizing if requested
|
||||||
if canvas_size:
|
if canvas_size:
|
||||||
image = self._apply_canvas(image, canvas_size, upscale)
|
image = self._apply_canvas(image, canvas_size, upscale, upscale_method)
|
||||||
|
|
||||||
# Determine output path and format
|
# Determine output path and format
|
||||||
if output_name is None:
|
if output_name is None:
|
||||||
|
|
@ -408,7 +408,7 @@ class TGAConverter:
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _apply_canvas(self, image, canvas_size, upscale=False):
|
def _apply_canvas(self, image, canvas_size, upscale=False, upscale_method='nearest'):
|
||||||
"""
|
"""
|
||||||
Place image centered on a canvas of specified size.
|
Place image centered on a canvas of specified size.
|
||||||
|
|
||||||
|
|
@ -416,6 +416,7 @@ class TGAConverter:
|
||||||
image: Source PIL Image
|
image: Source PIL Image
|
||||||
canvas_size: (width, height) for output canvas
|
canvas_size: (width, height) for output canvas
|
||||||
upscale: Whether to upscale small images to fit canvas better
|
upscale: Whether to upscale small images to fit canvas better
|
||||||
|
upscale_method: 'nearest' (sharp pixels), 'hq4x' (smoothed), or 'lanczos' (smooth)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
New image with canvas size, source image centered
|
New image with canvas size, source image centered
|
||||||
|
|
@ -435,17 +436,34 @@ class TGAConverter:
|
||||||
scale = min(max_size / img_w, max_size / img_h)
|
scale = min(max_size / img_w, max_size / img_h)
|
||||||
|
|
||||||
if scale > 1: # Only upscale, never downscale
|
if scale > 1: # Only upscale, never downscale
|
||||||
# For pixel art/game icons, use integer scaling + NEAREST for sharpness
|
new_w = int(img_w * scale)
|
||||||
# Round scale to nearest integer for clean pixel edges
|
new_h = int(img_h * scale)
|
||||||
int_scale = max(1, round(scale))
|
|
||||||
new_w = img_w * int_scale
|
|
||||||
new_h = img_h * int_scale
|
|
||||||
|
|
||||||
# Use NEAREST neighbor for pixel art - keeps edges sharp
|
if upscale_method == 'nearest':
|
||||||
image = image.resize((new_w, new_h), Image.Resampling.NEAREST)
|
# NEAREST: Sharp pixels, best for clean pixel art
|
||||||
|
image = image.resize((new_w, new_h), Image.Resampling.NEAREST)
|
||||||
|
|
||||||
# Apply slight sharpening to reduce blur from the upscale
|
elif upscale_method == 'hq4x':
|
||||||
image = image.filter(ImageFilter.SHARPEN)
|
# HQ4x-style: Integer scale first, then smooth to target
|
||||||
|
int_scale = max(2, int(scale))
|
||||||
|
temp_w = img_w * int_scale
|
||||||
|
temp_h = img_h * int_scale
|
||||||
|
|
||||||
|
# Scale up with NEAREST (integer multiple)
|
||||||
|
image = image.resize((temp_w, temp_h), Image.Resampling.NEAREST)
|
||||||
|
|
||||||
|
# Then downscale to target with LANCZOS for smoothing
|
||||||
|
image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# Enhance edges
|
||||||
|
image = image.filter(ImageFilter.EDGE_ENHANCE_MORE)
|
||||||
|
|
||||||
|
else: # lanczos
|
||||||
|
# LANCZOS: Smooth, best for photorealistic textures
|
||||||
|
image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# Apply subtle sharpening for crispness
|
||||||
|
image = image.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
|
||||||
|
|
||||||
img_w, img_h = new_w, new_h
|
img_w, img_h = new_w, new_h
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,13 @@ class TGAConvertWorker(QThread):
|
||||||
conversion_error = pyqtSignal(str)
|
conversion_error = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None,
|
def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None,
|
||||||
canvas_size=None, upscale: bool = False):
|
canvas_size=None, upscale: bool = False, upscale_method: str = 'nearest'):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.converter = converter
|
self.converter = converter
|
||||||
self.cache_path = cache_path
|
self.cache_path = cache_path
|
||||||
self.canvas_size = canvas_size
|
self.canvas_size = canvas_size
|
||||||
self.upscale = upscale
|
self.upscale = upscale
|
||||||
|
self.upscale_method = upscale_method
|
||||||
self._is_running = True
|
self._is_running = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
@ -70,7 +71,8 @@ class TGAConvertWorker(QThread):
|
||||||
output_path = self.converter.convert_tga_to_png(
|
output_path = self.converter.convert_tga_to_png(
|
||||||
tga_path,
|
tga_path,
|
||||||
canvas_size=self.canvas_size,
|
canvas_size=self.canvas_size,
|
||||||
upscale=self.upscale
|
upscale=self.upscale,
|
||||||
|
upscale_method=self.upscale_method
|
||||||
)
|
)
|
||||||
if output_path:
|
if output_path:
|
||||||
success += 1
|
success += 1
|
||||||
|
|
@ -197,10 +199,17 @@ class TGAConverterDialog(QDialog):
|
||||||
|
|
||||||
canvas_layout.addSpacing(20)
|
canvas_layout.addSpacing(20)
|
||||||
|
|
||||||
self.upscale_check = QCheckBox("Upscale small icons")
|
canvas_layout.addWidget(QLabel("Upscale:"))
|
||||||
self.upscale_check.setChecked(True)
|
self.upscale_method_combo = QComboBox()
|
||||||
self.upscale_check.setToolTip("Scale up small icons to better fill the canvas")
|
self.upscale_method_combo.addItem("Sharp Pixels (NEAREST)", "nearest")
|
||||||
canvas_layout.addWidget(self.upscale_check)
|
self.upscale_method_combo.addItem("Smooth (HQ4x-style)", "hq4x")
|
||||||
|
self.upscale_method_combo.addItem("Photorealistic (LANCZOS)", "lanczos")
|
||||||
|
self.upscale_method_combo.setToolTip(
|
||||||
|
"NEAREST: Sharp pixel edges (best for pixel art)\n"
|
||||||
|
"HQ4x: Smooth but keeps details (best for game icons)\n"
|
||||||
|
"LANCZOS: Very smooth (best for photos)"
|
||||||
|
)
|
||||||
|
canvas_layout.addWidget(self.upscale_method_combo)
|
||||||
|
|
||||||
canvas_layout.addStretch()
|
canvas_layout.addStretch()
|
||||||
layout.addWidget(canvas_group)
|
layout.addWidget(canvas_group)
|
||||||
|
|
@ -323,11 +332,14 @@ class TGAConverterDialog(QDialog):
|
||||||
|
|
||||||
# Get canvas settings
|
# Get canvas settings
|
||||||
canvas_size = self.canvas_combo.currentData()
|
canvas_size = self.canvas_combo.currentData()
|
||||||
upscale = self.upscale_check.isChecked()
|
upscale_method = self.upscale_method_combo.currentData()
|
||||||
|
|
||||||
# Start worker with settings
|
# Start worker with settings
|
||||||
cache_path = self.converter._cache_path if self.converter._cache_path else None
|
cache_path = self.converter._cache_path if self.converter._cache_path else None
|
||||||
self.convert_worker = TGAConvertWorker(self.converter, cache_path, canvas_size, upscale)
|
self.convert_worker = TGAConvertWorker(
|
||||||
|
self.converter, cache_path, canvas_size,
|
||||||
|
upscale=True, upscale_method=upscale_method
|
||||||
|
)
|
||||||
self.convert_worker.progress_update.connect(self._on_progress)
|
self.convert_worker.progress_update.connect(self._on_progress)
|
||||||
self.convert_worker.file_converted.connect(self._on_file_converted)
|
self.convert_worker.file_converted.connect(self._on_file_converted)
|
||||||
self.convert_worker.conversion_complete.connect(self._on_conversion_complete)
|
self.convert_worker.conversion_complete.connect(self._on_conversion_complete)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue