Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions labelme/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import os.path as osp
import sys
import traceback
from typing import AnyStr

import yaml
from loguru import logger
Expand All @@ -21,11 +20,10 @@


class _LoggerIO(io.StringIO):
def write(self, s: AnyStr) -> int:
assert isinstance(s, str)
if stripped_s := s.strip():
logger.debug(stripped_s)
return len(s)
def write(self, message: str) -> int:
if stripped_message := message.strip():
logger.debug(stripped_message)
return len(message)

def flush(self) -> None:
pass
Expand Down
6 changes: 0 additions & 6 deletions labelme/_automation/bbox_from_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ def get_bboxes_from_texts(
scores: NDArray[np.float32] = np.empty((num_annotations,), dtype=np.float32)
labels: NDArray[np.int32] = np.empty((num_annotations,), dtype=np.int32)
for i, annotation in enumerate(response.annotations):
if annotation.bounding_box is None:
raise ValueError("Bounding box is missing in the annotation.")
if annotation.text not in texts:
raise ValueError(
f"Unexpected text {annotation.text!r} found in the response."
)
boxes[i] = [
annotation.bounding_box.xmin,
annotation.bounding_box.ymin,
Expand Down
6 changes: 3 additions & 3 deletions labelme/_label_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ShapeDict(TypedDict):
flags: dict[str, bool]
description: str
group_id: int | None
mask: NDArray[np.bool] | None
mask: NDArray[np.bool_] | None
other_data: dict


Expand Down Expand Up @@ -97,7 +97,7 @@ def _load_shape_json_obj(shape_json_obj: dict) -> ShapeDict:
)
group_id = shape_json_obj["group_id"]

mask: NDArray[np.bool] | None = None
mask: NDArray[np.bool_] | None = None
if shape_json_obj.get("mask") is not None:
assert isinstance(shape_json_obj["mask"], str), (
f"mask must be base64-encoded PNG: {shape_json_obj['mask']}"
Expand All @@ -106,7 +106,7 @@ def _load_shape_json_obj(shape_json_obj: dict) -> ShapeDict:

other_data = {k: v for k, v in shape_json_obj.items() if k not in SHAPE_KEYS}

loaded: ShapeDict = ShapeDict(
loaded: ShapeDict = dict(
label=label,
points=points,
shape_type=shape_type,
Expand Down
92 changes: 74 additions & 18 deletions labelme/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,14 @@ def __init__(
self.tr("Start drawing ai_mask. Ctrl+LeftClick ends creation."),
enabled=False,
)
createBrushMode = action(
self.tr("Create Brush"),
lambda: self._switch_canvas_mode(edit=False, createMode="brush"),
shortcuts["create_brush"],
"note-pencil.svg",
self.tr("Start painting with brush. Enter/Space to confirm."),
enabled=False,
)
editMode = action(
self.tr("Edit Polygons"),
lambda: self._switch_canvas_mode(edit=True),
Expand Down Expand Up @@ -694,6 +702,7 @@ def __init__(
createLineStripMode=createLineStripMode,
createAiPolygonMode=createAiPolygonMode,
createAiMaskMode=createAiMaskMode,
createBrushMode=createBrushMode,
zoom=zoom,
zoomIn=zoomIn,
zoomOut=zoomOut,
Expand All @@ -716,6 +725,7 @@ def __init__(
("linestrip", createLineStripMode),
("ai_polygon", createAiPolygonMode),
("ai_mask", createAiMaskMode),
("brush", createBrushMode),
]

# Group zoom controls into a list for easier toggling.
Expand All @@ -737,6 +747,7 @@ def __init__(
createLineStripMode,
createAiPolygonMode,
createAiMaskMode,
createBrushMode,
brightnessContrast,
)
# menu shown at right click
Expand Down Expand Up @@ -856,6 +867,34 @@ def __init__(
ai_prompt_action = QtWidgets.QWidgetAction(self)
ai_prompt_action.setDefaultWidget(self._ai_text_to_annotation_widget)

# Brush size control
brushSizeAction = QtWidgets.QWidgetAction(self)
brushSizeAction.setDefaultWidget(QtWidgets.QWidget())
brushSizeAction.defaultWidget().setLayout(QtWidgets.QVBoxLayout())
#
brushSizeLabel = QtWidgets.QLabel(self.tr("Brush Size"))
brushSizeLabel.setAlignment(QtCore.Qt.AlignCenter)
brushSizeAction.defaultWidget().layout().addWidget(brushSizeLabel)
#
self._brushSizeSlider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self._brushSizeSlider.setMinimum(2)
self._brushSizeSlider.setMaximum(40)
self._brushSizeSlider.setValue(20) # Default brush size
self._brushSizeSlider.setTickPosition(QtWidgets.QSlider.TicksBelow)
self._brushSizeSlider.setTickInterval(5)
self._brushSizeSlider.setToolTip(self.tr("Brush Size: 2-40 pixels"))
brushSizeAction.defaultWidget().layout().addWidget(self._brushSizeSlider)
#
self._brushSizeLabel = QtWidgets.QLabel("20")
self._brushSizeLabel.setAlignment(QtCore.Qt.AlignCenter)
brushSizeAction.defaultWidget().layout().addWidget(self._brushSizeLabel)
#
self._brushSizeSlider.valueChanged.connect(
lambda value: self._on_brush_size_changed(value)
)
# Initialize brush size
self.canvas.set_brush_size(20)

self.addToolBar(
Qt.TopToolBarArea,
ToolBar(
Expand All @@ -880,6 +919,8 @@ def __init__(
selectAiModel,
None,
ai_prompt_action,
None,
brushSizeAction,
],
font_base=self.font(),
),
Expand Down Expand Up @@ -1037,6 +1078,11 @@ def queueEvent(self, function):
def show_status_message(self, message, delay=500):
self.statusBar().showMessage(message, delay)

def _on_brush_size_changed(self, value: int) -> None:
"""Handle brush size slider value change"""
self.canvas.set_brush_size(value)
self._brushSizeLabel.setText(str(value))

def _submit_ai_prompt(self, _) -> None:
if (
self.canvas.createMode
Expand Down Expand Up @@ -1436,9 +1482,6 @@ def _load_shape_dicts(self, shape_dicts: list[ShapeDict]) -> None:
default_flags = {}
if self._config["label_flags"]:
for pattern, keys in self._config["label_flags"].items():
if not isinstance(shape.label, str):
logger.warning("shape.label is not str: {}", shape.label)
continue
if re.match(pattern, shape.label):
for key in keys:
default_flags[key] = False
Expand Down Expand Up @@ -1577,17 +1620,30 @@ def newShape(self):
text = ""
if text:
self.labelList.clearSelection()
shape = self.canvas.setLastLabel(text, flags)
# For brush mode, shape is in self.current, not yet in shapes
if self.canvas.current is not None and self.canvas.createMode == "brush":
# Add the shape to shapes first
self.canvas.shapes.append(self.canvas.current)
self.canvas.storeShapes()
shape = self.canvas.setLastLabel(text, flags)
else:
shape = self.canvas.setLastLabel(text, flags)
shape.group_id = group_id
shape.description = description
self.addLabel(shape)
self.canvas.current = None # Clear current after adding to shapes
self.actions.editMode.setEnabled(True)
self.actions.undoLastPoint.setEnabled(False)
self.actions.undo.setEnabled(True)
self.setDirty()
else:
self.canvas.undoLastLine()
self.canvas.shapesBackups.pop()
# User cancelled, remove the shape that was created
if self.canvas.current is not None and self.canvas.createMode == "brush":
# For brush mode, just clear current
self.canvas.current = None
else:
self.canvas.undoLastLine()
self.canvas.shapesBackups.pop()

def scrollRequest(self, delta, orientation):
units = -delta * 0.1 # natural scroll
Expand Down Expand Up @@ -1824,14 +1880,14 @@ def _load_file(self, filename=None):
logger.debug("loaded file: {!r}", filename)
return True

def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
def resizeEvent(self, event):
if (
self.canvas
and not self.image.isNull()
and self._zoom_mode != _ZoomMode.MANUAL_ZOOM
):
self._adjust_scale()
super().resizeEvent(a0)
super().resizeEvent(event)

def _paint_canvas(self) -> None:
if self.image.isNull():
Expand Down Expand Up @@ -1865,9 +1921,9 @@ def enableSaveImageWithData(self, enabled):
self._config["store_data"] = enabled
self.actions.saveWithImageData.setChecked(enabled)

def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
def closeEvent(self, event):
if not self._can_continue():
a0.ignore()
event.ignore()
self.settings.setValue("filename", self.filename if self.filename else "")
self.settings.setValue("window/size", self.size())
self.settings.setValue("window/position", self.pos())
Expand All @@ -1876,23 +1932,23 @@ def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
# ask the use for where to save the labels
# self.settings.setValue('window/geometry', self.saveGeometry())

def dragEnterEvent(self, a0: QtGui.QDragEnterEvent) -> None:
def dragEnterEvent(self, event):
extensions = [
f".{fmt.data().decode().lower()}"
for fmt in QtGui.QImageReader.supportedImageFormats()
]
if a0.mimeData().hasUrls():
items = [i.toLocalFile() for i in a0.mimeData().urls()]
if event.mimeData().hasUrls():
items = [i.toLocalFile() for i in event.mimeData().urls()]
if any([i.lower().endswith(tuple(extensions)) for i in items]):
a0.accept()
event.accept()
else:
a0.ignore()
event.ignore()

def dropEvent(self, a0: QtGui.QDropEvent) -> None:
def dropEvent(self, event):
if not self._can_continue():
a0.ignore()
event.ignore()
return
items = [i.toLocalFile() for i in a0.mimeData().urls()]
items = [i.toLocalFile() for i in event.mimeData().urls()]
self.importDroppedImageFiles(items)

# User Dialogs #
Expand Down
2 changes: 2 additions & 0 deletions labelme/config/default_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ canvas:
linestrip: false
ai_polygon: false
ai_mask: false
brush: false

shortcuts:
close: Ctrl+W
Expand All @@ -106,6 +107,7 @@ shortcuts:
create_line: null
create_point: null
create_linestrip: null
create_brush: null
edit_polygon: Ctrl+J
delete_polygon: Delete
duplicate_polygon: Ctrl+D
Expand Down
Binary file modified labelme/translate/zh_CN.qm
Binary file not shown.
Loading
Loading