1
@@ -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` provider(dashscope / localai,config 切换)逐张生成落盘 + 入库(记录 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` provider(dashscope / localai,config 切换)逐张生成落盘 + 入库(记录 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 生成图是付费资产,前端确认文案提示) |
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -45,7 +45,4 @@ const (
|
|||||||
|
|
||||||
// 模型版本号前缀(m1.0.0),同数据集内递增
|
// 模型版本号前缀(m1.0.0),同数据集内递增
|
||||||
ModelVersionPrefix = "m"
|
ModelVersionPrefix = "m"
|
||||||
|
|
||||||
// AI 生成图距离/数量校验:单张不合格重生成次数上限(连续不合格即报错返回,防死循环烧钱)
|
|
||||||
GenValidateMaxAttempts = 3
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 数据集图片列表(标注工作台/网格预览用)
|
||||||
|
|||||||
@@ -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 数据集图片列表(创建时间正序,标注工作台/网格预览)
|
||||||
|
|||||||
@@ -39,48 +39,74 @@ imageGen:
|
|||||||
apiKey: "" # dashscope 必填:DashScope API Key
|
apiKey: "" # dashscope 必填:DashScope API Key
|
||||||
timeoutSeconds: 600 # 每张生成超时(默认 120;z-image-turbo 首次加载含模型入显存,稳态秒级~分钟级)
|
timeoutSeconds: 600 # 每张生成超时(默认 120;z-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 通道同步)
|
||||||
|
|||||||
|
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 |