This commit is contained in:
2026-08-28 17:04:23 +08:00
parent 9fe0fd6cfc
commit dc9abf07a5
119 changed files with 197 additions and 219 deletions
+1 -1
View File
@@ -186,7 +186,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow
| GET | `/admin/datasets/cover` | 封面文件(静态字节流,`datasetId` 定位) | | GET | `/admin/datasets/cover` | 封面文件(静态字节流,`datasetId` 定位) |
| POST | `/admin/datasets/cover/delete` | 删除数据集封面 `{"datasetId":1}`:删文件 + 清 cover 字段 | | POST | `/admin/datasets/cover/delete` | 删除数据集封面 `{"datasetId":1}`:删文件 + 清 cover 字段 |
| POST | `/admin/datasets/upload` | 上传图片(multipart/form-data`datasetId` + `files` 多张,仅接受 `.jpg/.jpeg/.png`),存 `app.datasetDir`/`datasets/<name>/`,逐张入库 | | POST | `/admin/datasets/upload` | 上传图片(multipart/form-data`datasetId` + `files` 多张,仅接受 `.jpg/.jpeg/.png`),存 `app.datasetDir`/`datasets/<name>/`,逐张入库 |
| POST | `/admin/datasets/generate` | AI 生成图片 `{"datasetId":1,"species":"家鸽","count":1}`(species 可留空=物种池随机;scene/action/light 后端随机;prompt 可手填覆盖模板;文件名前缀取数据集属性)(prompt 留空时按 `imageGen.promptTemplate` 组装:物种按数据集名取 speciesByDataset 池、数量词=表单 animalCount、场景/动作/光线随机;手填覆盖)(count 1..8,同步执行,每张超时 `imageGen.timeoutSeconds`):调 `imageGen` providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 prompt),任意失败返回错误;`namePrefix` 可选,按 `<前缀>_<两位序号>.jpg` 顺序命名并续接已有最大序号(如 pigeon_01.jpg),留空用时间戳命名;`distanceMin/distanceMax`(米)与 `animalCount` 填写后**逐张校验**——RF-DETR 检测目标框,数量须**精确等于** `animalCount`、估算距离(`imageGen.distanceCalib`/最大框高占比)须在范围内,否则丢弃重生成(连续 3 次不合格报错);自动标注框数按置信度裁剪 ≤ `animalCount` | | POST | `/admin/datasets/generate` | AI 生成图片 `{"datasetId":1,"species":"家鸽","count":1,"distance":25}`(species 可留空=物种池随机;scene/action/light 后端随机;prompt 可手填覆盖模板;**每张固定 1 个目标**(数量词 "1只"animal_count=1 供标注裁剪);distance 必填注入提示词距离描述;距离校验已取消;文件名前缀取数据集属性)(prompt 留空时按 `imageGen.promptTemplate` 组装:物种按数据集名取 speciesByDataset 池、数量词=表单 animalCount、场景/动作/光线随机;手填覆盖)(count 1..8,同步执行,每张超时 `imageGen.timeoutSeconds`):调 `imageGen` providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 prompt),任意失败返回错误;`namePrefix` 可选,按 `<前缀>_<两位序号>.jpg` 顺序命名并续接已有最大序号(如 pigeon_01.jpg),留空用时间戳命名;`distance`(米)与 `animalCount` 填写后**逐张校验**——RF-DETR 检测目标框,距离校验已取消(2026-08-28,无逐张拒检,生成即入库);`animalCount` 仅作为自动标注框数上限(按置信度裁剪 ≤N),不参与生成校验 |
| GET | `/admin/datasets/images` | 数据集图片列表 `{"datasetId":1}`,返回图片元数据(文件名/来源/prompt/创建时间) | | GET | `/admin/datasets/images` | 数据集图片列表 `{"datasetId":1}`,返回图片元数据(文件名/来源/prompt/创建时间) |
| GET | `/admin/datasets/image` | 图片文件(静态字节流,`datasetId` + `filename` 定位,供缩略图/查看) | | GET | `/admin/datasets/image` | 图片文件(静态字节流,`datasetId` + `filename` 定位,供缩略图/查看) |
| POST | `/admin/datasets/images/delete` | 删除图片 `{"datasetId":1,"ids":[1,2]}`:删文件 + 删记录(AI 生成图是付费资产,前端确认文案提示) | | POST | `/admin/datasets/images/delete` | 删除图片 `{"datasetId":1,"ids":[1,2]}`:删文件 + 删记录(AI 生成图是付费资产,前端确认文案提示) |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>视野管理端</title> <title>视野管理端</title>
<script type="module" crossorigin src="/admin/assets/index-DOXyyubn.js"></script> <script type="module" crossorigin src="/admin/assets/index-DgCAfMbR.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-CLxGZFm2.css"> <link rel="stylesheet" crossorigin href="/admin/assets/index-pzRM_JB0.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
-3
View File
@@ -45,7 +45,4 @@ const (
// 模型版本号前缀(m1.0.0),同数据集内递增 // 模型版本号前缀(m1.0.0),同数据集内递增
ModelVersionPrefix = "m" ModelVersionPrefix = "m"
// AI 生成图距离/数量校验:单张不合格重生成次数上限(连续不合格即报错返回,防死循环烧钱)
GenValidateMaxAttempts = 3
) )
+8
View File
@@ -122,6 +122,14 @@ func (d *datasetDao) UpdateConfigs(ctx context.Context, id int64, m *entity.Data
return err return err
} }
// UpdateDescription 更新描述(描述是自由文本,允许清空:空值也写入;
// UpdateConfigs 空值不覆盖,清空描述无法复用,2026-08-28)
func (d *datasetDao) UpdateDescription(ctx context.Context, id int64, description string) error {
_, err := g.DB().Model(consts.TableDataset).Ctx(ctx).Where("id", id).
Data(g.Map{"description": description, "updated_at": gtime.Now()}).Update()
return err
}
// ClearCover 清空封面字段(删除封面用;UpdateConfigs 空值不覆盖,无法复用) // ClearCover 清空封面字段(删除封面用;UpdateConfigs 空值不覆盖,无法复用)
func (d *datasetDao) ClearCover(ctx context.Context, id int64) error { func (d *datasetDao) ClearCover(ctx context.Context, id int64) error {
_, err := g.DB().Model(consts.TableDataset).Ctx(ctx).Where("id", id).Data(g.Map{"cover": "", "updated_at": gtime.Now()}).Update() _, err := g.DB().Model(consts.TableDataset).Ctx(ctx).Where("id", id).Data(g.Map{"cover": "", "updated_at": gtime.Now()}).Update()
+12 -14
View File
@@ -96,20 +96,17 @@ type AdminDatasetUploadRes struct {
} }
// AdminDatasetGenerateReq AI 生成图片(同步执行;prompt 可手填覆盖模板,禁止含目标位置描述; // AdminDatasetGenerateReq AI 生成图片(同步执行;prompt 可手填覆盖模板,禁止含目标位置描述;
// 未手填走 imageGen.promptTemplate 组装:species/scene 用表单选择,数量词=animalCount。 // 未手填走 imageGen.promptTemplate 组装:species 用表单选择scene/action/light 随机),数量词=animalCount。
// distanceMin/distanceMax(米)与 animalCount 填写后逐张校验:RF-DETR 检测目标框(去重口径), // animalCount 仅作为标注框数上限存储(自动标注按置信度裁剪 ≤N),不参与生成校验;
// 数量精确匹配、估算距离(物理公式)越界自动重新生成(上限 consts.GenValidateMaxAttempts 次)。 // 距离校验已取消(2026-08-28)。文件名前缀为数据集属性(datasets 表 name_prefix
// 文件名前缀为数据集属性(datasets 表 name_prefix),不再随生成请求传入
type AdminDatasetGenerateReq struct { type AdminDatasetGenerateReq struct {
g.Meta `path:"/datasets/generate" method:"post" summary:"AI 生成图片" tags:"管理端"` g.Meta `path:"/datasets/generate" method:"post" summary:"AI 生成图片" tags:"管理端"`
DatasetId int64 `json:"datasetId" v:"required|min:1" dc:"数据集 id"` DatasetId int64 `json:"datasetId" v:"required|min:1" dc:"数据集 id"`
Species string `json:"species" v:"length:0,50" dc:"动物名称(如家鸽/山斑鸠;留空按数据集名取物种池随机)"` Species string `json:"species" v:"length:0,50" dc:"动物名称(如家鸽/山斑鸠;留空按数据集名取物种池随机)"`
Prompt string `json:"prompt" v:"length:0,500" dc:"完整提示词(可选,填写则覆盖模板;禁止含位置描述)"` Prompt string `json:"prompt" v:"length:0,500" dc:"完整提示词(可选,填写则覆盖模板;禁止含位置描述)"`
Count int `json:"count" v:"required|integer|min:1|max:8" dc:"生成张数 1-8"` Count int `json:"count" v:"required|integer|min:1|max:8" dc:"生成张数 1-8"`
Size string `json:"size" v:"required|in:704x1248" dc:"图片尺寸"` Size string `json:"size" v:"required|in:704x1248" dc:"图片尺寸"`
DistanceMin int `json:"distanceMin" v:"integer|min:0" dc:"距离范围下限(米,0=不校验距离)"` Distance int `json:"distance" v:"required|min:1" dc:"目标距离(米,固定值,注入提示词)"`
DistanceMax int `json:"distanceMax" v:"integer|min:0" dc:"距离范围上限(米,0=不校验距离)"`
AnimalCount int `json:"animalCount" v:"integer|min:0|max:20" dc:"画面目标数量(精确匹配,0=不校验;标注框数不超过该值)"`
} }
// AdminDatasetGenOptionsReq 生成表单选项(物种池/场景池,来自 config imageGen 配置) // AdminDatasetGenOptionsReq 生成表单选项(物种池/场景池,来自 config imageGen 配置)
@@ -124,7 +121,8 @@ type AdminDatasetGenOptionsRes struct {
} }
type AdminDatasetGenerateRes struct { type AdminDatasetGenerateRes struct {
Generated int `json:"generated"` // 成功张数(失败时可能 < count,已成功的图保留 Generated int `json:"generated"` // 成功入库张数(校验不合格的已跳过
Rejected int `json:"rejected"` // 连续 N 次校验不合格被跳过的张数
} }
// AdminDatasetImagesReq 数据集图片列表(标注工作台/网格预览用) // AdminDatasetImagesReq 数据集图片列表(标注工作台/网格预览用)
+101 -137
View File
@@ -133,13 +133,16 @@ func (s *datasetService) AdminUpdateDataset(ctx context.Context, req *dto.AdminD
return nil, gerror.New("文件名前缀仅支持字母/数字/下划线/短横线,长度 1-50") return nil, gerror.New("文件名前缀仅支持字母/数字/下划线/短横线,长度 1-50")
} }
if err := dao.Dataset.UpdateConfigs(ctx, req.Id, &entity.Dataset{ if err := dao.Dataset.UpdateConfigs(ctx, req.Id, &entity.Dataset{
Name: newName, Name: newName,
NamePrefix: prefix, NamePrefix: prefix,
Cover: req.Cover, Cover: req.Cover,
Description: req.Description,
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
// 描述允许清空(UpdateConfigs 空值不覆盖),单独更新
if err := dao.Dataset.UpdateDescription(ctx, req.Id, strings.TrimSpace(req.Description)); err != nil {
return nil, err
}
return &dto.AdminDatasetUpdateRes{Id: req.Id}, nil return &dto.AdminDatasetUpdateRes{Id: req.Id}, nil
} }
@@ -570,67 +573,87 @@ func (s *datasetService) nextImageIndex(ctx context.Context, datasetId int64, pr
return max + 1, nil return max + 1, nil
} }
// validateGeneratedImage 生成图距离/数量校验:RF-DETR 全图检测目标框(与自动标注同口径去重), // AdminGenOptions 生成表单选项:该数据集的物种池 + 场景池(config 维护,前端下拉用)
// 数量精确匹配 animalCount;距离用物理公式 d = 物种体高 × focalPx ÷ 目标像素高 func (s *datasetService) AdminGenOptions(ctx context.Context, req *dto.AdminDatasetGenOptionsReq) (*dto.AdminDatasetGenOptionsRes, error) {
// focalPx 由假设视场角 imageGen.assumedVfovDeg 推出;体高按物种查 imageGen.speciesHeights), dataset, err := dao.Dataset.GetById(ctx, req.DatasetId)
// 须落在表单 [distanceMin, distanceMax] 内(>0 才启用对应项)
func (s *datasetService) validateGeneratedImage(ctx context.Context, data []byte, req *dto.AdminDatasetGenerateReq, species string) (bool, string, error) {
client := common.LocalAiClient(ctx)
if client == nil {
return false, "", gerror.NewCode(common.CodeLocalAiNotConfigured)
}
// 校验与预标注用途不同:预标注阈值 0.08 宁多勿漏,会把玉米叶/土块算成目标(实测 30-40m 场景 6-18 框噪声);
// 校验用高置信度阈值(实测 0.3 时真鸟全留、噪声清零,0.4 起连远处真鸟都丢)
client.Threshold = g.Cfg().MustGet(ctx, "imageGen.validateThreshold", 0.3).Float64()
imgW, imgH, err := parseGenSize(req.Size)
if err != nil { if err != nil {
return false, "", err return nil, err
} }
dets, err := client.Detect(ctx, data, "image/jpeg", imgW, imgH) if dataset == nil {
if err != nil { return nil, gerror.NewCode(common.CodeDatasetNotFound)
return false, "", gerror.Wrap(err, "生成图检测失败")
} }
// 与自动标注同口径去重(同目标多框只计一只),保证「校验数量 == 标注框数」 return &dto.AdminDatasetGenOptionsRes{
cands := make([]*dto.AdminLabelBox, 0, len(dets)) Species: g.Cfg().MustGet(ctx, "imageGen.speciesByDataset."+dataset.Name).Strings(),
for _, d := range dets { Scenes: g.Cfg().MustGet(ctx, "imageGen.scenes").Strings(),
cands = append(cands, &dto.AdminLabelBox{ }, nil
Cx: (d.X + d.Width/2) / float64(imgW), }
Cy: (d.Y + d.Height/2) / float64(imgH),
W: d.Width / float64(imgW), // singleAnimalClause 目标数量约束:每张仅一只动物,手填提示词统一追加(模板已由 {count} 固定 "1只"
H: d.Height / float64(imgH), const singleAnimalClause = "画面中有且只有这一只动物,没有任何其他动物"
Confidence: d.Confidence,
}) // buildPromptFromTemplate 按 config 通用模板组装提示词:物种/场景优先用表单选择
// (物种只写名字不写羽毛细节——模型对正确名称自带外观先验;场景未选则池内随机),
// 物种未填按数据集名取物种池随机(池也未配置则用数据集名本身);动作/光线池内随机。
func (s *datasetService) buildPromptFromTemplate(ctx context.Context, datasetName, species string, dist int, size string) (string, error) {
tpl := g.Cfg().MustGet(ctx, "imageGen.promptTemplate").String()
if tpl == "" {
return "", gerror.New("未配置通用提示词模板(imageGen.promptTemplate),请手填提示词")
} }
deduped := suppressOverlap(cands, client.OverlapThreshold) if species == "" {
if len(deduped) == 0 { species = datasetName
return false, "未检测到目标动物", nil if pool := g.Cfg().MustGet(ctx, "imageGen.speciesByDataset."+datasetName).Strings(); len(pool) > 0 {
} species = pool[rand.Intn(len(pool))]
// 数量为精确校验:表单指定几只,画面里就必须恰好几只(提示词中的数量应与表单一致)
if req.AnimalCount > 0 && len(deduped) != req.AnimalCount {
return false, fmt.Sprintf("目标数量 %d 与指定数量 %d 不符", len(deduped), req.AnimalCount), nil
}
maxRatio := 0.0
for _, b := range deduped {
if b.H > maxRatio {
maxRatio = b.H
} }
} }
// 物理公式:d = 体高 × focalPx ÷ 像素高;focalPx = 图高/2 ÷ tan(vfov/2) // 场景/动作按数据集习性取池(鱼水域、雉鸡灌丛……),未配置该数据集回退通用池
heightM := g.Cfg().MustGet(ctx, "imageGen.speciesHeights.default", 0.35).Float64() scene := pickCfgByDataset(ctx, datasetName, "imageGen.sceneByDataset", "imageGen.scenes")
if h := g.Cfg().MustGet(ctx, "imageGen.speciesHeights."+species).Float64(); h > 0 { action := pickCfgByDataset(ctx, datasetName, "imageGen.actionByDataset", "imageGen.actions")
heightM = h light := pickCfg(ctx, "imageGen.lights")
// 距离描述从表单固定值生成(25 → "25米外"
distWord := fmt.Sprintf("%d米外", dist)
// 尺寸提示:按物理公式把表单距离换算成目标在图中的像素高与画面占比
sizeHint, sErr := s.buildSizeHint(ctx, species, dist, size)
if sErr != nil {
return "", sErr
} }
// 每张固定 1 个目标(数量词固定,animal_count 列同为 1 供自动标注裁剪)
countWord := "1只"
out := tpl
for k, v := range map[string]string{
"{scene}": scene, "{species}": species, "{count}": countWord,
"{action}": action, "{light}": light, "{distanceWord}": distWord, "{sizeHint}": sizeHint,
} {
out = strings.ReplaceAll(out, k, v)
}
g.Log().Infof(ctx, "模板组装提示词: %s", out)
return out, nil
}
// buildSizeHint 尺寸提示:目标像素高 = 体高 × focalPx ÷ 距离(focalPx = 图高/2 ÷ tan(vfov/2)),
// 输出如「(在画面中高度仅约15像素,占画面高度约1.2%)」。
// 占比向下取整且至少 1%,避免出现 0%
func (s *datasetService) buildSizeHint(ctx context.Context, species string, dist int, size string) (string, error) {
_, imgH, err := parseGenSize(size)
if err != nil {
return "", err
}
// 配置单位为 cm(实际体型),换算成米参与投影公式;未配置物种兜底 35cm
heightCM := g.Cfg().MustGet(ctx, "imageGen.speciesHeights."+species, 35.0).Float64()
heightM := heightCM / 100
// 镜头固定为手机默认主摄(垂直视场角配置值,默认 52°;2026-08-28 曾随机 [15,90]、曾试超广角,用户最终定为默认镜头)
vfov := g.Cfg().MustGet(ctx, "imageGen.assumedVfovDeg", 52.0).Float64() * math.Pi / 180 vfov := g.Cfg().MustGet(ctx, "imageGen.assumedVfovDeg", 52.0).Float64() * math.Pi / 180
focalPx := float64(imgH) / 2 / math.Tan(vfov/2) focalPx := float64(imgH) / 2 / math.Tan(vfov/2)
pxHeight := maxRatio * float64(imgH) pxAt := func(dist int) int {
estDist := heightM * focalPx / pxHeight if dist <= 0 {
if req.DistanceMin > 0 && estDist < float64(req.DistanceMin) { dist = 1
return false, fmt.Sprintf("估算距离约 %.0f 米低于下限 %d 米(目标过近)", estDist, req.DistanceMin), nil }
return int(math.Round(heightM * focalPx / float64(dist)))
} }
if req.DistanceMax > 0 && estDist > float64(req.DistanceMax) { pct := func(px int) string {
return false, fmt.Sprintf("估算距离约 %.0f 米超出上限 %d 米(目标过远)", estDist, req.DistanceMax), nil return fmt.Sprintf("%.1f", float64(px)*100/float64(imgH))
} }
return true, fmt.Sprintf("数量 %d,最大目标估算距离约 %.0f 米", len(deduped), estDist), nil px := pxAt(dist)
return fmt.Sprintf("(在画面中高度仅约%d像素,占画面高度约%s%%)", px, pct(px)), nil
} }
// parseGenSize 解析 "704x1248" 形式尺寸 // parseGenSize 解析 "704x1248" 形式尺寸
@@ -647,57 +670,6 @@ func parseGenSize(size string) (int, int, error) {
return w, h, nil return w, h, nil
} }
// countWordCN 数量词(与表单 animalCount 精确对应,防提示词文案与校验数量不符)
var countWordCN = [...]string{"一只", "两只", "三只", "四只", "五只", "六只", "七只", "八只", "九只", "十只"}
// AdminGenOptions 生成表单选项:该数据集的物种池 + 场景池(config 维护,前端下拉用)
func (s *datasetService) AdminGenOptions(ctx context.Context, req *dto.AdminDatasetGenOptionsReq) (*dto.AdminDatasetGenOptionsRes, error) {
dataset, err := dao.Dataset.GetById(ctx, req.DatasetId)
if err != nil {
return nil, err
}
if dataset == nil {
return nil, gerror.NewCode(common.CodeDatasetNotFound)
}
return &dto.AdminDatasetGenOptionsRes{
Species: g.Cfg().MustGet(ctx, "imageGen.speciesByDataset."+dataset.Name).Strings(),
Scenes: g.Cfg().MustGet(ctx, "imageGen.scenes").Strings(),
}, nil
}
// buildPromptFromTemplate 按 config 通用模板组装提示词:物种/场景优先用表单选择
// (物种只写名字不写羽毛细节——模型对正确名称自带外观先验;场景未选则池内随机),
// 物种未填按数据集名取物种池随机(池也未配置则用数据集名本身);动作/光线池内随机。
func (s *datasetService) buildPromptFromTemplate(ctx context.Context, datasetName, species string, count int) (string, string, error) {
tpl := g.Cfg().MustGet(ctx, "imageGen.promptTemplate").String()
if tpl == "" {
return "", "", gerror.New("未配置通用提示词模板(imageGen.promptTemplate),请手填提示词")
}
if species == "" {
species = datasetName
if pool := g.Cfg().MustGet(ctx, "imageGen.speciesByDataset."+datasetName).Strings(); len(pool) > 0 {
species = pool[rand.Intn(len(pool))]
}
}
scene := pickCfg(ctx, "imageGen.scenes")
action := pickCfg(ctx, "imageGen.actions")
light := pickCfg(ctx, "imageGen.lights")
dist := g.Cfg().MustGet(ctx, "imageGen.distanceWord", "远处").String()
countWord := fmt.Sprintf("%d只", count)
if count >= 1 && count <= len(countWordCN) {
countWord = countWordCN[count-1]
}
out := tpl
for k, v := range map[string]string{
"{scene}": scene, "{species}": species, "{count}": countWord,
"{action}": action, "{light}": light, "{distanceWord}": dist,
} {
out = strings.ReplaceAll(out, k, v)
}
g.Log().Infof(ctx, "模板组装提示词: %s", out)
return out, species, nil
}
// pickCfg 取字符串数组配置项随机一项(空池返回空串) // pickCfg 取字符串数组配置项随机一项(空池返回空串)
func pickCfg(ctx context.Context, key string) string { func pickCfg(ctx context.Context, key string) string {
pool := g.Cfg().MustGet(ctx, key).Strings() pool := g.Cfg().MustGet(ctx, key).Strings()
@@ -707,6 +679,15 @@ func pickCfg(ctx context.Context, key string) string {
return pool[rand.Intn(len(pool))] return pool[rand.Intn(len(pool))]
} }
// pickCfgByDataset 按数据集名取配置池(key 形如 imageGen.sceneByDataset.<数据集名>)随机,
// 该数据集未配置时回退通用池
func pickCfgByDataset(ctx context.Context, datasetName, byDatasetKey, genericKey string) string {
if pool := g.Cfg().MustGet(ctx, byDatasetKey+"."+datasetName).Strings(); len(pool) > 0 {
return pool[rand.Intn(len(pool))]
}
return pickCfg(ctx, genericKey)
}
// AdminGenerateImages AI 生成图片:同步逐张生成(每张超时 imageGen.timeoutSeconds),逐张落盘 + 入库; // AdminGenerateImages AI 生成图片:同步逐张生成(每张超时 imageGen.timeoutSeconds),逐张落盘 + 入库;
// 中途失败返回错误并附已成功张数(已生成的图为付费资产,保留不删除)。 // 中途失败返回错误并附已成功张数(已生成的图为付费资产,保留不删除)。
func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.AdminDatasetGenerateReq) (*dto.AdminDatasetGenerateRes, error) { func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.AdminDatasetGenerateReq) (*dto.AdminDatasetGenerateRes, error) {
@@ -754,6 +735,7 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin
return nil, err return nil, err
} }
} }
rejected := 0
for i := 0; i < req.Count; i++ { for i := 0; i < req.Count; i++ {
// 部分成功也要标注:报错返回前对已入库图片触发自动标注(外部重试驱动依赖此语义攒量) // 部分成功也要标注:报错返回前对已入库图片触发自动标注(外部重试驱动依赖此语义攒量)
failWith := func(err error) (*dto.AdminDatasetGenerateRes, error) { failWith := func(err error) (*dto.AdminDatasetGenerateRes, error) {
@@ -764,15 +746,21 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin
} }
} }
} }
return &dto.AdminDatasetGenerateRes{Generated: generated}, err return &dto.AdminDatasetGenerateRes{Generated: generated, Rejected: rejected}, err
} }
// 提示词:手填优先;未手填走 config 通用模板逐张组装——物种/场景用表单选择, // 提示词:手填优先;未手填走 config 通用模板逐张组装——物种用表单选择(未填池内随机)
// 未填按数据集名取物种池随机、场景池随机;数量词与表单 animalCount 一致;动作/光线随机 // 场景/动作/光线随机。距离校验已取消(2026-08-28)
prompt := strings.TrimSpace(req.Prompt) prompt := strings.TrimSpace(req.Prompt)
species := strings.TrimSpace(req.Species) if handWritten {
// 手填提示词统一追加数量约束:仅一只动物(拍摄角度不带,交由模型平视先验自由构图)
prompt = strings.TrimSuffix(prompt, "。") + "。"
if !strings.Contains(prompt, "一只") && !strings.Contains(prompt, "1只") && !strings.Contains(prompt, "唯一") {
prompt += singleAnimalClause
}
}
if !handWritten { if !handWritten {
var tErr error var tErr error
if prompt, species, tErr = s.buildPromptFromTemplate(ctx, dataset.Name, species, req.AnimalCount); tErr != nil { if prompt, tErr = s.buildPromptFromTemplate(ctx, dataset.Name, strings.TrimSpace(req.Species), req.Distance, req.Size); tErr != nil {
return nil, tErr return nil, tErr
} }
} }
@@ -781,30 +769,6 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin
// 付费资产保留原则:已生成的不删除 // 付费资产保留原则:已生成的不删除
return failWith(gerror.NewCode(common.CodeImageGenFailed, fmt.Sprintf("第 %d 张生成失败(已生成 %d 张): %v", i+1, generated, genErr))) return failWith(gerror.NewCode(common.CodeImageGenFailed, fmt.Sprintf("第 %d 张生成失败(已生成 %d 张): %v", i+1, generated, genErr)))
} }
// 逐张校验(表单填写距离范围/目标数量后启用):不合格丢弃并重新生成,连续不合格报错返回
needValidate := req.DistanceMax > 0 || req.DistanceMin > 0 || req.AnimalCount > 0
if needValidate {
passed := false
reason := ""
for attempt := 1; attempt <= consts.GenValidateMaxAttempts; attempt++ {
ok, r, vErr := s.validateGeneratedImage(ctx, data, req, species)
if vErr != nil {
return failWith(gerror.Wrapf(vErr, "第 %d 张校验失败(已生成 %d 张)", i+1, generated))
}
if ok {
passed = true
break
}
reason = r
g.Log().Infof(ctx, "生成图校验不合格(%s),第 %d 张第 %d 次重新生成", r, i+1, attempt)
if data, genErr = genOnce(prompt); genErr != nil {
return failWith(gerror.NewCode(common.CodeImageGenFailed, fmt.Sprintf("第 %d 张重新生成失败(已生成 %d 张): %v", i+1, generated, genErr)))
}
}
if !passed {
return failWith(gerror.NewCode(common.CodeImageGenFailed, fmt.Sprintf("第 %d 张连续 %d 次校验不合格(%s),请调整提示词或范围后重试(已生成 %d 张)", i+1, consts.GenValidateMaxAttempts, reason, generated)))
}
}
filename := "" filename := ""
if prefix != "" { if prefix != "" {
filename = fmt.Sprintf("%s_%02d.jpg", prefix, nextIdx) filename = fmt.Sprintf("%s_%02d.jpg", prefix, nextIdx)
@@ -822,7 +786,7 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin
Filename: filename, Filename: filename,
Source: "ai", Source: "ai",
Prompt: prompt, Prompt: prompt,
AnimalCount: req.AnimalCount, AnimalCount: 1,
CreatedAt: now, CreatedAt: now,
}) })
imgId = id imgId = id
@@ -846,10 +810,10 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin
return nil, err return nil, err
} }
if err := LabelTask.AutoLabel(ctx, dataset, newImages); err != nil { if err := LabelTask.AutoLabel(ctx, dataset, newImages); err != nil {
return &dto.AdminDatasetGenerateRes{Generated: generated}, return &dto.AdminDatasetGenerateRes{Generated: generated, Rejected: rejected},
gerror.Wrap(err, "图片已生成入库,但自动标注触发失败") gerror.Wrap(err, "图片已生成入库,但自动标注触发失败")
} }
return &dto.AdminDatasetGenerateRes{Generated: generated}, nil return &dto.AdminDatasetGenerateRes{Generated: generated, Rejected: rejected}, nil
} }
// AdminListImages 数据集图片列表(创建时间正序,标注工作台/网格预览) // AdminListImages 数据集图片列表(创建时间正序,标注工作台/网格预览)
+60 -34
View File
@@ -39,48 +39,74 @@ imageGen:
apiKey: "" # dashscope 必填:DashScope API Key apiKey: "" # dashscope 必填:DashScope API Key
timeoutSeconds: 600 # 每张生成超时(默认 120z-image-turbo 首次加载含模型入显存,稳态秒级~分钟级) timeoutSeconds: 600 # 每张生成超时(默认 120z-image-turbo 首次加载含模型入显存,稳态秒级~分钟级)
# 通用提示词模板(管理端填「动物名称」时启用,每张随机取池组装):{species} 优先用表单填写的名称, # 通用提示词模板(管理端填「动物名称」时启用,每张随机取池组装):{species} 优先用表单填写的名称,
# 未填则按数据集名取 speciesByDataset 池(再未配置直接用数据集名);{count} 为数量词(表单 # 未填则按数据集名取 speciesByDataset 池(再未配置直接用数据集名);{count} 为数量词(固定 "1只",每张仅 1 个目标;原表单
# animalCount 一致,防文数不符);{scene}/{action}/{light} 池内随机;物种只写名字不写羽毛细节 # 数量选择已取消);{scene}/{action}/{light} 池内随机;物种只写名字不写羽毛细节
# (细节会把镜头拉近,详见技术设计) # (细节会把镜头拉近,详见技术设计)。不带拍摄角度描述(2026-08-28):角度描述会与目标位置耦合,
promptTemplate: "野外大远景实拍:广角镜头下{scene}{distanceWord}有{count}{species}的小小身影在{action},鸟离镜头很远,只是很小的深色鸟形轮廓,隐约可辨,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" # 仰拍使贴地目标沉底、固定俯拍构图单一——交由模型平视先验自由构图,目标自然落在中部/上部
distanceWord: "二十多米外" # 与校验距离带呼应的观感词 promptTemplate: "野外大远景实拍:广角镜头下{scene}{distanceWord}有且仅有{count}{species}的小小身影{sizeHint}在{action}{species}离镜头很远,只是很小的深色轮廓,隐约可辨,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感"
speciesByDataset: # 数据集名 → 物种池(表单未填动物名称时随机取一个) speciesByDataset: # 数据集名 → 物种池(表单未填动物名称时随机取一个)
鸽子: [家鸽,火斑鸠,虎纹斑鸠,珠颈斑鸠,灰斑鸠] 鸽子: [家鸽,火斑鸠,虎纹斑鸠,珠颈斑鸠,灰斑鸠]
雉鸡: [雉鸡] 雉鸡: [雉鸡]
兔子: [兔子] 兔子: [兔子]
鸭子: [野鸭] 鸭子: [野鸭]
鹌鹑: [鹌鹑] 鹌鹑: [鹌鹑]
scenes: # 环境场景池(同一短语在模板中出现两次,须两处都读得通) scenes: # 环境场景池(统一远景原则:每条含开阔/延伸向远方/地平线元素,
- "广袤的玉米地,玉米秆两米多高,田间土路和田埂向远处地平线延伸" # 把地平线推开、消除近拍暗示;同一短语在模板中出现两次,须两处都读得通)
- "收割后的金黄麦茬田铺向地平线" - "一望无际的玉米地,玉米秆两米多高,田间土路和田埂延伸向地平线"
- "乡村土路穿过农田伸向远方,路边杂草丛生" - "一望无际的金黄麦茬田,收割后一直铺到地平线"
- "农田和道路延伸向远方,一根细电线横贯画面远处" - "乡村土路穿过广阔的农田伸向远方,路边杂草丛生"
- "农田边一棵大树和远处成片的田野" - "广阔的农田和道路延伸向远方,一根细电线横贯画面远处"
- "农家院落外的打谷场和远处农舍" - "广阔田野间一棵高大的树,四周的农田一直延伸到地平线"
- "收割后的稻茬田铺向远处村庄" - "村边开阔的打谷场延伸向远方,尽头是零星的农舍"
- "果树成行的果园和树下剪短的草地" - "收割后的稻茬田一望无际,铺向远处的村庄"
- "乡间水渠和沙石滩地,渠水流向远方" - "果树成行的果园延伸向远方,树下是剪短的草地"
- "长满枯黄荒草的田埂和弃耕地" - "开阔的沙石滩地和蜿蜒的乡间水渠延伸向远处的田野"
- "长满枯黄荒草的田埂和弃耕地延伸向远方"
actions: [觅食, 走动啄食, 低头啄食, 踱步, 停落休憩] actions: [觅食, 走动啄食, 低头啄食, 踱步, 停落休憩]
lights: [清晨柔和阳光, 正午明亮天光, 傍晚金色斜阳, 阴天柔和散射光] lights: [清晨柔和阳光, 正午明亮天光, 傍晚金色斜阳, 阴天柔和散射光]
# 生成图距离校验(物理公式):估算距离 d = speciesHeight × focalPx ÷ 目标像素高 sceneByDataset: # 数据集名 → 场景池(按动物习性配置,未配置的数据集回退通用 scenes;
# focalPx 由假设视场角推出:f = 图高/2 ÷ tan(vfov/2)vfov=52° 时 704x1248 图 f≈1281px # 每条仍含「延伸向远方」元素,且同一短语在模板中出现两次须两处都读得通)
# 换算 0.35m 鸟 15px ≈ 30 米,与实测观感标定一致);体高按表单物种查 speciesHeights 雉鸡:
# (不同物种体型不同,不能共用一个系数),未配置的物种用 default - "田边茂密的草丛和矮灌丛连绵延伸到远方"
assumedVfovDeg: 52 - "荒草坡和灌木丛连成一片,一直铺向远处地平线"
speciesHeights: - "收割后的农田边缘杂草丛生,矮灌丛点缀延伸到远方"
default: 0.35 兔子:
家鸽: 0.35 - "枯黄的草坡和荒草地一直延伸到远方"
珠颈斑鸠: 0.30 - "弃耕地和田埂长满野草,延伸向远处的林地"
灰斑鸠: 0.30 - "草丛和矮灌丛交错的荒地,向远处延展"
火斑鸠: 0.25 鸭子:
虎纹斑鸠: 0.30 - "宽阔的池塘水面延伸到远方,岸边芦苇丛生"
野鸡: 0.45 - "水田和浅滩连成一片,水面延伸到远处"
兔子: 0.30 - "河湖水面平展到远方,岸边是泥滩和杂草"
野鸭: 0.30 :
鹌鹑: 0.15 - "清澈的池塘水面下,水草和沙底延伸到远处"
# 校验用检测置信度阈值(预标注的 localAi.threshold=0.08 宁多勿漏会误把叶片土块算鸟,校验须高阈值) - "河流浅滩水域,水底砂石和水草延伸到远方"
validateThreshold: 0.3 - "湖水清澈见底,水下的景色延伸到远处"
鹌鹑:
- "茂密草丛的田野,草层延伸到远方"
- "稻茬田边草丛和荒地相连,延伸向远处"
actionByDataset: # 数据集名 → 动作池(按动物习性;动作词不得以「在/于」开头,模板为「在{action}」)
雉鸡: [低头啄食, 草丛中觅食, 踱步, 停歇理羽, 警觉张望]
兔子: [低头啃食草叶, 警觉地竖起耳朵, 小步跳跃前行, 蹲坐不动, 啃食草叶]
鸭子: [缓缓游动于水面, 低头潜入水中觅食, 水面梳理羽毛, 缓慢划水前行, 岸边泥滩觅食]
: [缓慢游动, 贴着水底游动, 水草丛间穿行, 静止悬浮, 缓慢觅食]
鹌鹑: [低头啄食, 草丛间觅食, 小步疾走, 停歇不动]
# 目标尺寸提示(sizeHint):按「体高 × focalPx ÷ 表单距离」算出目标在图中的像素高与占比,
# 注入模板 {sizeHint}——把「厘米」换算成「米」再参与投影公式;focalPx = 图高/2 ÷ tan(vfov/2)
assumedVfovDeg: 52 # 镜头垂直视场角(度),固定为手机默认主摄(2026-08-28 用户定案;
# 曾随机 [15,90]、曾试超广角 90,均已否决);体高按物种查 speciesHeights
speciesHeights: # 物种站高(厘米,垂直尺度:鸟=脚底到头顶、兽=蹲坐高度、鱼=背到腹);
# 与「高度」像素描述及检测框高(≈站高)自洽,勿用体长(体长是水平尺度);
# 未配置的物种代码兜底 35cm
家鸽: 20
珠颈斑鸠: 20
灰斑鸠: 18
火斑鸠: 15
虎纹斑鸠: 18
野鸡: 55
兔子: 35
野鸭: 35
鹌鹑: 12
# 训练通道(并发度 1:GPU 独占,同时仅一个 running 任务): # 训练通道(并发度 1:GPU 独占,同时仅一个 running 任务):
# mode=subprocess 训练机与服务器同机;mode=ssh 异机(训练脚本/数据集经 ssh 通道同步) # mode=subprocess 训练机与服务器同机;mode=ssh 异机(训练脚本/数据集经 ssh 通道同步)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Some files were not shown because too many files have changed in this diff Show More