fix: per-item transform in batch export (correct placement across sizes)
QueuePanel reused one pixel TransformState for all non-fine-tuned items, mis-placing garments/prints of other sizes. Store the ratio-based template instead and recompute each item from its own garment/print size. - add core.composer.image_size() and resolve_transform() (pure, tested) - queue: set_template() replaces set_transform(); per-item resolve + size cache - template_panel: expose current_template(); main_window pushes it to the queue - tests: +4 covering fine-tune precedence, per-size recompute, empty inputs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -257,8 +257,14 @@ class MainWindow(QMainWindow):
|
||||
# Template selection updates canvas, param panel, and queue template
|
||||
self.template_panel.template_applied.connect(self.image_canvas.set_transform)
|
||||
self.template_panel.template_applied.connect(self.transform_panel.set_transform)
|
||||
self.template_panel.template_applied.connect(self.queue_panel.set_transform)
|
||||
self.template_panel.template_applied.connect(self.export_panel.set_transform)
|
||||
# Give the queue the template itself (ratio-based) so each item is laid
|
||||
# out from its own garment/print size, not one shared pixel transform.
|
||||
# template_changed fires on selection even before a garment is loaded;
|
||||
# template_applied covers re-applies once sizes are known.
|
||||
self.template_panel.template_changed.connect(self._sync_queue_template)
|
||||
self.template_panel.template_applied.connect(self._sync_queue_template)
|
||||
self.queue_panel.set_template(self.template_panel.current_template()) # initial sync
|
||||
|
||||
# Output settings (dir/format/quality) flow to the queue for batch export
|
||||
self.export_panel.export_options_changed.connect(self.queue_panel.set_export_options)
|
||||
@@ -346,6 +352,11 @@ class MainWindow(QMainWindow):
|
||||
self._loading_queue_item = False
|
||||
self._update_reset_availability()
|
||||
|
||||
def _sync_queue_template(self, _state=None):
|
||||
"""Push the currently selected template (ratio-based) to the queue so
|
||||
batch export recomputes each item from its own garment/print size."""
|
||||
self.queue_panel.set_template(self.template_panel.current_template())
|
||||
|
||||
def _on_transform_changed(self, state):
|
||||
"""Route transform changes to panels and mark the active queue item fine-tuned."""
|
||||
# Keep template panel and export panel up to date for save/export actions
|
||||
|
||||
@@ -21,8 +21,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from core.composer import compose
|
||||
from core.models import BatchMode, ExportOptions, ImageAsset, TransformState
|
||||
from core.composer import compose, image_size, resolve_transform
|
||||
from core.models import BatchMode, ExportOptions, ImageAsset, Template, TransformState
|
||||
from services.file_service import (
|
||||
get_output_dir,
|
||||
make_safe_output_path,
|
||||
@@ -182,7 +182,8 @@ class QueuePanel(QWidget):
|
||||
super().__init__(parent)
|
||||
self._garments: List[ImageAsset] = []
|
||||
self._prints: List[ImageAsset] = []
|
||||
self._template_transform: Optional[TransformState] = None
|
||||
self._template: Optional[Template] = None
|
||||
self._size_cache: dict = {} # path -> (w, h), reused within a batch run
|
||||
self._export_options: ExportOptions = ExportOptions()
|
||||
self._running = False
|
||||
self._setup_ui()
|
||||
@@ -277,9 +278,14 @@ class QueuePanel(QWidget):
|
||||
self._prints = prints
|
||||
self._rebuild_queue()
|
||||
|
||||
def set_transform(self, state: TransformState):
|
||||
"""Store the template transform applied to items that have not been fine-tuned."""
|
||||
self._template_transform = state
|
||||
def set_template(self, template: Optional[Template]):
|
||||
"""Store the selected template (ratio-based) for items not fine-tuned.
|
||||
|
||||
Each item's pixel transform is recomputed from its own garment and
|
||||
print sizes at export time, so differently sized images in one batch
|
||||
are placed correctly (see _resolve_transform).
|
||||
"""
|
||||
self._template = template
|
||||
|
||||
def set_export_options(self, options: ExportOptions):
|
||||
self._export_options = options
|
||||
@@ -366,6 +372,7 @@ class QueuePanel(QWidget):
|
||||
|
||||
def _new_run_dir(self):
|
||||
"""Timestamped folder shared by all images in one export run."""
|
||||
self._size_cache.clear() # fresh sizes per run
|
||||
base = self._export_options.output_dir or str(get_output_dir())
|
||||
return timestamped_run_dir(base)
|
||||
|
||||
@@ -387,11 +394,33 @@ class QueuePanel(QWidget):
|
||||
self._running = False
|
||||
self._batch_btn.setText("开始批量导出")
|
||||
|
||||
def _image_size(self, path):
|
||||
"""Return (w, h) for path, caching within a run. None if unreadable."""
|
||||
key = str(path)
|
||||
if key not in self._size_cache:
|
||||
try:
|
||||
self._size_cache[key] = image_size(path)
|
||||
except (OSError, ValueError) as exc:
|
||||
logger.error("Cannot read image size %s: %s", key, exc)
|
||||
self._size_cache[key] = None
|
||||
return self._size_cache[key]
|
||||
|
||||
def _resolve_transform(self, item: QueueItem) -> Optional[TransformState]:
|
||||
"""Per-item transform: the item's own fine-tune, else the selected
|
||||
template recomputed for this item's garment and print sizes."""
|
||||
if item.transform:
|
||||
return item.transform
|
||||
return resolve_transform(
|
||||
self._template, None,
|
||||
self._image_size(item.garment.path),
|
||||
self._image_size(item.print_asset.path),
|
||||
)
|
||||
|
||||
def _export_item(self, row: int, item: QueueItem, run_dir):
|
||||
transform = item.transform or self._template_transform
|
||||
transform = self._resolve_transform(item)
|
||||
if not transform:
|
||||
item.status = "失败"
|
||||
item.error = "无印花参数(请先在画布中调整印花位置)"
|
||||
item.error = "无印花参数(请先选择模板或在画布中调整印花位置)"
|
||||
self._model.update_row(row)
|
||||
return
|
||||
|
||||
|
||||
@@ -165,6 +165,14 @@ class TemplatePanel(QWidget):
|
||||
return self._state_for_template(t)
|
||||
return None
|
||||
|
||||
def current_template(self) -> Optional[Template]:
|
||||
"""Return the currently selected template (ratio-based), or None.
|
||||
|
||||
The queue uses this to recompute each item's placement from its own
|
||||
garment and print sizes, rather than reusing one pixel transform.
|
||||
"""
|
||||
return self._current_template()
|
||||
|
||||
def set_transform(self, state: TransformState):
|
||||
"""Store current print transform (used when saving a template)."""
|
||||
self._current_state = state
|
||||
|
||||
@@ -9,6 +9,36 @@ from core.models import ComposeResult, ExportOptions, TransformState
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def image_size(path: Union[str, Path]):
|
||||
"""Return (width, height) of an image in pixels.
|
||||
|
||||
Reads only the image header (PIL is lazy), so it is cheap to call per
|
||||
queue item during batch export. Raises on unreadable/invalid files.
|
||||
"""
|
||||
with Image.open(str(path)) as img:
|
||||
return img.size
|
||||
|
||||
|
||||
def resolve_transform(template, item_transform, garment_size, print_size):
|
||||
"""Pick the pixel transform for one batch item.
|
||||
|
||||
A fine-tuned per-item transform wins. Otherwise the template is recomputed
|
||||
from THIS item's garment and print sizes, so differently sized images in a
|
||||
single batch are each placed correctly (the previous code reused one shared
|
||||
pixel transform and mis-placed items of other sizes). Returns None when no
|
||||
transform can be determined.
|
||||
"""
|
||||
if item_transform is not None:
|
||||
return item_transform
|
||||
if template is None or not garment_size or not print_size:
|
||||
return None
|
||||
gw, gh = garment_size
|
||||
pw, ph = print_size
|
||||
if gw <= 0 or gh <= 0:
|
||||
return None
|
||||
return template.to_transform_state(gw, gh, pw or None, ph or None)
|
||||
|
||||
|
||||
def compose(
|
||||
garment_path: Union[str, Path],
|
||||
print_path: Union[str, Path],
|
||||
|
||||
Reference in New Issue
Block a user