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:
LemonNexus 2026-02-11 17:25:00 +00:00
parent da52b99a36
commit 0034cc9453
2 changed files with 51 additions and 21 deletions

View File

@ -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

View File

@ -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)