yatengLG / ISAT_with_segment_anything

Labeling tool with SAM(segment anything model),supports SAM, SAM2, sam-hq, MobileSAM EdgeSAM etc.交互式半自动图像标注工具
https://www.yatenglg.cn/isat/
Other
1.29k stars 136 forks source link

New feature request: when modified IMG multiple point rectangle select and batch label rename #148

Closed StanleyYake closed 7 months ago

StanleyYake commented 7 months ago

Thanks for your annation tool, it helps me a lot. New feature request to make it better: 1.When modified IMG annotations, could you add multiple point rectangle select. It is very annoying to pick one by one. Screenshot from 2024-03-23 11-33-27

  1. batch label or group rename. I have use your tools long time ago, the group id will start from 0 and the new will start from 1 (except background 0), so I wonder whether there is any chance to change group or label name after finish annotation.
yatengLG commented 7 months ago
  1. Multiple points select is supported.Press the ctrl key to select points, then you can move or delect these.

gnome-shell-screenshot-M4UBL2

gnome-shell-screenshot-KG9LL2

  1. This function is not support, but you can change group and label anytime throung double click polygon or annotiaion list.
  2. You said you have been using ISAT for a long time. ISAT has updated many versions, the group id start from 0 in old version, if you want change your old annotation, i will provide a script for group add 1.
yatengLG commented 7 months ago

这个脚本实现了:"读取isat标注文件,组id+1后另存"的功能。


from typing import Dict, Tuple
from json import dump, load
import yaml
import imgviz
import tqdm
import os

class ISAT:
    """
    The ISAT (Image Segmentation Annotation Tool) format provides a structured approach for representing image annotations
    File Naming: Each image has a corresponding .json file named after the image file (without the image extension)
        ['info']: Contains metadata about the dataset and image
            ['description']: Always 'ISAT'
            ['folder']: The directory where the images are stored
            ['name']: The name of the image file
            ['width'], ['height'], ['depth']: The dimensions of the image; depth is assumed to be 3 for RGB images
            ['note']: An optional field for any additional notes related to the image
        ['objects']: Lists all the annotated objects in the image
            ['category']: The class label of the object.
            ['group']: An identifier that groups objects based on overlapping bounding boxes. If an object's bounding box is within another, they share the same group number.
            ['segmentation']: A list of [x, y] coordinates forming the polygon around the object
            ['area']: The area covered by the object in pixels
            ['layer']: A float indicating the sequence of the object. It increments within the same group, starting at 1.0
            ['bbox']: The bounding box coordinates in the format [x_min, y_min, x_max, y_max]
            ['iscrowd']: A boolean value indicating if the object is part of a crowd
            ['note']: An optional field for any additional notes related to the object
    """
    class ANNO:
        class INFO:
            description = ''
            folder = ''
            name = ''
            width = None
            height = None
            depth = None
            note = ''
        class OBJ:
            category = ''
            group = None
            segmentation = None
            area = None
            layer = None
            bbox = None
            iscrowd = None
            note = ''
        info:INFO
        objs:Tuple[OBJ] = ()

    annos:Dict[str, ANNO] = {}  # name, ANNO (the name without the suffix)
    cates:Tuple[str] = ()

    def read_from_ISAT(self, json_root):
        if os.path.exists(os.path.join(json_root, 'isat.yaml')):
            cates = []
            with open(os.path.join(json_root, 'isat.yaml'), 'rb')as f:
                cfg = yaml.load(f.read(), Loader=yaml.FullLoader)
            for label in cfg.get('label', []):
                cates.append(label.get('name'))
            self.cates = tuple(cates)

        pbar = tqdm.tqdm([file for file in os.listdir(json_root) if file.endswith('.json')])
        for file in pbar:
            pbar.set_description('Load ISAT from {}'.format(file))
            anno = self._load_one_isat_json(os.path.join(json_root, file))
            self.annos[self.remove_file_suffix(file)] = anno
        return True

    def save_to_ISAT(self, json_root):
        os.makedirs(json_root, exist_ok=True)

        pbar = tqdm.tqdm(self.annos.items())
        for name_without_suffix, Anno in pbar:
            json_name = name_without_suffix + '.json'
            pbar.set_description('Save ISAT to {}'.format(json_name))
            self._save_one_isat_json(Anno, os.path.join(json_root, json_name))

        # 类别文件
        cmap = imgviz.label_colormap()
        self.cates = sorted(self.cates)
        categories = []
        for index, cat in enumerate(self.cates):
            r, g, b = cmap[index + 1]
            categories.append({
                'name': cat if isinstance(cat, str) else str(cat),
                'color': "#{:02x}{:02x}{:02x}".format(r, g, b)
            })
        s = yaml.dump({'label': categories})
        with open(os.path.join(json_root, 'isat.yaml'), 'w') as f:
            f.write(s)

        return True

    def remove_file_suffix(self, file_name):
        return os.path.splitext(file_name)[0]

    def _load_one_isat_json(self, json_path) -> ANNO:
        anno = self.ANNO()
        with open(json_path, 'r') as f:
            dataset = load(f)
            info = dataset.get('info', {})
            description = info.get('description', '')
            if description != 'ISAT':
                raise AttributeError('The json file {} is`t a ISAT json.'.format(json_path))
            folder = info.get('folder', '')
            img_name = info.get('name', '')
            width = info.get('width', None)
            height = info.get('height', None)
            depth = info.get('depth', None)
            note = info.get('note', '')

            anno.info = self.ANNO.INFO()
            anno.info.description = description
            anno.info.folder = folder
            anno.info.name = img_name
            anno.info.width = width
            anno.info.height = height
            anno.info.depth = depth
            anno.info.note = note

            objs = []
            objects = dataset.get('objects', [])
            for obj in objects:
                category = obj.get('category', 'UNKNOW')
                group = obj.get('group', 0)
                if group is None: group = 0
                segmentation = obj.get('segmentation', [])
                iscrowd = obj.get('iscrowd', 0)
                note = obj.get('note', '')
                area = obj.get('area', 0)
                layer = obj.get('layer', 2)
                bbox = obj.get('bbox', [])

                obj = self.ANNO.OBJ()
                obj.category = category
                obj.group = group
                obj.segmentation = segmentation
                obj.area = area
                obj.layer = layer
                obj.bbox = bbox
                obj.iscrowd = iscrowd
                obj.note = note
                objs.append(obj)

            anno.objs = tuple(objs)
        return anno

    def _save_one_isat_json(self, anno:ANNO, save_path):
        anno.info.description = 'ISAT'
        dataset = {}
        dataset['info'] = {}
        dataset['info']['description'] = anno.info.description
        dataset['info']['folder'] = anno.info.folder
        dataset['info']['name'] = anno.info.name
        dataset['info']['width'] = anno.info.width
        dataset['info']['height'] = anno.info.height
        dataset['info']['depth'] = anno.info.depth
        dataset['info']['note'] = anno.info.note
        dataset['objects'] = []
        for obj in anno.objs:
            object = {}
            object['category'] = obj.category if isinstance(obj.category, str) else str(obj.category)
            object['group'] = obj.group
            object['segmentation'] = obj.segmentation
            object['area'] = obj.area
            object['layer'] = obj.layer
            object['bbox'] = obj.bbox
            object['iscrowd'] = obj.iscrowd
            object['note'] = obj.note
            dataset['objects'].append(object)

        with open(save_path, 'w') as f:
            dump(dataset, f, indent=4)
        return True

if __name__ == '__main__':

    OLD_ANNOTATION_FILE_SAVE_ROOT = ''
    NEW_ANNOTATION_FILE_SAVE_ROOT = ''

    assert OLD_ANNOTATION_FILE_SAVE_ROOT != NEW_ANNOTATION_FILE_SAVE_ROOT

    isat = ISAT()
    isat.read_from_ISAT(OLD_ANNOTATION_FILE_SAVE_ROOT)

    for anno_key in isat.annos:
        for index in range(len(isat.annos[anno_key].objs)):
            print(isat.annos[anno_key].objs[index].group)
            isat.annos[anno_key].objs[index].group += 1

            print(isat.annos[anno_key].objs[index].group)

    isat.save_to_ISAT(NEW_ANNOTATION_FILE_SAVE_ROOT)```
StanleyYake commented 7 months ago

Thanks for your kindly and quick reply.

  1. The Ctrl is not the best way, in the above picture I uploaded, I have less than 100 points on the edge, if there is 1000 points, the user will be press Ctrl and click 1000 on there mouse. So I suggest to add a rectangle (or others) to quick select 1000 points at once.
  2. I have tested your code. The group string maybe equal "", so I changed to below:
    for anno_key in isat.annos:
        for index in range(len(isat.annos[anno_key].objs)):
            print(isat.annos[anno_key].objs[index].group)
            if isat.annos[anno_key].objs[index].group is None or isat.annos[anno_key].objs[index].group == "":
                isat.annos[anno_key].objs[index].group = 1
            else:
                isat.annos[anno_key].objs[index].group = int(isat.annos[anno_key].objs[index].group) + 1
            print(isat.annos[anno_key].objs[index].group)