def distribute_package(root, version=None, repository=None, *, upload=True): """ 发布包的工具函数 :param root: 项目的根目录,例如 'D:/slns/pyxllib' 根目录下有对应的 setup.py 等文件 :param repository: 比如我配置了 [xlpr],就可以传入 'xlpr' """ from pyxllib.file.specialist import File, Dir # 1 切换工作目录 os.chdir(str(root)) # 2 改版本号 if version: f = File('setup.py', root) s = re.sub(r"(version\s*=\s*)(['\"])(.+?)(\2)", fr'\1\g<2>{version}\4', f.read()) f.write(s) # 3 打包 subprocess.run('python setup.py sdist') # 4 上传 if upload: # 上传 cmd = 'twine upload dist/*' if repository: cmd += f' -r {repository}' subprocess.run(cmd) # 删除打包生成的中间文件 Dir('dist').delete() Dir('build').delete()
def to_images(self, dst_dir=None, file_fmt='{filestem}_{number}.png', num_width=None, *, scale=1, start=1, fmt_onepage=False): """ 将pdf转为若干页图片 :param dst_dir: 目标目录 默认情况下,只有一页pdf则存储到对应的pdf目录,多页则存储到同名子目录下 如果不想这样被智能控制,只要指定明确的dst即可 :param file_fmt: 后缀格式,包括修改导出的图片类型,注意要用 {} 占位符表示页码编号 :param num_width: 生成的每一页文件编号,使用的数字前导0域宽 默认根据pdf总页数来设置对应所用域宽 0表示不设域宽 :param scale: 对每页图片进行缩放,一般推荐都要设成2,导出的图片才清晰 :param start: 起始页码,一般建议从1开始比较符合常识直觉 :param fmt_onepage: 当pdf就只有一页的时候,是否还对导出的图片编号 默认只有一页的时候,进行优化,不增设后缀格式 :return: 返回转换完的图片名称清单 注:如果要导出单张图,可以用 FitzPdfPage.get_cv_image """ # 1 基本参数计算 srcfile, doc = self.src_file, self.doc filestem, n_page = srcfile.stem, doc.page_count # 自动推导目标目录 if dst_dir is None: dst_dir = Dir(srcfile.stem, srcfile.parent) if n_page > 1 else Dir( srcfile.parent) Dir(dst_dir).ensure_dir() # 域宽 num_width = num_width or get_number_width(n_page) # 根据总页数计算需要的对齐域宽 # 2 导出图片 if fmt_onepage or n_page != 1: # 多页的处理规则 res = [] for i in range(n_page): im = self.load_page(i).get_cv_image(scale) number = ('{:0' + str(num_width) + 'd}').format( i + start) # 前面的括号不要删,这样才是完整的一个字符串来使用format f = xlcv.write( im, File(file_fmt.format(filestem=filestem, number=number), dst_dir)) res.append(f) return res else: im = self.load_page(0).get_cv_image(scale) return [ xlcv.write( im, File(srcfile.stem + os.path.splitext(file_fmt)[1], dst_dir)) ]
def to_labelme_gt(self, imdir, dst_dir=None, *, segmentation=False, max_workers=4): """ 在图片目录里生成图片的可视化json配置文件 :param segmentation: 是否显示分割效果 """ def func(g): # 1 获得图片id和文件 image_id, df = g imfile = File(df.iloc[0]['file_name'], imdir) if not imfile: return # 如果没有图片不处理 # 2 生成这张图片对应的json标注 if dst_dir: imfile = imfile.copy(dst_dir, if_exists='skip') lm = Coco2Labelme(imfile) height, width = lm.img.size( ) # 也可以用image['height'], image['width']获取 # 注意df取出来的image_id默认是int64类型,要转成int,否则json会保存不了int64类型 lm.add_shape('', [0, 0, 10, 0], shape_type='line', shape_color=[0, 0, 0], n_gt_box=len(df), image_id=int(image_id), size=f'{height}x{width}') lm.anns_gt(df, segmentation=segmentation) lm.write() # 保存json文件到img对应目录下 if dst_dir: dst_dir = Dir(dst_dir) dst_dir.ensure_dir() gt_anns = self.gt_anns.copy() # 为了方便labelme操作,需要扩展几列内容 gt_anns['file_name'] = [ self.images.loc[x, 'file_name'] for x in gt_anns['image_id'] ] gt_anns['gt_category_name'] = [ self.categories.loc[x, 'name'] for x in gt_anns['gt_category_id'] ] gt_anns['gt_supercategory'] = [ self.categories.loc[x, 'supercategory'] for x in gt_anns['gt_category_id'] ] mtqdm(func, list(gt_anns.groupby('image_id').__iter__()), 'create labelme gt jsons', max_workers=max_workers)
def main_normal(cls, imdir, labeldir=None, label_file_suffix='.txt'): """ 封装更高层的接口,输入目录,直接标注目录下所有图片 :param imdir: 图片路径 :param labeldir: 标注数据路径,默认跟imdir同目录 :return: """ ims = Dir(imdir).select_files(['*.jpg', '*.png']) if not labeldir: labeldir = imdir txts = [File(f.stem, labeldir, suffix=label_file_suffix) for f in ims] cls.main_pair(ims, txts)
def __init__(self, root): """ data:dict key: 相对路径/图片stem (这一节称为loc) label名称:对应属性dict1 (跟loc拼在一起,称为loclabel) label2:dict2 """ self.root = Dir(root) self.data = defaultdict(dict) self.imfiles = {} for file in self.root.select_files('**/*.json'): lmdict = file.read() imfile = file.with_name(lmdict['imagePath']) img = xlcv.read(imfile) loc = file.relpath(self.root).replace('\\', '/')[:-5] self.imfiles[loc] = imfile for shape in lmdict['shapes']: attrs = self.parse_shape(shape, img) self.data[loc][attrs['label']] = attrs
def browser_jsons_kv(fd, files='**/*.json', encoding=None, max_items=10, max_value_length=100): """ demo_keyvaluescounter,查看目录下json数据的键值对信息 :param fd: 目录 :param files: 匹配的文件格式 :param encoding: 文件编码 :param max_items: 项目显示上限,有些数据项目太多了,要精简下 设为假值则不设上限 :param max_value_length: 添加的值,进行截断,防止有些值太长 :return: """ kvc = KeyValuesCounter() d = Dir(fd) for p in d.select_files(files): # print(p) data = p.read(encoding=encoding, mode='.json') kvc.add(data, max_value_length=max_value_length) p = File(r'demo_keyvaluescounter.html', Dir.TEMP) p.write(kvc.to_html_table(max_items=max_items), if_exists='replace') browser(p.to_str())
def gen_images(cls, imdir, start_idx=1): """ 自动生成标准的images字段 :param imdir: 图片目录 :param start_idx: 图片起始下标 :return: list[dict(id, file_name, width, height)] """ files = Dir(imdir).select_files(['*.jpg', '*.png']) images = [] for i, f in enumerate(files, start=start_idx): w, h = Image.open(str(f)).size images.append({ 'id': i, 'file_name': f.name, 'width': w, 'height': h }) return images
def find_pattern(self, pattern, files=None) -> pd.DataFrame: """ :param pattern: re.compile 对象 :param files: 要搜索的文件清单 191108周五10:40,目前跑 c:/pycode 要2分钟 >> browser(Git('C:/pycode/').find_pattern(re.compile(r'ssb等改明文'))) """ # 1 主对象 df = self.list_commits() all_shas = list(reversed(list(df['sha']))) # 2 files没有设置则匹配所有文件 # TODO 当前已经不存在的文件这样是找不到的,有办法把历史文件也挖出来? if not files: with Dir(self.g.working_dir): files = filesmatch('**/*.py') # 3 遍历文件 for file in files: d = {} for sha in self.commits_sha(file=file): try: cnt = len(pattern.findall(self.show(file, sha))) if cnt: d[sha] = cnt except git.exc.GitCommandError: pass if d: li = [] v = 0 for sha in all_shas: if sha in d: v = d[sha] li.append(v) df[file] = list(reversed(li)) return df
def __init__(self, root, relpath2data=None, *, reads=True, prt=False, fltr=None, slt=None, extdata=None): """ :param root: 数据所在根目录 :param dict[str, readed_data] relpath2data: {relpath: data1, 'a/1.txt': data2, ...} 如果未传入data具体值,则根据目录里的情况自动初始化获得data的值 relpath是对应的File标注文件的相对路径字符串 data1, data2 是读取的标注数据,根据不同情况,会存成不同格式 如果是json则直接保存json内存对象结构 如果是txt可能会进行一定的结构化解析存储 :param extdata: 可以存储一些扩展信息内容 :param fltr: filter的缩写,PathGroups 的过滤规则。一般用来进行图片匹配。 None,没有过滤规则,就算不存在slt格式的情况下,也会保留分组 'json'等字符串规则, 使用 select_group_which_hassuffix,必须含有特定后缀的分组 judge(k, v),自定义函数规则 :param slt: select的缩写,要选中的标注文件后缀格式 如果传入slt参数,该 Basic 基础类只会预设好 file 参数,数据部分会置 None,需要后续主动读取 >> BasicLabelData('textGroup/aabb', {'a.json': ..., 'a/1.json': ...}) >> BasicLabelData('textGroup/aabb', slt='json') >> BasicLabelData('textGroup/aabb', fltr='jpg', slt='json') # 只获取有对应jpg图片的json文件 >> BasicLabelData('textGroup/aabb', fltr='jpg|png', slt='json') """ # 1 基础操作 root = Dir(root) self.root, self.rp2data, self.extdata = root, relpath2data or {}, extdata or {} self.pathgs = None if relpath2data is not None or slt is None: return # 2 如果没有默认data数据,以及传入slt参数,则需要使用默认文件关联方式读取标注 relpath2data = {} gs = PathGroups.groupby(Dir(root).select_files('**/*')) if isinstance(fltr, str): gs = gs.select_group_which_hassuffix(fltr) elif callable(fltr): gs = gs.select_group(fltr) self.pathgs = gs # 3 读取数据 for stem, suffixs in tqdm(gs.data.items(), f'{self.__class__.__name__}读取数据', disable=not prt): f = File(stem, suffix=slt) if reads and f: # dprint(f) # 空json会报错:json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) relpath2data[f.relpath(self.root)] = f.read() else: relpath2data[f.relpath(self.root)] = None self.rp2data = relpath2data
def codefile(self): """ 多用途文件处理器 核心理念是不设定具体功能,而是让用户在 subinput 自己写需要执行的py代码功能 而在subinput,可以用一些特殊的标识符来传参 以file为例,有4类参数: file,处理当前目录 文件 rfile,递归处理所有目录下 文件 files,当前目录 所有文件 rfiles,递归获取所有目录下 所有文件 类型上,file可以改为 dir,只分析目录 path,所有文件和目录 扩展了一些特殊的类型: imfile,图片文件 """ from functools import reduce try: from xlproject.kzconfig import KzDataSync from pyxllib.data.labelme import reduce_labelme_jsonfile except ModuleNotFoundError: pass tt = TicToc() # 1 获得所有标识符 # 每个单词前后都有空格,方便定界 keywords = filter( lambda x: re.match(r'[a-zA-Z_]+$', x), set(re.findall(r'\.?[a-zA-Z_]+\(?', self.cmds['subinput']))) keywords = ' ' + ' '.join(keywords) + ' ' # print('keywords:', keywords) # 2 生成一些备用的智能参数 # 一级目录下选中的文件 paths = self.paths # 用正则判断一些智能参数是否要计算,注意不能只判断files,如果出现file,也是要引用files的 files = [File(p) for p in paths if p.is_file()] if re.search(r'files? ', keywords) else [] imfiles = [f for f in files if self.is_image(f)] if re.search( r' imfiles? ', keywords) else [] # 出现r系列的递归操作,都是要计算出dirs的 dirs = [Dir(p) for p in paths if p.is_dir()] if re.search( r' (dirs?|r[a-zA-Z_]+) ', keywords) else [] # 递归所有的文件 rpaths = reduce(lambda x, y: x + y.select('**/*').subpaths(), [paths] + dirs) \ if re.search(r' (r[a-zA-Z_]+) ', keywords) else [] rfiles = [File(p) for p in rpaths if p.is_file()] if re.search( r' (r(im)?files?) ', keywords) else [] rimfiles = [f for f in rfiles if self.is_image(f)] if re.search( r' rimfiles? ', keywords) else [] rdirs = [Dir(p) for p in rpaths if p.is_dir()] if re.search(r' (rdirs?) ', keywords) else [] # 3 判断是否要智能开循环处理 m = re.search(r' (r?(path|(im)?file|dir)) ', keywords) if m: name = m.group(1) objs = eval(name + 's') print('len(' + name + 's)=', len(objs)) for x in objs: locals()[name] = x eval(self.cmds['subinput']) else: # 没有的话就直接处理所有文件 eval(self.cmds['subinput']) # 4 运行结束标志 print(f'finished in {format_timespan(tt.tocvalue())}.')
class AutoGuiLabelData: """ AutoGuiLabelData """ def __init__(self, root): """ data:dict key: 相对路径/图片stem (这一节称为loc) label名称:对应属性dict1 (跟loc拼在一起,称为loclabel) label2:dict2 """ self.root = Dir(root) self.data = defaultdict(dict) self.imfiles = {} for file in self.root.select_files('**/*.json'): lmdict = file.read() imfile = file.with_name(lmdict['imagePath']) img = xlcv.read(imfile) loc = file.relpath(self.root).replace('\\', '/')[:-5] self.imfiles[loc] = imfile for shape in lmdict['shapes']: attrs = self.parse_shape(shape, img) self.data[loc][attrs['label']] = attrs @classmethod def parse_shape(cls, shape, image=None): """ 解析一个shape的数据为dict字典 """ # 1 解析原label为字典 attrs = DictTool.json_loads(shape['label'], 'label') attrs.update(DictTool.sub(shape, ['label'])) # 2 中心点 center shape_type = shape['shape_type'] pts = shape['points'] if shape_type in ('rectangle', 'polygon', 'line'): attrs['center'] = np.array(np.array(pts).mean(axis=0), dtype=int).tolist() elif shape_type == 'circle': attrs['center'] = pts[0] # 3 外接矩形 rect if shape_type in ('rectangle', 'polygon'): attrs['ltrb'] = np.array(pts, dtype=int).reshape(-1).tolist() elif shape_type == 'circle': x, y = pts[0] r = ((x - pts[1][0])**2 + (y - pts[1][1])**2)**0.5 attrs['ltrb'] = [ round_int(v) for v in [x - r, y - r, x + r, y + r] ] # 4 图片数据 img, etag if image is not None and attrs['ltrb']: attrs['img'] = xlcv.get_sub(image, attrs['ltrb']) # attrs['etag'] = get_etag(attrs['img']) # TODO 这里目的其实就是让相同图片对比尽量相似,所以可以用dhash而不是etag # 5 中心点像素值 pixel p = attrs['center'] if image is not None and p: attrs['pixel'] = tuple(image[p[1], p[0]].tolist()[::-1]) # if 'rect' in attrs: # del attrs['rect'] # 旧版的格式数据,删除 return attrs def update_loclabel_img(self, loclabel, img, *, if_exists='update'): """ 添加一张图片数据 :param if_exists: update,更新 skip,跳过,不更新 """ loc, label = os.path.split(loclabel) h, w = img.shape[:2] # 1 如果loc图片不存在,则新建一个jpg图片 update = True if loc not in self.data or not self.data[loc]: imfile = xlcv.write(img, File(loc, self.root, suffix='.jpg')) self.imfiles[loc] = imfile shape = LabelmeDict.gen_shape(label, [[0, 0], [w, h]]) self.data[loc][label] = self.parse_shape(shape) # 2 不存在的标签,则在最后一行新建一张图 elif label not in self.data[loc]: image = xlcv.read(self.imfiles[loc]) height, width = image.shape[:2] assert width == w # 先按行拼接,以后有时间可以扩展更灵活的拼接操作 # 拼接,并重新存储为图片 image = np.concatenate([image, img]) xlcv.write(image, self.imfiles[loc]) shape = LabelmeDict.gen_shape(label, [[0, height], [width, height + h]]) self.data[loc][label] = self.parse_shape(shape) # 3 已有的图,则进行替换 elif if_exists == 'update': image = xlcv.read(self.imfiles[loc]) [x1, y1, x2, y2] = self.data[loc][label]['ltrb'] image[y1:y2, x1:x2] = img xlcv.write(image, self.imfiles[loc]) else: update = False if update: # 需要实时保存到文件中 self.write(loc) def write(self, loc): f = File(loc, self.root, suffix='.json') imfile = self.imfiles[loc] lmdict = LabelmeDict.gen_data(imfile) for label, ann in self.data[loc].items(): a = ann.copy() DictTool.isub(a, ['img']) shape = LabelmeDict.gen_shape(json.dumps(a, ensure_ascii=False), a['points'], a['shape_type'], group_id=a['group_id'], flags=a['flags']) lmdict['shapes'].append(shape) f.write(lmdict, indent=2) def writes(self): for loc in self.data.keys(): self.write(loc) def __getitem__(self, loclabel): loc, label = os.path.split(loclabel) try: return self.data[loc][label] except KeyError: return None def __setitem__(self, loclabel, value): loc, label = os.path.split(loclabel) self.data[loc][label] = value
def check_repeat_filenames(dir, key='stem', link=True): """ 检查目录下文件结构情况的功能函数 https://www.yuque.com/xlpr/pyxllib/check_repeat_filenames :param dir: 目录Dir类型,也可以输入路径,如果没有files成员,则默认会获取所有子文件 :param key: 以什么作为行分组的key名称,基本上都是用'stem',偶尔可能用'name' 遇到要忽略 -eps-to-pdf.pdf 这种后缀的,也可以自定义处理规则 例如 key=lambda p: re.sub(r'-eps-to-pdf', '', p.stem).lower() :param link: 默认True会生成文件超链接 :return: 一个df表格,行按照key的规则分组,列默认按suffix扩展名分组 """ # 1 智能解析dir参数 if not isinstance(dir, Dir): dir = Dir(dir) if not dir.subs: dir = dir.select('**/*', type_='file') # 2 辅助函数,智能解析key参数 if isinstance(key, str): def extract_key(p): return getattr(p, key).lower() elif callable(key): extract_key = key else: raise TypeError # 3 制作df表格数据 columns = ['key', 'suffix', 'filename'] li = [] for f in dir.subs: p = File(f) li.append([extract_key(p), p.suffix.lower(), f]) df = pd.DataFrame.from_records(li, columns=columns) # 4 分组 def joinfile(files): if len(files): if link: return ', '.join([ f'<a href="{dir / f}" target="_blank">{f}</a>' for f in files ]) else: return ', '.join(files) else: return '' groups = df.groupby(['key', 'suffix']).agg({'filename': joinfile}) groups.reset_index(inplace=True) view_table = groups.pivot(index='key', columns='suffix', values='filename') view_table.fillna('', inplace=True) # 5 判断每个key的文件总数 count_df = df.groupby('key').agg({'filename': 'count'}) view_table = pd.concat([view_table, count_df], axis=1) view_table.rename({'filename': 'count'}, axis=1, inplace=True) browser(view_table, to_html_args={'escape': not link}) return df
def to_labelme_cls(self, root, *, bbox=True, seg=False, info=False): """ :param root: 图片根目录 :return: extdata,存储了一些匹配异常信息 """ root, data = Dir(root), {} catid2name = {x['id']: x['name'] for x in self.gt_dict['categories']} # 1 准备工作,构建文件名索引字典 gs = PathGroups.groupby(root.select_files('**/*')) # 2 遍历生成labelme数据 not_finds = set() # coco里有的图片,root里没有找到 multimatch = dict() # coco里的某张图片,在root找到多个匹配文件 for img, anns in tqdm(self.group_gt(reserve_empty=True), disable=not info): # 2.1 文件匹配 imfiles = gs.find_files(img['file_name']) if not imfiles: # 没有匹配图片的,不处理 not_finds.add(img['file_name']) continue elif len(imfiles) > 1: multimatch[img['file_name']] = imfiles imfile = imfiles[0] else: imfile = imfiles[0] # 2.2 数据内容转换 lmdict = LabelmeDict.gen_data(imfile) img = DictTool.or_(img, {'xltype': 'image'}) lmdict['shapes'].append( LabelmeDict.gen_shape(json.dumps(img, ensure_ascii=False), [[-10, 0], [-5, 0]])) for ann in anns: if bbox: ann = DictTool.or_( ann, {'category_name': catid2name[ann['category_id']]}) label = json.dumps(ann, ensure_ascii=False) shape = LabelmeDict.gen_shape(label, xywh2ltrb(ann['bbox'])) lmdict['shapes'].append(shape) if seg: # 把分割也显示出来(用灰色) for x in ann['segmentation']: an = { 'box_id': ann['id'], 'xltype': 'seg', 'shape_color': [191, 191, 191] } label = json.dumps(an, ensure_ascii=False) lmdict['shapes'].append(LabelmeDict.gen_shape( label, x)) f = imfile.with_suffix('.json') data[f.relpath(root)] = lmdict return LabelmeDataset(root, data, extdata={ 'categories': self.gt_dict['categories'], 'not_finds': not_finds, 'multimatch': Groups(multimatch) })
def _to_labelme_match(self, match_func_name, imdir, dst_dir=None, *, segmentation=False, hide_match_dt=False, **kwargs): """ 可视化目标检测效果 :param imdir: 默认会把结果存储到imdir :param dst_dir: 但如果写了dst_dir参数,则会有选择地从imdir筛选出图片到dst_dir """ def func(g): # 1 获得图片id和文件 image_id, df = g imfile = File(df.iloc[0]['file_name'], imdir) if not imfile: return # 如果没有图片不处理 image = self.images.loc[image_id] image = image.drop(['file_name', 'height', 'width']) # 2 生成这张图片对应的json标注 if dst_dir: imfile = imfile.copy(dst_dir, if_exists='skip') lm = Coco2Labelme(imfile) height, width = lm.data['imageHeight'], lm.data['imageWidth'] # 注意df取出来的image_id默认是int64类型,要转成int,否则json会保存不了int64类型 lm.add_shape('', [0, 0, 10, 0], shape_type='line', shape_color=[0, 0, 0], size=f'{height}x{width}', **(image.to_dict())) getattr(lm, match_func_name)(df, segmentation=segmentation, hide_match_dt=hide_match_dt, **kwargs) lm.write() # 保存json文件到img对应目录下 if dst_dir is not None: dst_dir = Dir(dst_dir) dst_dir.ensure_dir() match_anns = self.match_anns.copy() # 为了方便labelme操作,需要扩展几列内容 match_anns['file_name'] = [ self.images.loc[x, 'file_name'] for x in match_anns['image_id'] ] match_anns['gt_category_name'] = [ self.categories.loc[x, 'name'] for x in match_anns['gt_category_id'] ] match_anns['dt_category_name'] = [ self.categories.loc[x, 'name'] for x in match_anns['dt_category_id'] ] match_anns['gt_supercategory'] = [ self.categories.loc[x, 'supercategory'] for x in match_anns['gt_category_id'] ] mtqdm(func, list(iter(match_anns.groupby('image_id'))), max_workers=8, desc='make labelme json:')