def from_file(cls, file, **kwargs): """ 输入本地一个html文件的路径,加上导航栏打开 """ file = File(file) content = file.read() # 输入文件的情况,生成的_content等html要在同目录 return cls.from_content(content, os.path.splitext(str(file))[0], **kwargs)
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对应目录下
def zoomsvg(file, scale=1): """ 缩放svg文件 :param file: 如果输入一个目录,会处理目录下所有的svg图片 否则只处理指定的文件 如果是文本文件,则处理完文本后返回 :param scale: 缩放的比例,默认100%不调整 :return: """ if scale == 1: return def func(m): def g(m): return m.group(1) + str(float(m.group(2)) * scale) return re.sub(r'((?:height|width)=")(\d+(?:\.\d+)?)', g, m.group()) if os.path.isfile(file): s = re.sub(r'<svg .+?>', func, File(file).read(), flags=re.DOTALL) File(file).write(s, if_exists='replace') elif os.path.isdir(file): for f in os.listdir(file): if not f.endswith('.svg'): continue f = os.path.join(file, f) s = re.sub(r'<svg\s+.+?>', func, File(f).read(), flags=re.DOTALL) File(file).write(s, if_exists='replace') elif isinstance(file, str) and '<svg ' in file: # 输入svg的代码文本 return re.sub(r'<svg .+?>', func, file, flags=re.DOTALL)
def judge(f): if root: f = os.path.join(root, f) if type_ == 'file' and not os.path.isfile(f): return False elif type_ == 'dir' and not os.path.isdir(f): return False # 尽量避免调用 os.stat,判断是否有自定义大小、时间规则,没有可以跳过这部分 check_arg = first_nonnone([min_size, max_size, min_ctime, max_ctime, min_mtime, max_mtime]) if check_arg is not None: msg = os.stat(f) if first_nonnone([min_size, max_size]) is not None: size = File(f).size if min_size is not None and size < min_size: return False if max_size is not None and size > max_size: return False if min_ctime or max_ctime: file_ctime = datetime.fromtimestamp(msg.st_ctime) if min_ctime and file_ctime < min_ctime: return False if max_ctime and file_ctime > max_ctime: return False if min_mtime or max_mtime: file_mtime = datetime.fromtimestamp(msg.st_mtime) if min_mtime and file_mtime < min_mtime: return False if max_mtime and file_mtime > max_mtime: return False if ignore_special: parts = File(f).parts if '.git' in parts or '$RECYCLE.BIN' in parts: return False if ignore_backup and File(f).backup_time: return False return True
def debug_images(dir_, func, *, save=None, show=False): """ :param dir_: 选中的文件清单 :param func: 对每张图片执行的功能,函数应该只有一个图片路径参数 new_img = func(img) 当韩式有个参数时,可以用lambda函数技巧: lambda im: func(im, arg1=..., arg2=...) :param save: 如果输入一个目录,会将debug结果图存储到对应的目录里 :param show: 如果该参数为True,则每处理一张会imshow显示处理效果 此时弹出的窗口里,每按任意键则显示下一张,按ESC退出 :return: TODO 显示原图、处理后图的对比效果 TODO 支持同时显示多张图处理效果 """ if save: save = File(save) for f in dir_.subfiles(): im1 = xlcv.read(f) im2 = func(im1) if save: xlcv.write(im2, File(save / f.name, dir_)) if show: xlcv.imshow2(im2) key = cv2.waitKey() if key == '0x1B': # ESC 键 break
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 __init__(self, root, relpath2data=None, *, reads=True, prt=False, fltr='json', slt='json', extdata=None): """ :param root: 文件根目录 :param relpath2data: {jsonfile: lmdict, ...},其中 lmdict 为一个labelme文件格式的标准内容 如果未传入data具体值,则根据目录里的情况自动初始化获得data的值 210602周三16:26,为了工程等一些考虑,删除了 is_labelme_json_data 的检查 尽量通过 fltr、slt 的机制选出正确的 json 文件 """ super().__init__(root, relpath2data, reads=reads, prt=prt, fltr=fltr, slt=slt, extdata=extdata) # 已有的数据已经读取了,这里要补充空labelme标注 if self.pathgs: for stem, suffixs in tqdm(self.pathgs.data.items(), f'{self.__class__.__name__}优化数据', disable=not prt): f = File(stem, suffix=slt) if reads and not f: self.rp2data[f.relpath(self.root)] = LabelmeDict.gen_data( File(stem, suffix=suffixs[0]))
def magick(infile, *, outfile=None, if_exists='error', transparent=None, trim=False, density=None, other_args=None): """ 调用iamge magick的magick.exe工具 :param infile: 处理对象文件 :param outfile: 输出文件,可以不写,默认原地操作(只设置透明度、裁剪时可能会原地操作) :param if_exists: 如果目标文件已存在要怎么处理 :param transparent: True: 将白底设置为透明底 可以传入颜色参数,控制要设置的为透明底的背景色,默认为'white' 注意也可以设置rgb:'rgb(164,192,167)' :param trim: 裁剪掉四周空白 :param density: 设置图片的dpi值 :param other_args: 其他参数,输入格式如:['-quality', 100] :return: False:infile不存在或者不支持的文件扩展名 返回生成的文件名(outfile) """ # 1 条件判断,有些情况下不用处理 if not outfile: outfile = infile # 2 # 200914周一20:40,这有个相对路径的bug,修复了下,否则 test/a.png 会变成 test/test/a.png if File(outfile).exist_preprcs(if_exists): # 2.1 判断是否是支持的输入文件类型 ext = os.path.splitext(infile)[1].lower() if not File(infile) or not ext in ('.png', '.eps', '.pdf', '.jpg', '.jpeg', '.wmf', '.emf'): return False # 2.2 生成需要执行的参数 cmd = ['magick.exe'] # 透明、裁剪、density都是可以重复操作的 if density: cmd.extend(['-density', str(density)]) cmd.append(infile) if transparent: if not isinstance(transparent, str): transparent_ = 'white' else: transparent_ = transparent cmd.extend(['-transparent', transparent_]) if trim: cmd.append('-trim') if other_args: cmd.extend(other_args) cmd.append(outfile) # 2.3 生成目标png图片 cmd = [x.replace('\\', '/') for x in cmd] print(' '.join(cmd)) subprocess.run(cmd, stderr=subprocess.PIPE, shell=True) # 看不懂magick出错写的是啥,关了 return outfile
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 extract_files(src, dst, pattern, if_exists='replace'): """ 提取满足pattern模式的文件 """ d1, d2 = Dir(src), Dir(dst) files = d1.select(pattern).subs for f in files: p1, p2 = File(d1 / f), File(d2 / f) p1.copy(p2, if_exists=if_exists)
def browser_json(f): """ 可视化一个json文件结构 """ data = File(f).read() # 使用NestedDict.to_html_table转成html的嵌套表格代码,存储到临时文件夹 htmlfile = File(r'chrome_json.html', root=Dir.TEMP).write(NestedDict.to_html_table(data)) # 展示html文件内容 browser(htmlfile)
def browser(_self, opt='html'): if opt == 'html': data = _self.get_text('html') # html、xhtml 可以转网页,虽然排版相对来说还是会乱一点 data = ''.join(data) etag = get_etag(data) f = File(etag, Dir.TEMP, suffix='.html') f.write(data) browser(f) else: raise ValueError
def to_image(_self, outfile, *, scale=1, if_exists=None): """ 转成为文件 """ f = File(outfile) suffix = f.suffix.lower() if suffix == '.svg': content = _self.get_svg_image() f.write(content, if_exists=if_exists) else: im = _self.get_cv_image(scale) xlcv.write(im, if_exists=if_exists)
def viewfiles(procname, *files, **kwargs): """ 调用procname相关的文件程序打开files :param procname: 程序名 :param files: 一个文件名参数清单,每一个都是文件路径,或者是字符串等可以用writefile转成文件的路径 :param kwargs: save: 如果True,则会按时间保存文件名;否则采用特定名称,每次运行就会把上次的覆盖掉 wait: 是否等待当前进程结束后,再运行后续py代码 filename: 控制写入的文件名 TODO:根据不同软件,这里还可以扩展很多功能 :param kwargs: wait: True:在同一个进程中执行子程序,即会等待bc退出后,再进入下一步 False:在新的进程中执行子程序 细节:注意bc跟其他程序有比较大不同,建议使用专用的bcompare函数 目前已知可以扩展多文件的有:chrome、notepad++、texstudio >> ls = list(range(100)) >> viewfiles('notepad++', ls, save=True) """ # 1 生成文件名 ls = [] # 将最终所有绝对路径文件名存储到ls save = kwargs.get('save') basename = ext = None if 'filename' in kwargs and kwargs['filename']: basename, ext = os.path.splitext(kwargs['filename']) for i, t in enumerate(files): if File(t) or is_url(t): ls.append(str(t)) else: bn = basename or ... ls.append( File(bn, Dir.TEMP, suffix=ext).write(t, if_exists=kwargs.get( 'if_exists', 'error')).to_str()) # 2 调用程序(并计算外部操作时间) tictoc = TicToc() try: if kwargs.get('wait'): subprocess.run([procname, *ls]) else: subprocess.Popen([procname, *ls]) except FileNotFoundError: if procname in ('chrome', 'chrome.exe'): procname = 'explorer' # 如果是谷歌浏览器找不到,尝试用系统默认浏览器 viewfiles(procname, *files, **kwargs) else: raise FileNotFoundError(f'未找到程序:{procname}。请检查是否有安装及设置了环境变量。') return tictoc.tocvalue()
def __init__(self, imgpath): """ :param imgpath: 可选参数图片路径,强烈建议要输入,否则建立的label json会少掉图片宽高信息 """ self.imgpath = File(imgpath) # 读取图片数据,在一些转换规则比较复杂,有可能要用到原图数据 if self.imgpath: # 一般都只需要获得尺寸,用pil读取即可,速度更快,不需要读取图片rgb数据 self.img = xlpil.read(self.imgpath) else: self.img = None self.data = self.get_data_base() # 存储json的字典数据
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 __init__(self, alias=None, database=None, *, user='******', passwd='123456', host=None, port='3306', connect_timeout=None, account_file_path=None): """ 初始化需要连接数据库 :param alias: 数据库的简化别名,为了方便快速调用及隐藏明文密码 使用该参数将会覆盖掉已有的user、passwd、host、port参数值 例如我自己设置的别名有: ckz,我自己阿里云上的个人数据库 ckzlocal,本PC开的数据库 :param account_file_path: 使用alias时才有效 该参数指定存储账号信息的pkl文件所在位置,注意pkl的格式必须用类似下述的代码方式生成 默认从与该脚本同目录下的 sqllibaccount.pkl 文件获取 :param database: 数据库名称 例如在快乐做教研时一些相关数据库名: tr,教研 tr_develop,教研开发数据 tr_test,教研测试数据 :param connect_timeout: 连接超时时等待秒数 如果设置,建议2秒以上 :return: """ # 1 读取地址、账号信息 if alias: if account_file_path is None: account_file_path = File(SQL_LIB_ACCOUNT_FILE) # dprint(alias,account_file_path) record = File(account_file_path).read().loc[alias] # 从文件读取账号信息 user, passwd, host, port = record.user, record.passwd, record.host, record.port # 2 '数据库类型+数据库驱动名称://用户名:口令@机器地址:端口号/数据库名' address = f'mysql+mysqldb://{user}:{passwd}@{host}:{port}/{database}?charset=utf8mb4' # 3 存储成员 self.alias, self.database = alias, database connect_args = { "connect_timeout": connect_timeout } if connect_timeout else {} self.engine = sqlalchemy.create_engine(address, connect_args=connect_args)
def gen_data(cls, imfile=None, **kwargs): """ 主要框架结构 :param imfile: 可以传入一张图片路径 """ # 1 传入图片路径的初始化 if imfile: file = File(imfile) name = file.name img = xlpil.read(file) height, width = img.height, img.width else: name, height, width = '', 0, 0 # 2 字段值 data = { 'version': '4.5.7', 'flags': {}, 'shapes': [], 'imagePath': name, 'imageData': None, 'imageWidth': width, 'imageHeight': height, } if kwargs: data.update(kwargs) return data
def ensure_content(ob=None, encoding=None): """ :param ob: 未输入:从控制台获取文本 存在的文件名:读取文件的内容返回 tex、py、 docx、doc pdf 有read可调用成员方法:返回f.read() 其他字符串:返回原值 :param encoding: 强制指定编码 """ # TODO: 如果输入的是一个文件指针,也能调用f.read()返回所有内容 # TODO: 增加鲁棒性判断,如果输入的不是字符串类型也要有出错判断 if ob is None: return sys.stdin.read() # 注意输入是按 Ctrl + D 结束 elif File(ob): # 如果存在这样的文件,那就读取文件内容(bug点:如果输入是目录名会PermissionError) if ob.endswith('.docx'): # 这里还要再扩展pdf、doc文件的读取 # 安装详见: https://blog.csdn.net/code4101/article/details/79328636 check_install_package('textract') text = textract.process(ob) return text.decode('utf8', errors='ignore') elif ob.endswith('.doc'): raise NotImplementedError elif ob.endswith('.pdf'): raise NotImplementedError else: # 按照普通的文本文件读取内容 return readtext(ob, encoding) else: # 判断不了的情况,也认为是字符串 return ob
def file_or_dir_size(path): if os.path.isfile(path): return File(path).size elif os.path.isdir(path): return Dir(path).size else: return 0
def to_bcompare_files(cls, *args, files=None): """ 这个需要一次性获得所有的数据,才适合分析整体上要怎么获取对应的多个文件 :param files: 每个arg对应的文件名,默认按 'left'、'right', 'base' 来生成 也可以输入一个list[str],表示多个args依次对应的文件名 filename的长度可以跟args不一致,多的不用,少的自动生成 """ # 1 如果oldfile和newfile都是dict、set、list、tuple,则使用特殊方法文本化 # 如果两个都是list,不应该提取key后比较,所以限制第1个类型必须是dict或set,然后第二个类型可以适当放宽条件 # if not oldfile: oldfile = str(oldfile) if len(args) > 1 and isinstance(args[0], (dict, set)) and isinstance( args[1], (dict, set, list, tuple)): args = copy.copy(list(args)) t = [ prettifystr(li) for li in intersection_split(args[0], args[1]) ] args[0] = f'【共有部分】,{t[0]}\n\n【独有部分】,{t[1]}' args[1] = f'【共有部分】,{t[2]}\n\n【独有部分】,{t[3]}' # 2 参数对齐 if not isinstance(files, (list, tuple)): files = [files] if len(files) < len(args): files += [None] * (len(args) - len(files)) ref_names = ['left', 'right', 'base'] # 3 将每个参数转成一个文件 new_args = [] default_suffix = None for i, arg in enumerate(args): f = File.safe_init(arg) if f: # 是文件对象,且存在 new_args.append(f) if not default_suffix: default_suffix = f.suffix # elif isinstance(f, File): # 文本内容也可能生成合法的伪路径,既然找不到还是统一按字符串对比差异好 # # 是文件对象,但不存在 -> 报错 # raise FileNotFoundError(f'{f}') else: # 不是文件对象,要转存到文件 if not files[i]: # 没有设置文件名则生成一个 files[i] = File(ref_names[i], Dir.TEMP, suffix=default_suffix) files[i].write(arg) new_args.append(files[i]) return new_args
def browser(self, opt='pdf'): if opt == 'pdf': f = self.src_file browser(self.src_file) elif opt == 'html': ls = [] for i in range(self.page_count): page = self.load_page(i) ls.append(page.get_text('html')) data = '\n'.join(ls) etag = get_etag(data) f = File(etag, Dir.TEMP, suffix='.html') f.write(data) browser(f) else: raise ValueError(f'{opt}') return f
def write(self, dst=None, if_exists='replace'): """ :param dst: 往dst目标路径存入json文件,默认名称在self.imgpath同目录的同名json文件 :return: 写入后的文件路径 """ if dst is None and self.imgpath: dst = self.imgpath.with_suffix('.json') # 官方json支持indent=None的写法,但是ujson必须要显式写indent=0 return File(dst).write(self.data, if_exists=if_exists, indent=0)
def writefile(ob, path='', *, encoding='utf8', if_exists='backup', suffix=None, root=None, etag=None) -> str: """往文件path写入ob内容 :param ob: 写入的内容 如果要写txt文本文件且ob不是文本对象,只会进行简单的字符串化 :param path: 写入的文件名,使用空字符串时,会使用etag值 :param encoding: 强制写入的编码 :param if_exists: 如果文件已存在,要进行的操作 :param suffix: 文件扩展名 以'.'为开头,设置“候补扩展名”,即只在fn没有指明扩展名时,会采用 :param root: 相对位置 :return: 返回写入的文件名,这个主要是在写临时文件时有用 """ if etag is None: etag = (not path) if path == '': path = ... f = File(path, root, suffix=suffix).write(ob, encoding=encoding, if_exists=if_exists) if etag: f = f.rename(get_etag(str(f))) return str(f)
def gen_gt_dict(cls, images, annotations, categories, outfile=None): data = { 'images': images, 'annotations': annotations, 'categories': categories } if outfile is not None: File(outfile).write(data) return data
def filetext_replace(files, func, *, count=-1, start=1, bc=False, write=False, if_exists=None): r"""遍历目录下的文本文件进行批量处理的功能函数 :param files: 文件匹配规则,详见filesmatch用法 :param func: 通用文本处理函数 :param count: 匹配到count个文件后结束,防止满足条件的文件太多,程序会跑死 :param start: 从编号几的文件开始查找,一般在遇到意外调试的时候使用 :param bc: 使用beyond compare软件 注意bc的优先级比write高,如果bc和write同时为True,则会开bc,但并不会执行write :param write: 是否原地修改文件内容进行保存 :param if_exists: 是否进行备份,详见writefile里的参数文件 :return: 满足条件的文件清单 """ ls = [] total = 0 for f in filesmatch(files): # if 'A4-Exam' in f: # continue total += 1 if total < start: continue s0 = File(f).read() s1 = func(s0) if s0 != s1: match = len(ls) + 1 dprint(f, total, match) if bc: bcompare(f, s1) elif write: # 如果开了bc,程序是绝对不会自动写入的 File(f).write(s1, if_exists=if_exists) ls.append(f) if len(ls) == count: break match_num = len(ls) dprint(total, match_num) return ls
def read(file, flags=None, **kwargs): if xlpil.is_pil_image(file): im = file elif xlcv.is_cv2_image(file): im = xlcv.to_pil_image(file) elif File.safe_init(file): im = PIL.Image.open(str(file), **kwargs) else: raise TypeError(f'类型错误或文件不存在:{type(file)} {file}') return xlpil.cvt_channel(im, flags)
def setToC(self): """设置书签目录 可以调层级、改名称、修改指向页码 """ toc = self.doc.getToC() toc[1][1] = '改标题名称' self.doc.setToC(toc) file = File('a.pdf', Dir.TEMP).to_str() self.doc.save(file, garbage=4) browser(file)
def get_data(self, infile): lines = File(infile).read().splitlines() for line in lines: # 一般是要改这里,每行数据的解析规则 vals = line.split(',', maxsplit=8) if len(vals) < 9: continue pts = [int(v) for v in vals[:8]] # 点集 label = vals[-1] # 标注的文本 # get_shape还有shape_type形状参数可以设置 # 如果是2个点的矩形,或者3个点以上的多边形,会自动判断,不用指定shape_type self.add_shape(label, pts)
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)