hazard.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. package controllers
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "image"
  8. "image/color"
  9. "image/draw"
  10. _ "image/gif"
  11. "image/jpeg"
  12. "image/png"
  13. "io"
  14. "net/http"
  15. "net/url"
  16. "os"
  17. "shudao-chat-go/models"
  18. "shudao-chat-go/utils"
  19. "strings"
  20. "time"
  21. "github.com/aws/aws-sdk-go/aws"
  22. "github.com/aws/aws-sdk-go/aws/credentials"
  23. "github.com/aws/aws-sdk-go/aws/session"
  24. "github.com/aws/aws-sdk-go/service/s3"
  25. "github.com/beego/beego/v2/server/web"
  26. "github.com/fogleman/gg"
  27. )
  28. type HazardController struct {
  29. web.Controller
  30. }
  31. // 图片压缩配置 - 使用shudaooss.go中的配置
  32. type HazardRequest struct {
  33. //场景名称
  34. SceneName string `json:"scene_name"`
  35. //图片
  36. Image string `json:"image"`
  37. // 用户ID、手机号、用户姓名从token中获取
  38. //日期
  39. Date string `json:"date"`
  40. }
  41. // YOLOResponse 定义YOLO响应结构
  42. type YOLOResponse struct {
  43. Boxes [][]float64 `json:"boxes"`
  44. Labels []string `json:"labels"`
  45. Scores []float64 `json:"scores"`
  46. ModelType string `json:"model_type"`
  47. }
  48. // HazardResponse 定义隐患识别响应结构
  49. type HazardResponse struct {
  50. Code int `json:"code"`
  51. Msg string `json:"msg"`
  52. Data map[string]interface{} `json:"data"`
  53. }
  54. // 隐患识别
  55. func (c *HazardController) Hazard() {
  56. // 从token中获取用户信息
  57. userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
  58. if err != nil {
  59. c.Data["json"] = HazardResponse{
  60. Code: 401,
  61. Msg: "获取用户信息失败: " + err.Error(),
  62. }
  63. c.ServeJSON()
  64. return
  65. }
  66. var requestData HazardRequest
  67. if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
  68. c.Data["json"] = HazardResponse{
  69. Code: 400,
  70. Msg: "参数错误",
  71. }
  72. c.ServeJSON()
  73. return
  74. }
  75. fmt.Println("requestData", requestData)
  76. // 验证参数
  77. if requestData.SceneName == "" || requestData.Image == "" {
  78. c.Data["json"] = HazardResponse{
  79. Code: 400,
  80. Msg: "场景名称和图片链接不能为空",
  81. }
  82. c.ServeJSON()
  83. return
  84. }
  85. // 从OSS下载图片
  86. imageData, err := downloadImageFromOSS(requestData.Image)
  87. if err != nil {
  88. c.Data["json"] = HazardResponse{
  89. Code: 500,
  90. Msg: "下载图片失败: " + err.Error(),
  91. }
  92. c.ServeJSON()
  93. return
  94. }
  95. // 验证图片数据
  96. if len(imageData) == 0 {
  97. c.Data["json"] = HazardResponse{
  98. Code: 500,
  99. Msg: "下载的图片数据为空",
  100. }
  101. c.ServeJSON()
  102. return
  103. }
  104. fmt.Printf("下载图片成功,大小: %d 字节\n", len(imageData))
  105. // 将图片转换为base64
  106. imageBase64 := base64.StdEncoding.EncodeToString(imageData)
  107. // 获取YOLO API配置
  108. yoloBaseURL, err := web.AppConfig.String("yolo_base_url")
  109. if err != nil || yoloBaseURL == "" {
  110. yoloBaseURL = "http://172.16.35.50:18080" // 默认值
  111. }
  112. // 构建YOLO请求
  113. yoloRequest := map[string]interface{}{
  114. "modeltype": requestData.SceneName,
  115. "image": imageBase64,
  116. "conf_threshold": 0.5, // 默认置信度
  117. }
  118. jsonData, err := json.Marshal(yoloRequest)
  119. if err != nil {
  120. c.Data["json"] = HazardResponse{
  121. Code: 500,
  122. Msg: "序列化请求失败: " + err.Error(),
  123. }
  124. c.ServeJSON()
  125. return
  126. }
  127. // 调用YOLO API
  128. resp, err := http.Post(
  129. fmt.Sprintf("%s/predict", yoloBaseURL),
  130. "application/json",
  131. bytes.NewBuffer(jsonData),
  132. )
  133. if err != nil {
  134. c.Data["json"] = HazardResponse{
  135. Code: 500,
  136. Msg: "调用YOLO API失败: " + err.Error(),
  137. }
  138. c.ServeJSON()
  139. return
  140. }
  141. defer resp.Body.Close()
  142. // 读取响应
  143. body, err := io.ReadAll(resp.Body)
  144. if err != nil {
  145. c.Data["json"] = HazardResponse{
  146. Code: 500,
  147. Msg: "读取YOLO响应失败: " + err.Error(),
  148. }
  149. c.ServeJSON()
  150. return
  151. }
  152. fmt.Println("body111", string(body))
  153. if resp.StatusCode != http.StatusOK {
  154. c.Data["json"] = HazardResponse{
  155. Code: resp.StatusCode,
  156. Msg: "YOLO API返回错误: " + string(body),
  157. }
  158. c.ServeJSON()
  159. return
  160. }
  161. // 解析YOLO响应
  162. var yoloResp YOLOResponse
  163. if err := json.Unmarshal(body, &yoloResp); err != nil {
  164. c.Data["json"] = HazardResponse{
  165. Code: 500,
  166. Msg: "解析YOLO响应失败: " + err.Error(),
  167. }
  168. c.ServeJSON()
  169. return
  170. }
  171. fmt.Println("yoloResp222", yoloResp)
  172. //如果labels为空,则返回错误
  173. if len(yoloResp.Labels) == 0 {
  174. c.Data["json"] = HazardResponse{
  175. Code: 500,
  176. Msg: "没有识别到任何隐患",
  177. }
  178. c.ServeJSON()
  179. return
  180. }
  181. // 构建返回数据
  182. detectionResults := make([]map[string]interface{}, 0)
  183. for i, label := range yoloResp.Labels {
  184. if i < len(yoloResp.Scores) && i < len(yoloResp.Boxes) {
  185. result := map[string]interface{}{
  186. "label": label,
  187. "score": yoloResp.Scores[i],
  188. "box": yoloResp.Boxes[i],
  189. "percent": fmt.Sprintf("%.2f%%", yoloResp.Scores[i]*100),
  190. }
  191. detectionResults = append(detectionResults, result)
  192. }
  193. }
  194. // 生成标注后的图片并上传到OSS
  195. var annotatedImageURL string
  196. if len(detectionResults) > 0 {
  197. // 解码原始图片
  198. fmt.Printf("开始解码图片,数据大小: %d 字节\n", len(imageData))
  199. // 检查图片格式
  200. img, format, err := image.Decode(bytes.NewReader(imageData))
  201. if err != nil {
  202. fmt.Printf("图片解码失败: %v\n", err)
  203. fmt.Printf("图片数据前20字节: %x\n", imageData[:min(20, len(imageData))])
  204. // 尝试手动检测PNG格式并转换
  205. if len(imageData) >= 8 && string(imageData[:8]) == "\x89PNG\r\n\x1a\n" {
  206. fmt.Printf("检测到PNG格式,尝试特殊处理...\n")
  207. // 尝试使用PNG解码器
  208. img, err = decodePNGImage(imageData)
  209. if err != nil {
  210. fmt.Printf("PNG特殊解码也失败: %v\n", err)
  211. c.Data["json"] = HazardResponse{
  212. Code: 500,
  213. Msg: "PNG图片解码失败: " + err.Error() + ",请检查图片是否损坏",
  214. }
  215. c.ServeJSON()
  216. return
  217. }
  218. format = "png"
  219. fmt.Printf("PNG特殊解码成功\n")
  220. } else {
  221. c.Data["json"] = HazardResponse{
  222. Code: 500,
  223. Msg: "解码图片失败: " + err.Error() + ",请检查图片格式是否支持(JPEG、PNG、GIF等)",
  224. }
  225. c.ServeJSON()
  226. return
  227. }
  228. }
  229. fmt.Printf("图片解码成功,格式: %s,尺寸: %dx%d\n", format, img.Bounds().Dx(), img.Bounds().Dy())
  230. // 绘制边界框 - 使用token中的用户信息
  231. annotatedImg := drawBoundingBox(img, yoloResp.Boxes, yoloResp.Labels, yoloResp.Scores, userInfo.Name, userInfo.ContactNumber, requestData.Date)
  232. // 将标注后的图片编码为JPEG
  233. var buf bytes.Buffer
  234. if err := jpeg.Encode(&buf, annotatedImg, &jpeg.Options{Quality: 95}); err != nil {
  235. c.Data["json"] = HazardResponse{
  236. Code: 500,
  237. Msg: "编码标注图片失败: " + err.Error(),
  238. }
  239. c.ServeJSON()
  240. return
  241. }
  242. // 生成文件名
  243. utcNow := time.Now().UTC()
  244. timestamp := utcNow.Unix()
  245. fileName := fmt.Sprintf("hazard_annotated/%d/%s_%d.jpg",
  246. utcNow.Year(),
  247. utcNow.Format("0102"),
  248. timestamp)
  249. // 上传标注后的图片到OSS
  250. annotatedImageURL, err = uploadImageToOSS(buf.Bytes(), fileName)
  251. if err != nil {
  252. c.Data["json"] = HazardResponse{
  253. Code: 500,
  254. Msg: "上传标注图片到OSS失败: " + err.Error(),
  255. }
  256. c.ServeJSON()
  257. return
  258. }
  259. fmt.Printf("标注图片上传成功: %s\n", fileName)
  260. }
  261. // fmt.Println("annotatedImageURL", annotatedImageURL)
  262. // 使用token中的user_id
  263. user_id := int(userInfo.ID)
  264. if user_id == 0 {
  265. user_id = 1
  266. }
  267. //查询场景名称
  268. var scene models.Scene
  269. models.DB.Where("scene_en_name = ? and is_deleted = ?", requestData.SceneName, 0).First(&scene)
  270. if scene.ID == 0 {
  271. c.Data["json"] = map[string]interface{}{
  272. "statusCode": 500,
  273. "msg": "场景名称不存在",
  274. }
  275. c.ServeJSON()
  276. return
  277. }
  278. var tx = models.DB.Begin()
  279. recognitionRecord := &models.RecognitionRecord{
  280. OriginalImageUrl: requestData.Image,
  281. RecognitionImageUrl: annotatedImageURL,
  282. UserID: user_id,
  283. Labels: removeDuplicateLabels(yoloResp.Labels), // 存储YOLO识别的标签(去重)
  284. Title: scene.SceneName,
  285. TagType: requestData.SceneName,
  286. SecondScene: "", // 默认空字符串
  287. ThirdScene: "", // 默认空字符串
  288. }
  289. if err := tx.Create(recognitionRecord).Error; err != nil {
  290. tx.Rollback()
  291. c.Data["json"] = map[string]interface{}{
  292. "statusCode": 500,
  293. "msg": "识别失败: " + err.Error(),
  294. }
  295. c.ServeJSON()
  296. return
  297. }
  298. //获取labels对应的二级场景和三级场景
  299. var thirdSceneNames []string
  300. // 判断是否为高速公路场景
  301. isHighwayScene := strings.Contains(scene.SceneName, "运营高速公路")
  302. //隧道和简支梁
  303. isTunnelScene := strings.Contains(scene.SceneName, "隧道")
  304. isSimplySupportedBeamScene := strings.Contains(scene.SceneName, "简支梁")
  305. fmt.Println("yoloResp.Labels", yoloResp.Labels)
  306. for _, label := range yoloResp.Labels {
  307. // 处理标签:对于高速公路场景,需要去掉前缀
  308. processedLabel := label
  309. if isHighwayScene || isTunnelScene || isSimplySupportedBeamScene {
  310. processedLabel = processHighwayLabel(label)
  311. }
  312. var secondScene models.SecondScene
  313. models.DB.Where("second_scene_name = ? and is_deleted = ?", processedLabel, 0).First(&secondScene)
  314. if secondScene.ID != 0 {
  315. tx.Create(&models.RecognitionRecordSecondScene{
  316. RecognitionRecordID: int(recognitionRecord.ID),
  317. SecondSceneID: int(secondScene.ID),
  318. })
  319. // 通过secondScene.ID查询ThirdScene
  320. var thirdScene []models.ThirdScene
  321. models.DB.Where("second_scene_id = ? and is_deleted = ?", secondScene.ID, 0).Find(&thirdScene)
  322. if len(thirdScene) > 0 {
  323. for _, thirdScene := range thirdScene {
  324. thirdSceneNames = append(thirdSceneNames, thirdScene.ThirdSceneName)
  325. }
  326. }
  327. }
  328. }
  329. //对thirdSceneNames去重
  330. thirdSceneNames = removeDuplicates(thirdSceneNames)
  331. // 将三级场景名称数组更新到recognitionRecord的Description
  332. if len(thirdSceneNames) > 0 {
  333. // 将数组转换为空格分隔的字符串
  334. description := strings.Join(thirdSceneNames, " ")
  335. tx.Model(recognitionRecord).Update("Description", description)
  336. }
  337. tx.Commit()
  338. fmt.Println("thirdSceneNames", thirdSceneNames)
  339. // 返回成功响应
  340. c.Data["json"] = HazardResponse{
  341. Code: 200,
  342. Msg: "识别成功",
  343. Data: map[string]interface{}{
  344. "scene_name": requestData.SceneName,
  345. "total_detections": len(detectionResults),
  346. "detections": detectionResults,
  347. "model_type": yoloResp.ModelType,
  348. "original_image": requestData.Image,
  349. "annotated_image": annotatedImageURL, //前端用这个预链接渲染
  350. "labels": strings.Join(yoloResp.Labels, ", "),
  351. "third_scenes": thirdSceneNames, //三级场景名称数组
  352. },
  353. }
  354. c.ServeJSON()
  355. }
  356. // min 返回两个整数中的较小值
  357. func min(a, b int) int {
  358. if a < b {
  359. return a
  360. }
  361. return b
  362. }
  363. // decodePNGImage 专门解码PNG图片
  364. func decodePNGImage(imageData []byte) (image.Image, error) {
  365. // 直接使用PNG解码器
  366. img, err := png.Decode(bytes.NewReader(imageData))
  367. if err != nil {
  368. return nil, fmt.Errorf("PNG解码失败: %v", err)
  369. }
  370. return img, nil
  371. }
  372. // isImageContentType 检查Content-Type是否是图片类型
  373. func isImageContentType(contentType string) bool {
  374. imageTypes := []string{
  375. "image/jpeg",
  376. "image/jpg",
  377. "image/png",
  378. "image/gif",
  379. "image/bmp",
  380. "image/webp",
  381. "image/tiff",
  382. }
  383. for _, imgType := range imageTypes {
  384. if contentType == imgType {
  385. return true
  386. }
  387. }
  388. return false
  389. }
  390. // downloadImageFromOSS 从OSS链接下载图片(支持代理URL)
  391. func downloadImageFromOSS(imageURL string) ([]byte, error) {
  392. fmt.Printf("开始下载图片: %s\n", imageURL)
  393. // 检查是否是代理URL,如果是则直接使用代理接口
  394. if strings.Contains(imageURL, "/apiv1/oss/parse/?url=") {
  395. fmt.Printf("检测到代理URL,直接使用代理接口\n")
  396. return downloadImageFromProxy(imageURL)
  397. }
  398. // 原始OSS URL的处理
  399. client := &http.Client{
  400. Timeout: 30 * time.Second,
  401. }
  402. resp, err := client.Get(imageURL)
  403. if err != nil {
  404. return nil, fmt.Errorf("下载图片失败: %v", err)
  405. }
  406. defer resp.Body.Close()
  407. fmt.Printf("下载响应状态码: %d\n", resp.StatusCode)
  408. fmt.Printf("响应头: %+v\n", resp.Header)
  409. if resp.StatusCode != http.StatusOK {
  410. return nil, fmt.Errorf("下载图片失败,状态码: %d", resp.StatusCode)
  411. }
  412. imageData, err := io.ReadAll(resp.Body)
  413. if err != nil {
  414. return nil, fmt.Errorf("读取图片数据失败: %v", err)
  415. }
  416. fmt.Printf("图片下载完成,大小: %d 字节\n", len(imageData))
  417. // 检查Content-Type
  418. contentType := resp.Header.Get("Content-Type")
  419. if contentType != "" {
  420. fmt.Printf("图片Content-Type: %s\n", contentType)
  421. // 检查是否是图片类型
  422. if !isImageContentType(contentType) {
  423. fmt.Printf("警告: Content-Type不是图片类型: %s\n", contentType)
  424. }
  425. }
  426. return imageData, nil
  427. }
  428. // downloadImageFromProxy 通过代理接口下载图片
  429. func downloadImageFromProxy(proxyURL string) ([]byte, error) {
  430. fmt.Printf("通过代理接口下载图片: %s\n", proxyURL)
  431. // 从代理URL中提取原始OSS URL
  432. // 格式: /apiv1/oss/parse/?url=http://172.16.17.52:8060/...
  433. parsedURL, err := url.Parse(proxyURL)
  434. if err != nil {
  435. return nil, fmt.Errorf("解析代理URL失败: %v", err)
  436. }
  437. // 获取url参数
  438. originalURL := parsedURL.Query().Get("url")
  439. if originalURL == "" {
  440. return nil, fmt.Errorf("代理URL中缺少原始URL参数")
  441. }
  442. fmt.Printf("从代理URL提取的原始URL: %s\n", originalURL)
  443. // 直接使用原始URL下载图片
  444. client := &http.Client{
  445. Timeout: 30 * time.Second,
  446. }
  447. resp, err := client.Get(originalURL)
  448. if err != nil {
  449. return nil, fmt.Errorf("通过原始URL下载图片失败: %v", err)
  450. }
  451. defer resp.Body.Close()
  452. fmt.Printf("原始URL下载响应状态码: %d\n", resp.StatusCode)
  453. if resp.StatusCode != http.StatusOK {
  454. return nil, fmt.Errorf("原始URL下载图片失败,状态码: %d", resp.StatusCode)
  455. }
  456. imageData, err := io.ReadAll(resp.Body)
  457. if err != nil {
  458. return nil, fmt.Errorf("读取原始URL图片数据失败: %v", err)
  459. }
  460. fmt.Printf("原始URL图片下载完成,大小: %d 字节\n", len(imageData))
  461. // 检查Content-Type
  462. contentType := resp.Header.Get("Content-Type")
  463. if contentType != "" {
  464. fmt.Printf("原始URL图片Content-Type: %s\n", contentType)
  465. // 检查是否是图片类型
  466. if !isImageContentType(contentType) {
  467. fmt.Printf("警告: 原始URL Content-Type不是图片类型: %s\n", contentType)
  468. }
  469. }
  470. return imageData, nil
  471. }
  472. // drawBoundingBox 在图像上绘制边界框和标签
  473. func drawBoundingBox(img image.Image, boxes [][]float64, labels []string, scores []float64, username, account, date string) image.Image {
  474. // 创建可绘制的图像副本
  475. bounds := img.Bounds()
  476. drawableImg := image.NewRGBA(bounds)
  477. draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
  478. // 在左上角添加logo图片
  479. drawableImg = addLogoToImage(drawableImg)
  480. // 添加文字水印 - 传递动态参数
  481. drawableImg = addTextWatermark(drawableImg, username, account, date)
  482. // 红色边界框
  483. red := image.NewUniform(color.RGBA{255, 0, 0, 255})
  484. for _, box := range boxes {
  485. if len(box) >= 4 {
  486. x1, y1, x2, y2 := int(box[0]), int(box[1]), int(box[2]), int(box[3])
  487. // 绘制边界框
  488. drawRect(drawableImg, x1, y1, x2, y2, red)
  489. }
  490. }
  491. return drawableImg
  492. }
  493. // addLogoToImage 在图片左上角添加logo
  494. func addLogoToImage(img *image.RGBA) *image.RGBA {
  495. // 读取本地logo图片
  496. logoPath := "static/image/1.png"
  497. logoFile, err := os.Open(logoPath)
  498. if err != nil {
  499. fmt.Printf("无法打开logo文件: %v\n", err)
  500. return img // 如果无法打开logo,返回原图
  501. }
  502. defer logoFile.Close()
  503. // 解码logo图片
  504. logoImg, err := png.Decode(logoFile)
  505. if err != nil {
  506. fmt.Printf("解码logo图片失败: %v\n", err)
  507. return img // 如果解码失败,返回原图
  508. }
  509. // 获取logo图片尺寸
  510. logoBounds := logoImg.Bounds()
  511. originalWidth := logoBounds.Dx()
  512. originalHeight := logoBounds.Dy()
  513. // 缩小两倍
  514. logoWidth := originalWidth / 2
  515. logoHeight := originalHeight / 2
  516. // 设置logo在左上角的位置(留一些边距)
  517. margin := 10
  518. logoX := margin
  519. logoY := margin
  520. // 确保logo不会超出图片边界
  521. imgBounds := img.Bounds()
  522. if logoX+logoWidth > imgBounds.Max.X {
  523. logoX = imgBounds.Max.X - logoWidth - margin
  524. if logoX < 0 {
  525. logoX = 0
  526. }
  527. }
  528. if logoY+logoHeight > imgBounds.Max.Y {
  529. logoY = imgBounds.Max.Y - logoHeight - margin
  530. if logoY < 0 {
  531. logoY = 0
  532. }
  533. }
  534. // 创建缩放后的logo图像
  535. scaledLogo := image.NewRGBA(image.Rect(0, 0, logoWidth, logoHeight))
  536. // 使用简单的最近邻缩放算法
  537. for y := 0; y < logoHeight; y++ {
  538. for x := 0; x < logoWidth; x++ {
  539. // 计算原始图像中的对应位置
  540. srcX := x * 2
  541. srcY := y * 2
  542. // 确保不超出原始图像边界
  543. if srcX < originalWidth && srcY < originalHeight {
  544. scaledLogo.Set(x, y, logoImg.At(srcX, srcY))
  545. }
  546. }
  547. }
  548. // 将缩放后的logo绘制到图片上
  549. logoRect := image.Rect(logoX, logoY, logoX+logoWidth, logoY+logoHeight)
  550. draw.Draw(img, logoRect, scaledLogo, image.Point{}, draw.Over)
  551. fmt.Printf("成功在位置(%d,%d)添加logo,原始尺寸: %dx%d,缩放后尺寸: %dx%d\n", logoX, logoY, originalWidth, originalHeight, logoWidth, logoHeight)
  552. return img
  553. }
  554. // drawRect 绘制矩形
  555. func drawRect(img *image.RGBA, x1, y1, x2, y2 int, color *image.Uniform) {
  556. bounds := img.Bounds()
  557. // 确保坐标在图像边界内
  558. if x1 < bounds.Min.X {
  559. x1 = bounds.Min.X
  560. }
  561. if y1 < bounds.Min.Y {
  562. y1 = bounds.Min.Y
  563. }
  564. if x2 >= bounds.Max.X {
  565. x2 = bounds.Max.X - 1
  566. }
  567. if y2 >= bounds.Max.Y {
  568. y2 = bounds.Max.Y - 1
  569. }
  570. // 设置线条粗细(像素数)
  571. lineThickness := 6
  572. // 绘制上边(粗线)
  573. for i := 0; i < lineThickness; i++ {
  574. y := y1 + i
  575. if y >= bounds.Max.Y {
  576. break
  577. }
  578. for x := x1; x <= x2; x++ {
  579. img.Set(x, y, color)
  580. }
  581. }
  582. // 绘制下边(粗线)
  583. for i := 0; i < lineThickness; i++ {
  584. y := y2 - i
  585. if y < bounds.Min.Y {
  586. break
  587. }
  588. for x := x1; x <= x2; x++ {
  589. img.Set(x, y, color)
  590. }
  591. }
  592. // 绘制左边(粗线)
  593. for i := 0; i < lineThickness; i++ {
  594. x := x1 + i
  595. if x >= bounds.Max.X {
  596. break
  597. }
  598. for y := y1; y <= y2; y++ {
  599. img.Set(x, y, color)
  600. }
  601. }
  602. // 绘制右边(粗线)
  603. for i := 0; i < lineThickness; i++ {
  604. x := x2 - i
  605. if x < bounds.Min.X {
  606. break
  607. }
  608. for y := y1; y <= y2; y++ {
  609. img.Set(x, y, color)
  610. }
  611. }
  612. }
  613. // removeDuplicates 去除字符串数组中的重复元素
  614. func removeDuplicates(strs []string) []string {
  615. keys := make(map[string]bool)
  616. var result []string
  617. for _, str := range strs {
  618. if !keys[str] {
  619. keys[str] = true
  620. result = append(result, str)
  621. }
  622. }
  623. return result
  624. }
  625. // OSS配置信息 - 使用shudaooss.go中的配置
  626. // uploadImageToOSS 上传图片数据到OSS并返回预签名URL
  627. func uploadImageToOSS(imageData []byte, fileName string) (string, error) {
  628. // 压缩图片
  629. if EnableImageCompression {
  630. fmt.Printf("开始压缩标注图片到200KB以下...\n")
  631. compressedBytes, err := compressImage(imageData, MaxImageWidth, MaxImageHeight, 0)
  632. if err != nil {
  633. fmt.Printf("图片压缩失败,使用原始图片: %v\n", err)
  634. // 压缩失败时使用原始图片
  635. } else {
  636. fmt.Printf("标注图片压缩完成,最终大小: %.2f KB\n", float64(len(compressedBytes))/1024)
  637. imageData = compressedBytes
  638. }
  639. } else {
  640. fmt.Printf("图片压缩已禁用,使用原始图片\n")
  641. }
  642. // 获取S3会话
  643. s3Config := &aws.Config{
  644. Credentials: credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
  645. Endpoint: aws.String(ossEndpoint),
  646. Region: aws.String(ossRegion),
  647. S3ForcePathStyle: aws.Bool(true),
  648. }
  649. sess, err := session.NewSession(s3Config)
  650. if err != nil {
  651. return "", fmt.Errorf("创建S3会话失败: %v", err)
  652. }
  653. // 创建S3服务
  654. s3Client := s3.New(sess)
  655. // 上传图片到S3
  656. _, err = s3Client.PutObject(&s3.PutObjectInput{
  657. Bucket: aws.String(ossBucket),
  658. Key: aws.String(fileName),
  659. Body: aws.ReadSeekCloser(strings.NewReader(string(imageData))),
  660. ContentType: aws.String("image/jpeg"),
  661. ACL: aws.String("public-read"),
  662. })
  663. if err != nil {
  664. return "", fmt.Errorf("上传图片到OSS失败: %v", err)
  665. }
  666. // // 生成预签名URL(24小时有效期)
  667. // req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{
  668. // Bucket: aws.String(ossBucket),
  669. // Key: aws.String(fileName),
  670. // })
  671. // presignedURL, err := req.Presign(24 * time.Hour)
  672. presignedURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
  673. // 使用代理接口包装URL,前端需要显示图片
  674. proxyURL := utils.GetProxyURL(presignedURL)
  675. if err != nil {
  676. return "", fmt.Errorf("生成预签名URL失败: %v", err)
  677. }
  678. return proxyURL, nil
  679. }
  680. // TestWatermarkFunction 测试文字水印功能的简单函数
  681. func TestWatermarkFunction() {
  682. fmt.Println("开始测试文字水印功能...")
  683. // 读取测试图片
  684. imagePath := "static/image/1.png"
  685. imageFile, err := os.Open(imagePath)
  686. if err != nil {
  687. fmt.Printf("无法打开测试图片: %v\n", err)
  688. return
  689. }
  690. defer imageFile.Close()
  691. // 解码图片
  692. img, err := png.Decode(imageFile)
  693. if err != nil {
  694. fmt.Printf("解码测试图片失败: %v\n", err)
  695. return
  696. }
  697. fmt.Printf("成功读取测试图片,尺寸: %dx%d\n", img.Bounds().Dx(), img.Bounds().Dy())
  698. // 转换为可绘制的RGBA图像
  699. bounds := img.Bounds()
  700. drawableImg := image.NewRGBA(bounds)
  701. draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
  702. // 添加logo
  703. fmt.Println("添加logo...")
  704. drawableImg = addLogoToImage(drawableImg)
  705. // 添加文字水印
  706. fmt.Println("添加文字水印...")
  707. drawableImg = addTextWatermark(drawableImg, "测试用户", "1234", "2025/01/15")
  708. // 将处理后的图片保存到static/image目录
  709. outputPath := "static/image/test_output.jpg"
  710. outputFile, err := os.Create(outputPath)
  711. if err != nil {
  712. fmt.Printf("创建输出文件失败: %v\n", err)
  713. return
  714. }
  715. defer outputFile.Close()
  716. // 编码为JPEG
  717. if err := jpeg.Encode(outputFile, drawableImg, &jpeg.Options{Quality: 95}); err != nil {
  718. fmt.Printf("编码图片失败: %v\n", err)
  719. return
  720. }
  721. fmt.Printf("测试完成!处理后的图片已保存到: %s\n", outputPath)
  722. fmt.Println("请查看图片效果,确认logo和文字水印是否正确显示")
  723. }
  724. // addTextWatermark 使用gg库添加45度角的文字水印(改进版)
  725. func addTextWatermark(img *image.RGBA, username, account, date string) *image.RGBA {
  726. // 水印文本 - 使用传入的动态数据
  727. watermarks := []string{username, account, date}
  728. // 获取图片尺寸
  729. bounds := img.Bounds()
  730. width, height := bounds.Max.X, bounds.Max.Y
  731. // 创建一个新的绘图上下文
  732. dc := gg.NewContextForImage(img)
  733. // 设置字体和颜色
  734. fontSize := 20.0 // 从30调整为20,字体更小
  735. // 尝试多种字体加载方案
  736. fontLoaded := false
  737. // 方案1:优先使用项目中的字体文件
  738. localFontPaths := []string{
  739. "static/font/AlibabaPuHuiTi-3-55-Regular.ttf", // 阿里巴巴普惠体(项目字体)
  740. }
  741. for _, fontPath := range localFontPaths {
  742. if err := dc.LoadFontFace(fontPath, fontSize); err == nil {
  743. fmt.Printf("成功加载本地字体: %s\n", fontPath)
  744. fontLoaded = true
  745. break
  746. }
  747. }
  748. // 方案2:如果本地字体失败,尝试使用内置字体
  749. if !fontLoaded {
  750. fmt.Printf("本地字体加载失败,尝试使用内置字体\n")
  751. // 使用空字符串加载默认字体,这在大多数情况下都能工作
  752. if err := dc.LoadFontFace("", fontSize); err == nil {
  753. fmt.Printf("成功加载内置默认字体\n")
  754. fontLoaded = true
  755. } else {
  756. fmt.Printf("内置字体加载失败: %v\n", err)
  757. }
  758. }
  759. // 方案3:如果所有字体都失败,使用像素绘制(备用方案)
  760. if !fontLoaded {
  761. fmt.Printf("所有字体加载失败,将使用像素绘制方案\n")
  762. // 这里可以添加像素绘制的备用方案
  763. }
  764. // 设置更深的颜色(从灰色改为深灰色)
  765. dc.SetColor(color.RGBA{R: 120, G: 120, B: 120, A: 120}) // 深灰色,更不透明
  766. // 设置旋转角度
  767. angle := gg.Radians(-45) // 设置为-45度
  768. // 循环绘制水印
  769. textWidthEstimate := 150.0 // 估算文本宽度(字体变小,间距也相应调整)
  770. textHeightEstimate := 80.0 // 估算文本高度,作为行间距(字体变小,间距也相应调整)
  771. // 旋转整个画布来绘制
  772. dc.Rotate(angle)
  773. // 计算旋转后的绘制范围
  774. // 为了覆盖整个图片,我们需要在更大的范围内绘制
  775. // 简单起见,我们循环一个足够大的范围
  776. // 这里的x,y是旋转后的坐标
  777. for y := -float64(height); y < float64(height)*1.5; y += textHeightEstimate {
  778. for x := -float64(width); x < float64(width)*1.5; x += textWidthEstimate {
  779. // 每排使用相同的内容:根据y坐标确定使用哪个文本
  780. rowIndex := int(y/textHeightEstimate) % len(watermarks)
  781. if rowIndex < 0 {
  782. rowIndex = (rowIndex%len(watermarks) + len(watermarks)) % len(watermarks)
  783. }
  784. text := watermarks[rowIndex]
  785. dc.DrawString(text, x, y)
  786. }
  787. }
  788. fmt.Printf("成功添加45度角文字水印(改进版:颜色更深,每排内容相同)\n")
  789. return dc.Image().(*image.RGBA)
  790. }
  791. // 前端传值步骤、json文件、封面图过来
  792. type SaveStepRequest struct {
  793. AIConversationID uint64 `json:"ai_conversation_id"`
  794. Step int `json:"step"`
  795. PPTJsonUrl string `json:"ppt_json_url"`
  796. CoverImage string `json:"cover_image"`
  797. PPTJsonContent string `json:"ppt_json_content"`
  798. }
  799. func (c *HazardController) SaveStep() {
  800. var requestData SaveStepRequest
  801. if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
  802. c.Data["json"] = map[string]interface{}{
  803. "statusCode": 400,
  804. "msg": "请求数据解析失败",
  805. }
  806. c.ServeJSON()
  807. return
  808. }
  809. tx := models.DB.Begin()
  810. //更新到ai_conversation表
  811. if err := tx.Model(&models.AIConversation{}).Where("id = ?", requestData.AIConversationID).Updates(map[string]interface{}{
  812. "step": requestData.Step,
  813. "ppt_json_url": requestData.PPTJsonUrl,
  814. "cover_image": requestData.CoverImage,
  815. "ppt_json_content": requestData.PPTJsonContent,
  816. }).Error; err != nil {
  817. tx.Rollback()
  818. c.Data["json"] = map[string]interface{}{
  819. "statusCode": 500,
  820. "msg": "更新步骤失败",
  821. }
  822. c.ServeJSON()
  823. return
  824. }
  825. tx.Commit()
  826. c.Data["json"] = map[string]interface{}{
  827. "statusCode": 200,
  828. "msg": "success",
  829. }
  830. c.ServeJSON()
  831. }
  832. // removeDuplicateLabels 去除重复的标签并返回逗号分隔的字符串
  833. func removeDuplicateLabels(labels []string) string {
  834. if len(labels) == 0 {
  835. return ""
  836. }
  837. // 使用map来去重
  838. labelMap := make(map[string]bool)
  839. var uniqueLabels []string
  840. for _, label := range labels {
  841. // 去除前后空格
  842. label = strings.TrimSpace(label)
  843. if label != "" && !labelMap[label] {
  844. labelMap[label] = true
  845. uniqueLabels = append(uniqueLabels, label)
  846. }
  847. }
  848. return strings.Join(uniqueLabels, ", ")
  849. }
  850. // processHighwayLabel 处理高速公路场景的标签
  851. // 例如:"绿化_路侧植株_路侧植株" -> "路侧植株_路侧植株"
  852. // 去掉前缀(第一个下划线及其之前的内容)
  853. func processHighwayLabel(label string) string {
  854. // 查找第一个下划线的位置
  855. underscoreIndex := strings.Index(label, "_")
  856. if underscoreIndex == -1 {
  857. // 如果没有下划线,直接返回原标签
  858. return label
  859. }
  860. // 返回从下划线之后的内容
  861. return label[underscoreIndex+1:]
  862. }