hazard.go 26 KB

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