ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Gradio ImageEditor 裁剪工具(CropTool)实现解析:基于 PIXI.js 的交互式裁剪机制与源码级详解

Gradio ImageEditor 裁剪工具(CropTool)实现解析:基于 PIXI.js 的交互式裁剪机制与源码级详解 Gradio ImageEditor 裁剪工具CropTool实现解析基于 PIXI.js 的交互式裁剪机制与源码级详解【免费下载链接】gradioBuild and share delightful machine learning apps, all in Python. Star to support our work!项目地址: https://gitcode.com/GitHub_Trending/gr/gradio导读gr.ImageEditor是 Gradio 提供的一个功能完整的图像编辑组件支持上传、绘制、擦除、图层与裁剪等操作其中“裁剪Crop”是其最常用的变换能力之一。本文以仓库内 js/imageeditor/shared/crop/CROP.md 为骨架结合 crop.ts 的完整实现深入解析CropTool类的状态管理、交互事件、坐标换算、PIXI.js 遮罩技巧与裁剪产出流程并打通前端 PIXI 编辑器与后端gr.ImageEditorPython 组件的调用链。读完本文你将理解 Gradio 图片编辑器裁剪功能从“框选”到“产出裁剪图”的完整实现原理掌握其可复用的架构设计思路。1. 裁剪工具在 Gradio ImageEditor 中的定位1.1 功能概述裁剪工具crop tool是图片编辑器的一个子组件允许用户框选图像的一部分并丢弃其余部分。它提供了带可拖拽控制柄handle的交互式 UI四角有 L 形拐角控制柄、四边中间有横条状边缘控制柄用户可通过拖拽调整裁剪区域也可拖动裁剪框内部整体移动选区。被选区域之外的图像被半透明遮罩覆盖形成清晰的视觉反馈。1.2 文件结构文件作用js/imageeditor/shared/crop/crop.tsCropTool裁剪工具的完整实现js/imageeditor/shared/core/editor.ts定义Tool接口与ImageEditorContext上下文js/imageeditor/shared/Toolbar.svelte定义工具类型Tool与子工具类型Subtool从源码结构看crop.ts中的CropTool实现了editor.ts中定义的Tool接口并通过ImageEditorContext与编辑器核心交互Toolbar.svelte则负责在工具栏中渲染裁剪入口按钮。1.3 工具与子工具的类型定义Toolbar.svelte 的模块级脚本定义了Tool与Subtool类型export type Tool image | draw | erase | pan; export type Subtool | upload | paste | webcam | color | size | crop | remove_background | null;其中裁剪对应tool image且subtool crop的组合。CropTool内部也据此声明了自己的身份name crop as const; current_tool: ToolbarTool image; current_subtool: Subtool crop;裁剪按钮的显示受transforms参数控制。在 Toolbar.svelte 中仅当transforms.includes(crop)时才会渲染裁剪图标按钮let can_crop $derived(transforms.includes(crop)); ... {#if can_crop} IconButton Icon{Crop} label{i18n(image_editor.crop)} onclick{(e) handle_subtool_click(e, crop)} highlight{subtool crop} ... / {/if}对应到 Python 端gradio/components/image_editor.py 中gr.ImageEditor的transforms参数默认值为(crop, resize)注释明确说明crop允许用户裁剪图像transforms: Iterable[Literal[crop, resize]] | None (crop, resize),该默认值由后端配置直接传递到前端 Svelte 组件见 ImageEditor.svelte 中transforms [crop, resize]并由测试 test/components/test_image_editor.py 验证def test_brush_false_eraser_false_config(self): ... transforms(crop,), ... assert config[transforms] [crop]2. CropTool 类状态、常量与公共接口2.1 类声明与常量CropTool实现Tool接口editor.tsexport interface Tool { name: string; setup(context: ImageEditorContext, tool: ToolbarTool, subtool: Subtool): Promisevoid; cleanup(): void; set_tool(tool: ToolbarTool, subtool: Subtool): void; on?: (event: string, callback: () void) void; off?: (event: string, callback: () void) void; }crop.ts 中定义了若干绘制常量直接决定控制柄的视觉尺寸与命中区域// Constants private readonly CORNER_SIZE 25; // 拐角控制柄边长 private readonly LINE_THICKNESS 5; // 控制柄线条粗细 private readonly HANDLE_COLOR 0x000000; // 控制柄颜色黑色 private readonly HIT_AREA_SIZE 40; // 基础命中区域尺寸边缘控制柄放大 1.5 倍2.2 状态变量裁剪工具维护以下状态crop.ts状态变量类型说明crop_boundsCropBounds裁剪区域含x、y、width、heightis_draggingboolean是否正在拖拽某个控制柄is_dragging_windowboolean是否正在拖拽整个裁剪窗口selected_handleContainer \| null当前选中的控制柄引用active_corner_indexnumber当前活动的拐角索引-1 表示无active_edge_indexnumber当前活动的边缘索引-1 表示无last_pointer_positionPoint \| null上一次指针位置图像局部坐标drag_start_positionPoint \| null窗口拖拽起始指针位置drag_start_boundsCropBounds \| null窗口拖拽起始的裁剪边界快照has_been_manually_changedboolean用户是否手动修改过裁剪区域CropBounds接口定义如下crop.tsinterface CropBounds { x: number; y: number; width: number; height: number; }2.3 公共能力事件订阅与结果输出CropTool通过on/off/notify实现轻量级事件机制并在拖拽结束时广播change事件crop.ts。ImageEditor在构造时会把所有工具注册的change回调桥接到自身的notify(change)见 editor.ts从而让外部监听者感知到裁剪动作。对外暴露两个读取方法get_crop_bounds()返回{ x, y, crop_dimensions, image_dimensions }其中裁剪与图像尺寸都来自图像局部坐标crop.tsget_image()基于当前crop_bounds从背景图像纹理生成Blobcrop.ts内部通过utils/pixi.ts的get_canvas_blob完成像素级导出。3. 生命周期setup 与 cleanup3.1 setup订阅上下文、初始化 UI、挂载事件setup(context, tool, subtool)是Tool接口的核心方法crop.ts完成四件事保存上下文与当前工具状态记录image_editor_context、current_tool、current_subtool。订阅三个 Svelte 可读 storedimensions.subscribe图像尺寸变化时将crop_bounds重置为整幅图像{ x: 0, y: 0, width, height }并同步刷新 UI 与遮罩position.subscribe记录图像位置屏幕坐标scale.subscribe记录图像缩放比例。注册背景图像监听器通过app.ticker.add(background_image_watcher)把回调挂到 PIXI.js 的 ticker 上每个渲染帧都会检查背景图像是否变化若裁剪工具处于激活状态且有遮罩则对最新背景图像重新应用遮罩确保“图层更换后遮罩依然有效”。初始化 UI 并注册事件调用init_crop_ui()创建 UI 元素调用setup_event_listeners()在 stage 上挂载pointermove/pointerup/pointerupoutside最后依据当前工具状态设置裁剪 UI 容器的可见性。值得注意的是setup中订阅dimensions的匿名回调会捕获当前上下文因此cleanup必须显式移除 ticker 监听与 stage 事件否则会产生泄漏。3.2 cleanup逆序释放全部资源cleanup()crop.ts按以下顺序清理从app.stage移除crop_ui_container移除crop_mask并从background_image、image_container上解除mask引用仅当 mask 与当前 mask 相等时置空crop_mask与crop_ui_container引用从 ticker 移除background_image_watcher调用cleanup_event_listeners()解绑 stage 上的pointermove/pointerup/pointerupoutside。3.3 set_tool按需显隐 UIset_tool(tool, subtool)crop.ts判断tool image subtool crop激活时显示crop_ui_container并刷新遮罩非激活时仅隐藏 UI不触碰遮罩状态。ImageEditor.set_tool/set_subtooleditor.ts会遍历所有已注册工具并调用各自的set_tool从而实现工具切换的全局广播。4. 视觉元素裁剪 UI 的构建过程4.1 make_crop_ui容器、轮廓与命中区域make_crop_ui(width, height)crop.ts创建裁剪 UI 的根容器crop_containereventMode static、interactiveChildren true允许子元素接收指针事件创建黑色半透明描边矩形crop_outlinestroke({ width: 1, color: 0x000000, alpha: 0.5 })作为裁剪框轮廓设置cursor move并给hitArea赋与轮廓同尺寸的Rectangle使整个裁剪框可被点击拖动在crop_outline上监听pointerdown→handle_window_drag_start依次添加 4 个拐角控制柄create_corner_handles与 4 个边缘控制柄create_edge_handles。4.2 create_handle拐角与边缘两种控制柄create_handle(is_edge)crop.ts根据是否边缘控制柄绘制不同图形if (is_edge) { // 边缘控制柄短横条1.5 倍边长 x 线条粗细 handle_graphics .rect(0, 0, this.CORNER_SIZE * 1.5, this.LINE_THICKNESS) .fill(this.HANDLE_COLOR); } else { // 拐角控制柄L 形 handle_graphics .rect(0, 0, this.CORNER_SIZE, this.LINE_THICKNESS) .rect(0, 0, this.LINE_THICKNESS, this.CORNER_SIZE) .fill(this.HANDLE_COLOR); }命中区域hitArea按类型放大以提升拖拽体验const hit_size is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE; handle.hitArea new Rectangle( -hit_size / 2 this.LINE_THICKNESS, -hit_size / 2 this.LINE_THICKNESS, hit_size, hit_size );4.3 拐角与边缘控制柄的排布create_corner_handlescrop.ts定义四个拐角的位置、镜像缩放与光标样式索引位置缩放光标拖拽语义0左上(0, 0)(1, 1)nwse-resize同时调整左、上两侧1右上(width, 0)(-1, 1)nesw-resize调整右、上两侧2左下(0, height)(1, -1)nesw-resize调整左、下两侧3右下(width, height)(-1, -1)nwse-resize调整右、下两侧拐角位置通过corner.scale.set(xScale, yScale)镜像绘制并通过handle_pointer_down(event, corner, i, -1)注册指针事件其中第三、四参数分别表示拐角索引与边缘索引-1 表示“非该类型”。create_edge_handlescrop.ts创建四条边中间的控制柄上、下边缘rotation 0位于(width/2 - ..., -LINE_THICKNESS)与(width/2 - ..., height)光标ns-resize索引 0/1左、右边缘rotation Math.PI / 2旋转 90° 变成竖条位于(-LINE_THICKNESS, height/2 - ...)与(width, height/2 - ...)光标ew-resize索引 2/3。事件绑定同样通过handle_pointer_down(event, handle, -1, index)索引语义为0上、1下、2左、3右。5. 状态管理从指针事件到裁剪边界5.1 指针事件总览裁剪工具在setup_event_listenerscrop.ts中把监听器挂在stage上stage.on(pointermove, this.handle_pointer_move.bind(this)); stage.on(pointerup, this.handle_pointer_up.bind(this)); stage.on(pointerupoutside, this.handle_pointer_up.bind(this));而pointerdown则分散注册在各控制柄与裁剪框轮廓上。这样设计的优势是指针按下后即使拖出控制柄范围pointermove仍能持续跟踪pointerupoutside也能保证拖拽状态被正确复位。5.2 handle_pointer_down开始拖拽handle_pointer_down(event, handle, corner_index, edge_index)crop.ts若当前子工具不是crop直接返回event.stopPropagation()防止事件冒泡到其他层置is_dragging true、记录selected_handle、active_corner_index、active_edge_index关键一步把event.global全局坐标通过image_container.toLocal()转成图像容器局部坐标保存为last_pointer_position。这个坐标转换贯穿整个拖拽流程是保证裁剪边界与图像内容对齐的基础。5.3 handle_pointer_move增量更新裁剪边界handle_pointer_move(event)crop.ts分两条路径路径 A窗口拖拽is_dragging_window为真。计算当前局部坐标与drag_start_position的增量delta将裁剪边界整体平移this.crop_bounds { x: this.drag_start_bounds.x delta.x, y: this.drag_start_bounds.y delta.y, width: this.drag_start_bounds.width, height: this.drag_start_bounds.height };随后constrain_crop_bounds()、update_crop_mask()并把遮罩重新应用到背景图像。路径 B控制柄拖拽is_dragging为真。同样先转局部坐标计算相对last_pointer_position的增量然后调用update_crop_bounds(delta)更新边界并刷新last_pointer_position与遮罩。5.4 update_crop_bounds四种拐角与四种边缘的增量语义update_crop_bounds(delta)crop.ts首先置has_been_manually_changed true保存原始边界快照然后按active_corner_index/active_edge_index分派。拐角corner增量逻辑索引位置逻辑0左上宽度与高度减小、x/y随之增大1右上宽度增加、高度减小、y增大2左下宽度减小、x增大、高度增加3右下宽度与高度均增加边缘edge增量逻辑索引位置逻辑0上高度减小、y增大1下高度增加2左宽度减小、x增大3右宽度增加所有宽度/高度计算都包裹Math.max(20, ...)即最小裁剪尺寸为 20×20 像素。更新结束后统一调用constrain_crop_bounds()、update_crop_ui()、update_crop_mask()并确保遮罩应用到当前背景图像。5.5 constrain_crop_bounds越界约束与“对边补偿”constrain_crop_bounds()crop.ts是保证裁剪区域始终落在图像内的关键方法分四步宽度约束Math.max(20, Math.min(width, dimensions.width))高度约束同上处理heightx 约束Math.max(0, Math.min(x, dimensions.width - width))若左边界被约束左上角或左边缘被拖动则通过crop_bounds.width - xDiff补偿宽度防止对边“漂移”y 约束Math.max(0, Math.min(y, dimensions.height - height))若上边界被约束上边缘或左/右上的拐角被拖动同样对高度做补偿。最后再做一次“当前位置下的最大尺寸”钳制确保x width ≤ dimensions.width、y height ≤ dimensions.height恒成立。5.6 handle_window_drag_start 与 handle_pointer_uphandle_window_drag_start(event)crop.ts记录拖拽起始指针局部坐标与裁剪边界快照并置has_been_manually_changed true。handle_pointer_up()crop.ts复位全部拖拽状态is_dragging、is_dragging_window、selected_handle、last_pointer_position、drag_start_position、drag_start_bounds、active_corner_index、active_edge_index并广播notify(change)告知外部“裁剪区域已变化”。6. 遮罩与缩放两个核心技术点6.1 遮罩技术Masking Techniqueset_crop_mask()crop.ts与update_crop_mask()crop.ts共同实现遮罩效果set_crop_mask先移除旧的crop_mask并解除其对image_container/background_image的引用再新建Graphics实例并加入image_container的显示列表update_crop_mask清除旧图形后先绘制整幅图像范围再在裁剪区域上“挖洞”this.crop_mask .rect(0, 0, width, height) .fill({ color: 0x000000, alpha: 0.4 }) // 半透明黑色遮罩 .rect( this.crop_bounds.x, this.crop_bounds.y, this.crop_bounds.width, this.crop_bounds.height ) .cut(); // 裁剪区域“挖空”这里的关键点在于遮罩对象 alpha 使用 0.4 而非 0。CROP.md 提到早期实现使用alpha0的 mask 让遮罩不可见但仍作为 PIXI.js mask 起作用以避免白色背景出现在遮罩区域当前实现则更进一步直接以半透明黑色 overlay 的方式呈现视觉上更接近主流图片编辑器的“暗化非选区”效果。此外update_crop_mask中还会把background_image.mask置为null明确当前不再是“用 mask 裁剪纹理”而是“overlay 叠加遮罩”。background_image_watcher挂载在 ticker 上保证无论背景图层何时被替换裁剪遮罩都能跟随最新背景重新应用crop.ts。6.2 缩放考量Scaling Considerations裁剪工具在三套坐标系之间穿梭图像坐标unscaledcrop_bounds始终以原始图像像素为单位存储屏幕坐标scaledposition图像在舞台上的偏移与scale缩放比例共同决定图像在屏幕上的实际位置全局指针坐标PIXI 事件携带的event.global。换算规则在update_crop_ui()crop.ts中集中体现// 裁剪 UI 容器锚定在“图像位置 裁剪偏移 × 缩放” this.crop_ui_container.position.set( this.position.x this.crop_bounds.x * this.scale, this.position.y this.crop_bounds.y * this.scale ); // 轮廓尺寸 裁剪尺寸 × 缩放 const scaled_width this.crop_bounds.width * this.scale; const scaled_height this.crop_bounds.height * this.scale;而指针坐标的换算在拖拽入口处通过image_container.toLocal(event.global)一次性完成因此后续delta计算天然处于图像局部坐标中无需再除以缩放——这与 CROP.md 的“Scaling Considerations”一节的描述完全一致。init_crop_uicrop.ts还展示了初始化顺序的细节以dimensions × scale创建初始 UI以position定位容器先隐藏容器随后创建遮罩并把update_crop_ui绑定到 ticker 持续驱动 UI 刷新。6.3 控制柄位置更新update_handle_positions(width, height)crop.ts在每个渲染周期按当前缩放后的裁剪尺寸重新排布 4 个拐角与 4 条边缘控制柄确保控制柄始终贴住裁剪框边缘这也是裁剪框拖动/缩放时控制柄“跟手”的原因。7. 与编辑器的集成ImageEditorContext 与双编辑器架构7.1 ImageEditorContext 接口CropTool通过 editor.ts 定义的ImageEditorContext与编辑器交互CROP.md 列出的核心成员成员类型用途appApplicationPIXI.js 应用实例渲染器、stage、tickerimage_containerContainer承载图像的容器dimensionsReadable{width, height}图像尺寸可读 storepositionReadable{x, y}图像位置可读 storescaleReadablenumber图像缩放可读 store此外上下文还提供background_image当前背景 Sprite、command_manager、layer_manager、set_image_properties、set_background_image等能力。ImageEditor的get context()editor.ts以闭包形式把这些成员装配成上下文对象。7.2 双编辑器架构主编辑器与裁剪编辑器从 ImageEditor.svelte 可以看到一个非常独特的架构组件内部同时创建了两个 PIXI 编辑器实例editor new ImageEditor({ target_element: pixi_target, width: canvas_size[0], height: canvas_size[1], tools: [image, zoom, new ResizeTool(), brush], ... }); crop new ImageEditor({ target_element: pixi_target_crop, width: canvas_size[0], height: canvas_size[1], tools: [image, crop_zoom, new CropTool()], dark: true, fixed_canvas: false, border_region: 0, pad_bottom: 40 });主编辑器editor承载画笔、擦除、图层、缩放、尺寸调整等完整编辑功能裁剪编辑器crop仅注册[image, crop_zoom, new CropTool()]专门负责裁剪交互。两个编辑器的 DOM 容器通过current_subtool crop切换可见性ImageEditor.svelte裁剪模式还附带dark-bg暗色背景与确认按钮。上传/粘贴图片时图片会同时喂给两个编辑器await editor.add_image({ image: _file }); await crop.add_image({ image: _file }); crop.reset();进入裁剪模式时自动让裁剪编辑器适配缩放ImageEditor.svelte$effect(() { if (current_tool image current_subtool crop) { crop_zoom.set_zoom(fit); } });7.3 裁剪确认从 CropTool 到主编辑器的数据流点击绿色对勾确认按钮触发handle_crop_confirmImageEditor.svelteasync function handle_crop_confirm(): Promisevoid { const { image } await crop.get_crop_bounds(); if (!image) return; await editor.add_image({ image, resize: false }); handle_subtool_change({ tool: image, subtool: null }); onchange?.(); oninput?.(); }这里的get_crop_bounds()实为ImageEditor.get_crop_bounds()editor.ts它内部取出CropTool先调用crop_tool.get_crop_bounds()拿到裁剪与图像尺寸再调用crop_tool.get_image()得到裁剪后的Blobasync get_crop_bounds(): Promise{...} { const crop_tool this.tools.get(crop) as CropTool; const crop_bounds crop_tool.get_crop_bounds(); const image await crop_tool.get_image(); return { image, ...crop_bounds }; }CropTool.get_image()crop.ts把背景纹理包进临时ContainerSprite再调用 utils/pixi.ts 的get_canvas_blobreturn get_canvas_blob( this.image_editor_context.app.renderer, container, this.crop_bounds // 作为 frame 传入实现像素级裁剪导出 );get_canvas_blob通过renderer.extract.canvas({ target, resolution: 1, frame })提取指定矩形区域再toBlob转成 Blob。裁剪后的图片随后以resize: false方式作为新背景喂回主编辑器触发change与input事件最终由 Python 端gr.ImageEditor的preprocessgradio/components/image_editor.py把background/layers/composite转成 numpy / PIL / filepath 形式交给预测函数。7.4 从渲染到导出的完整调用链综合以上源码裁剪功能的完整调用链可归纳为Toolbar 点击裁剪按钮 → Toolbar.svelte handle_subtool_click({tool:image, subtool:crop}) → ImageEditor.svelte handle_subtool_change → editor.set_subtool(crop) / crop.set_subtool(crop) → CropTool.set_tool 显示裁剪 UI → 用户拖拽控制柄pointerdown / pointermove / pointerup on stage → update_crop_bounds → constrain_crop_bounds → update_crop_ui / update_crop_mask → 点击确认按钮 → handle_crop_confirm → crop.get_crop_bounds() → CropTool.get_crop_bounds() CropTool.get_image() → get_canvas_blob(renderer, container, crop_bounds) 导出裁剪 Blob → editor.add_image({image: blob, resize: false}) → onchange / oninput → Python 端 preprocess 输出 EditorValue8. 维护注意事项与约束汇总CROP.md 的 Maintenance Notes 在源码中均有对应落点整理如下事件处理控制柄与轮廓的pointerdown均调用event.stopPropagation()pointerup与pointerupoutside都绑定handle_pointer_up避免拖拽状态悬挂见 crop.ts 与 crop.ts。坐标系拖拽起点统一用image_container.toLocal(event.global)转换边界增量始终基于图像局部坐标crop.ts。缩放处理crop_bounds以未缩放图像坐标存储UI 定位时乘以this.scale指针转换时不再额外除以缩放crop.ts。性能ticker 上绑定的update_crop_ui只做位置与图形重绘update_crop_mask在拖拽期间按需调用避免无谓的重绘。边界情况最小裁剪尺寸 20×20、越界钳制、对边补偿、空背景图像background_image不存在时安全返回等都在constrain_crop_bounds与get_image中处理。约束条件总结约束实现位置最小裁剪尺寸 20×20 像素update_crop_bounds中Math.max(20, ...)裁剪区域不超出图像边界constrain_crop_bounds的四步钳制控制柄不可拖出有效范围对边补偿 末尾二次钳制非裁剪子工具下忽略交互handle_pointer_down/handle_pointer_move开头判断current_subtool ! crop即返回9. 从 Python 端配置裁剪能力虽然裁剪交互完全发生在前端但其可用性由 Python 端gr.ImageEditor的参数决定。核心配置如下gradio/components/image_editor.pygr.ImageEditor( transforms(crop, resize), # crop 启用裁剪 canvas_size(800, 800), # 画布初始尺寸 fixed_canvasFalse, # False 时画布随图片尺寸变化 sources(upload, webcam, clipboard), ... )transforms控制裁剪/缩放按钮是否出现传(crop,)可只保留裁剪canvas_size/fixed_canvas决定裁剪编辑器初始化时的画布尺寸与是否固定sources决定图片来源上传 / 摄像头 / 剪贴板裁剪的输入图片由此而来。可直接运行 demo/image_editor/run.py 体验完整流程该 demo 创建了一个gr.ImageEditor输入与gr.Image预览输出并分别统计upload/change/input事件次数其中im.change(predict, ...)会把裁剪后的composite实时显示到预览图中。10. 小结CropTool是一个典型的“状态机 事件驱动”PIXI.js 交互组件它以crop_bounds为唯一事实来源single source of truth通过 stage 级指针事件 控制柄分发实现拖拽通过四步钳制与对边补偿保证边界合法通过半透明 overlay 遮罩提供视觉反馈并通过独立的“裁剪编辑器”实例与get_canvas_blob完成裁剪像素导出。理解这套实现不仅有助于二次开发 Gradio 图片编辑器其“局部坐标存储 屏幕坐标渲染 全局坐标输入”的三层坐标模型以及“专用编辑器实例隔离复杂交互”的架构思路也值得在其他 Canvas/WebGL 图像工具中借鉴。延伸阅读js/imageeditor/shared/crop/crop.ts裁剪工具完整实现js/imageeditor/shared/core/editor.tsTool接口与ImageEditorContext、ImageEditor核心类js/imageeditor/shared/Toolbar.svelte工具与子工具类型定义、裁剪按钮渲染js/imageeditor/shared/ImageEditor.svelte双编辑器架构与裁剪确认流程js/imageeditor/shared/utils/pixi.tsget_canvas_blob裁剪导出工具gradio/components/image_editor.pygr.ImageEditorPython 组件与transforms参数test/components/test_image_editor.pytransforms配置的测试验证demo/image_editor/run.py可直接运行的裁剪交互 demo【免费下载链接】gradioBuild and share delightful machine learning apps, all in Python. Star to support our work!项目地址: https://gitcode.com/GitHub_Trending/gr/gradio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表