image-analysis.service.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  1. import { Injectable } from '@angular/core';
  2. import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
  3. const Parse = FmodeParse.with('nova');
  4. /**
  5. * 图片分析结果接口
  6. */
  7. export interface ImageAnalysisResult {
  8. // 基础信息
  9. fileName: string;
  10. fileSize: number;
  11. dimensions: {
  12. width: number;
  13. height: number;
  14. };
  15. // 质量评估
  16. quality: {
  17. score: number; // 0-100分
  18. level: 'low' | 'medium' | 'high' | 'ultra'; // 低、中、高、超高
  19. sharpness: number; // 清晰度 0-100
  20. brightness: number; // 亮度 0-100
  21. contrast: number; // 对比度 0-100
  22. detailLevel: 'minimal' | 'basic' | 'detailed' | 'ultra_detailed'; // 🔥 内容精细程度
  23. pixelDensity: 'low' | 'medium' | 'high' | 'ultra_high'; // 🔥 像素密度等级
  24. textureQuality: number; // 🔥 纹理质量 0-100
  25. colorDepth: number; // 🔥 色彩深度 0-100
  26. };
  27. // 内容分析
  28. content: {
  29. category: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' | 'unknown';
  30. confidence: number; // 置信度 0-100
  31. description: string; // 内容描述
  32. tags: string[]; // 标签
  33. isArchitectural: boolean; // 是否为建筑相关
  34. hasInterior: boolean; // 是否包含室内场景
  35. hasFurniture: boolean; // 是否包含家具
  36. hasLighting: boolean; // 是否有灯光效果
  37. hasColor?: boolean; // 🔥 是否有色彩(非纯白、非灰度)
  38. hasTexture?: boolean; // 🔥 是否有材质纹理
  39. };
  40. // 技术参数
  41. technical: {
  42. format: string; // 文件格式
  43. colorSpace: string; // 色彩空间
  44. dpi: number; // 分辨率
  45. aspectRatio: string; // 宽高比
  46. megapixels: number; // 像素数(百万)
  47. };
  48. // 建议分类
  49. suggestedStage: 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
  50. suggestedReason: string; // 分类原因
  51. // 设计分析维度(新增)
  52. design?: {
  53. style: string; // 风格:现代简约、欧式、中式等
  54. atmosphere: string; // 氛围营造方式
  55. material: string; // 材质分析
  56. texture: string; // 纹理特征
  57. quality: string; // 质感描述
  58. form: string; // 形体特征
  59. structure: string; // 空间结构
  60. colorComposition: string; // 色彩组成
  61. };
  62. // 色彩解析报告(新增)
  63. colorReport?: {
  64. brightness: string; // 明度:低长调/高长调分析
  65. hue: string; // 色相:色彩种类
  66. saturation: string; // 饱和度:低/中/高
  67. openness: string; // 色彩开放度:根据色相种类划分
  68. extractedColors: string[]; // 提取的基础颜色
  69. organizedColors: Array<{ color: string; role: string; percentage: number }>; // 组织颜色(主次)
  70. expandedColors: string[]; // 拓展颜色(配色)
  71. harmonizedColors: string[]; // 调和颜色(配色)
  72. };
  73. // 风格元素分析(新增)
  74. styleElements?: {
  75. styleKeywords: string[]; // 风格关键词(如:新古典主义、现代轻奢、艺术装饰等)
  76. };
  77. // 色彩搭配分析(新增)
  78. colorScheme?: {
  79. primaryColors: string[]; // 主色调
  80. secondaryColors: string[]; // 辅助色
  81. accentColors: string[]; // 点缀色
  82. };
  83. // 材质分析(新增)
  84. materialAnalysis?: {
  85. materials: string[]; // 识别的材质(大理石、玻璃、布、金属等)
  86. };
  87. // 布局特征分析(新增)
  88. layoutFeatures?: {
  89. features: string[]; // 布局特征(对称式布局、多区域采光、开放式空间等)
  90. };
  91. // 空间氛围分析(新增)
  92. atmosphereAnalysis?: {
  93. atmosphere: string; // 空间氛围描述
  94. };
  95. // AI分析时间
  96. analysisTime: number; // 分析耗时(毫秒)
  97. analysisDate: string; // 分析时间
  98. }
  99. /**
  100. * 图片分析服务
  101. * 基于豆包1.6模型进行图片内容识别和质量评估
  102. */
  103. @Injectable({
  104. providedIn: 'root'
  105. })
  106. export class ImageAnalysisService {
  107. // 使用豆包1.6模型
  108. private readonly MODEL = 'fmode-1.6-cn';
  109. constructor() {}
  110. /**
  111. * 动态加载 completionJSON,避免编译路径不兼容
  112. */
  113. private async callCompletionJSON(
  114. prompt: string,
  115. outputSchema: string,
  116. onStream?: (content: any) => void,
  117. retry: number = 2,
  118. options: any = {}
  119. ): Promise<any> {
  120. const mod = await import('fmode-ng/core/agent/chat/completion');
  121. const completionJSON = (mod as any).completionJSON as (
  122. prompt: string,
  123. output: string,
  124. onStream?: (content: any) => void,
  125. retry?: number,
  126. options?: any
  127. ) => Promise<any>;
  128. return completionJSON(prompt, outputSchema, onStream, retry, options);
  129. }
  130. /**
  131. * 分析单张图片
  132. */
  133. async analyzeImage(
  134. imageUrl: string,
  135. file: File,
  136. onProgress?: (progress: string) => void
  137. ): Promise<ImageAnalysisResult> {
  138. const startTime = Date.now();
  139. try {
  140. onProgress?.('正在分析图片内容...');
  141. // 获取图片基础信息
  142. const basicInfo = await this.getImageBasicInfo(file);
  143. onProgress?.('正在进行AI内容识别...');
  144. // 使用豆包1.6进行内容分析
  145. const contentAnalysis = await this.analyzeImageContent(imageUrl);
  146. onProgress?.('正在评估图片质量...');
  147. // 质量评估
  148. const qualityAnalysis = await this.analyzeImageQuality(imageUrl, basicInfo);
  149. onProgress?.('正在生成分析报告...');
  150. // 综合分析结果
  151. const result: ImageAnalysisResult = {
  152. fileName: file.name,
  153. fileSize: file.size,
  154. dimensions: basicInfo.dimensions,
  155. quality: qualityAnalysis,
  156. content: contentAnalysis,
  157. technical: {
  158. format: file.type,
  159. colorSpace: 'sRGB', // 默认值,实际可通过更深入分析获得
  160. dpi: basicInfo.dpi || 72,
  161. aspectRatio: this.calculateAspectRatio(basicInfo.dimensions.width, basicInfo.dimensions.height),
  162. megapixels: Math.round((basicInfo.dimensions.width * basicInfo.dimensions.height) / 1000000 * 100) / 100
  163. },
  164. suggestedStage: this.determineSuggestedStage(contentAnalysis, qualityAnalysis),
  165. suggestedReason: this.generateSuggestionReason(contentAnalysis, qualityAnalysis),
  166. analysisTime: Date.now() - startTime,
  167. analysisDate: new Date().toISOString()
  168. };
  169. onProgress?.('分析完成');
  170. return result;
  171. } catch (error) {
  172. console.error('图片分析失败:', error);
  173. throw new Error('图片分析失败: ' + (error as Error).message);
  174. }
  175. }
  176. /**
  177. * 获取图片基础信息
  178. */
  179. private async getImageBasicInfo(file: File): Promise<{
  180. dimensions: { width: number; height: number };
  181. dpi?: number;
  182. }> {
  183. return new Promise((resolve, reject) => {
  184. const img = new Image();
  185. img.onload = () => {
  186. resolve({
  187. dimensions: {
  188. width: img.naturalWidth,
  189. height: img.naturalHeight
  190. },
  191. dpi: 72 // 默认DPI,实际项目中可以通过EXIF数据获取
  192. });
  193. };
  194. img.onerror = () => reject(new Error('无法加载图片'));
  195. img.src = URL.createObjectURL(file);
  196. });
  197. }
  198. /**
  199. * 使用豆包1.6分析图片内容
  200. */
  201. private async analyzeImageContent(imageUrl: string): Promise<ImageAnalysisResult['content']> {
  202. const prompt = `请分析这张室内设计相关的图片,并按以下JSON格式输出分析结果:
  203. {
  204. "category": "图片类别(white_model/soft_decor/rendering/post_process/unknown)",
  205. "confidence": "置信度(0-100)",
  206. "description": "详细的内容描述",
  207. "tags": ["标签1", "标签2", "标签3"],
  208. "isArchitectural": "是否为建筑相关(true/false)",
  209. "hasInterior": "是否包含室内场景(true/false)",
  210. "hasFurniture": "是否包含家具(true/false)",
  211. "hasLighting": "是否有灯光效果(true/false)",
  212. "hasColor": "是否有色彩(非纯白、非灰度)(true/false)",
  213. "hasTexture": "是否有材质纹理(true/false)"
  214. }
  215. 分类标准:
  216. - white_model: 白模、线框图、基础建模、结构图(特征:纯白色或灰色、无色彩、无材质、无家具、无灯光)
  217. - soft_decor: 软装搭配、家具配置、装饰品、材质贴图(特征:有家具、有色彩、有材质、但灯光不突出)
  218. - rendering: 渲染图、效果图、光影表现、材质细节(特征:有灯光效果、有色彩、有材质、质量较高)
  219. - post_process: 后期处理、色彩调整、特效、最终成品(特征:完整场景、精致色彩、专业质量)
  220. 重要判断依据:
  221. 1. 如果图片是纯白色或灰色草图,无任何色彩 → 白模
  222. 2. 如果图片有丰富色彩和材质 → 不是白模
  223. 3. 如果图片有灯光效果 → 渲染或后期
  224. 4. 如果图片有家具但无灯光 → 软装
  225. 要求:
  226. 1. 准确识别图片中的设计元素
  227. 2. 特别注意图片是否有色彩(区分白模和渲染图的关键)
  228. 3. 判断图片的制作阶段和用途
  229. 4. 提取关键的视觉特征`;
  230. const output = `{
  231. "category": "white_model",
  232. "confidence": 85,
  233. "description": "室内空间白模图,显示了基础的空间结构",
  234. "tags": ["白模", "室内", "结构"],
  235. "isArchitectural": true,
  236. "hasInterior": true,
  237. "hasFurniture": false,
  238. "hasLighting": false,
  239. "hasColor": false,
  240. "hasTexture": false
  241. }`;
  242. try {
  243. const result = await this.callCompletionJSON(
  244. prompt,
  245. output,
  246. (content) => {
  247. console.log('AI分析进度:', content?.length || 0);
  248. },
  249. 2,
  250. {
  251. model: this.MODEL,
  252. vision: true,
  253. images: [imageUrl]
  254. }
  255. );
  256. return result || {
  257. category: 'unknown',
  258. confidence: 0,
  259. description: '无法识别图片内容',
  260. tags: [],
  261. isArchitectural: false,
  262. hasInterior: false,
  263. hasFurniture: false,
  264. hasLighting: false
  265. };
  266. } catch (error) {
  267. console.error('内容分析失败:', error);
  268. return {
  269. category: 'unknown',
  270. confidence: 0,
  271. description: '内容分析失败',
  272. tags: [],
  273. isArchitectural: false,
  274. hasInterior: false,
  275. hasFurniture: false,
  276. hasLighting: false
  277. };
  278. }
  279. }
  280. /**
  281. * 🔥 分析图片质量(增强版:包含精细度和像素密度)
  282. */
  283. private async analyzeImageQuality(
  284. imageUrl: string,
  285. basicInfo: { dimensions: { width: number; height: number } }
  286. ): Promise<ImageAnalysisResult['quality']> {
  287. const prompt = `请分析这张室内设计图片的质量,并按以下JSON格式输出:
  288. {
  289. "score": "总体质量分数(0-100)",
  290. "level": "质量等级(low/medium/high/ultra)",
  291. "sharpness": "清晰度(0-100)",
  292. "brightness": "亮度(0-100)",
  293. "contrast": "对比度(0-100)",
  294. "textureQuality": "纹理质量(0-100)",
  295. "colorDepth": "色彩深度(0-100)"
  296. }
  297. 评估标准:
  298. - score: 综合质量评分,考虑清晰度、构图、色彩、纹理等
  299. - level: low(<60分), medium(60-75分), high(75-90分), ultra(>90分)
  300. - sharpness: 图片清晰度,是否有模糊、噪点、锯齿
  301. - brightness: 亮度是否适中,不过暗或过亮
  302. - contrast: 对比度是否合适,层次是否分明
  303. - textureQuality: 纹理质量,材质细节是否清晰(木纹、布纹、石材等)
  304. - colorDepth: 色彩深度,色彩过渡是否自然,是否有色带
  305. 请客观评估图片质量,重点关注专业室内设计图片的标准。`;
  306. const output = `{
  307. "score": 75,
  308. "level": "high",
  309. "sharpness": 80,
  310. "brightness": 70,
  311. "contrast": 75,
  312. "textureQuality": 75,
  313. "colorDepth": 80
  314. }`;
  315. try {
  316. const result = await this.callCompletionJSON(
  317. prompt,
  318. output,
  319. (content) => {
  320. console.log('🔍 质量分析进度:', content?.length || 0);
  321. },
  322. 2,
  323. {
  324. model: this.MODEL,
  325. vision: true,
  326. images: [imageUrl]
  327. }
  328. );
  329. // 🔥 结合图片分辨率和像素密度进行质量调整
  330. const resolutionScore = this.calculateResolutionScore(basicInfo.dimensions);
  331. const pixelDensity = this.calculatePixelDensity(basicInfo.dimensions);
  332. // 🔥 评估内容精细程度
  333. const detailLevel = await this.evaluateDetailLevel(imageUrl, basicInfo.dimensions);
  334. // 🔥 综合评分:AI评分(40%) + 分辨率(30%) + 纹理质量(20%) + 色彩深度(10%)
  335. const adjustedScore = Math.round(
  336. result.score * 0.4 +
  337. resolutionScore * 0.3 +
  338. (result.textureQuality || 50) * 0.2 +
  339. (result.colorDepth || 50) * 0.1
  340. );
  341. console.log('📊 质量分析结果:', {
  342. AI评分: result.score,
  343. 分辨率评分: resolutionScore,
  344. 纹理质量: result.textureQuality,
  345. 色彩深度: result.colorDepth,
  346. 综合评分: adjustedScore,
  347. 像素密度: pixelDensity,
  348. 精细程度: detailLevel
  349. });
  350. return {
  351. score: adjustedScore,
  352. level: this.getQualityLevel(adjustedScore),
  353. sharpness: result.sharpness || 50,
  354. brightness: result.brightness || 50,
  355. contrast: result.contrast || 50,
  356. detailLevel: detailLevel,
  357. pixelDensity: pixelDensity,
  358. textureQuality: result.textureQuality || 50,
  359. colorDepth: result.colorDepth || 50
  360. };
  361. } catch (error) {
  362. console.error('❌ 质量分析失败:', error);
  363. // 基于分辨率和像素的备选评估
  364. const resolutionScore = this.calculateResolutionScore(basicInfo.dimensions);
  365. const pixelDensity = this.calculatePixelDensity(basicInfo.dimensions);
  366. const megapixels = (basicInfo.dimensions.width * basicInfo.dimensions.height) / 1000000;
  367. // 根据像素推测精细程度
  368. let detailLevel: 'minimal' | 'basic' | 'detailed' | 'ultra_detailed' = 'basic';
  369. if (megapixels >= 8) detailLevel = 'ultra_detailed';
  370. else if (megapixels >= 2) detailLevel = 'detailed';
  371. else if (megapixels >= 0.9) detailLevel = 'basic';
  372. else detailLevel = 'minimal';
  373. return {
  374. score: resolutionScore,
  375. level: this.getQualityLevel(resolutionScore),
  376. sharpness: 50,
  377. brightness: 50,
  378. contrast: 50,
  379. detailLevel: detailLevel,
  380. pixelDensity: pixelDensity,
  381. textureQuality: resolutionScore * 0.8,
  382. colorDepth: resolutionScore * 0.9
  383. };
  384. }
  385. }
  386. /**
  387. * 🔥 基于分辨率和像素密度计算质量分数(更精细)
  388. */
  389. private calculateResolutionScore(dimensions: { width: number; height: number }): number {
  390. const totalPixels = dimensions.width * dimensions.height;
  391. const megapixels = totalPixels / 1000000;
  392. // 更精细的像素分级
  393. if (megapixels >= 33) return 98; // 8K (7680×4320)
  394. if (megapixels >= 24) return 96; // 6K (6144×3160)
  395. if (megapixels >= 16) return 94; // 5K (5120×2880)
  396. if (megapixels >= 8) return 92; // 4K (3840×2160)
  397. if (megapixels >= 6) return 88; // 2.5K+ (2560×2304)
  398. if (megapixels >= 4) return 84; // QHD+ (2560×1600)
  399. if (megapixels >= 2) return 78; // 1080p (1920×1080)
  400. if (megapixels >= 1) return 68; // 720p+ (1280×720)
  401. if (megapixels >= 0.5) return 55; // 中等分辨率
  402. if (megapixels >= 0.3) return 40; // 低分辨率
  403. return 25; // 极低分辨率
  404. }
  405. /**
  406. * 🔥 计算像素密度等级
  407. */
  408. private calculatePixelDensity(dimensions: { width: number; height: number }): 'low' | 'medium' | 'high' | 'ultra_high' {
  409. const totalPixels = dimensions.width * dimensions.height;
  410. const megapixels = totalPixels / 1000000;
  411. if (megapixels >= 8) return 'ultra_high'; // 4K及以上
  412. if (megapixels >= 2) return 'high'; // 1080p及以上
  413. if (megapixels >= 0.9) return 'medium'; // 720p及以上
  414. return 'low'; // 低于720p
  415. }
  416. /**
  417. * 🔥 评估内容精细程度
  418. */
  419. private async evaluateDetailLevel(
  420. imageUrl: string,
  421. dimensions: { width: number; height: number }
  422. ): Promise<'minimal' | 'basic' | 'detailed' | 'ultra_detailed'> {
  423. const prompt = `请评估这张室内设计图片的内容精细程度,并返回JSON:
  424. {
  425. "detailLevel": "精细程度(minimal/basic/detailed/ultra_detailed)",
  426. "textureQuality": "纹理质量评分(0-100)",
  427. "colorDepth": "色彩深度评分(0-100)",
  428. "reasoning": "评估理由"
  429. }
  430. 评估标准:
  431. - minimal: 极简图,只有基本轮廓,无细节纹理
  432. - basic: 基础图,有简单纹理和色彩,细节较少
  433. - detailed: 详细图,有丰富纹理、材质细节、光影效果
  434. - ultra_detailed: 超精细图,有极致纹理、真实材质、复杂光影、细微细节
  435. 重点关注:
  436. 1. 纹理细节(木纹、布纹、石材纹理等)
  437. 2. 材质表现(金属反射、玻璃透明度、布料质感等)
  438. 3. 光影效果(阴影、高光、环境光等)
  439. 4. 细微元素(装饰品细节、边角处理等)`;
  440. const output = `{
  441. "detailLevel": "detailed",
  442. "textureQuality": 75,
  443. "colorDepth": 80,
  444. "reasoning": "图片包含丰富的纹理和材质细节"
  445. }`;
  446. try {
  447. const result = await this.callCompletionJSON(
  448. prompt,
  449. output,
  450. undefined,
  451. 2,
  452. {
  453. model: this.MODEL,
  454. vision: true,
  455. images: [imageUrl]
  456. }
  457. );
  458. return result.detailLevel || 'basic';
  459. } catch (error) {
  460. console.error('精细程度评估失败:', error);
  461. // 基于分辨率的备选评估
  462. const megapixels = (dimensions.width * dimensions.height) / 1000000;
  463. if (megapixels >= 8) return 'ultra_detailed';
  464. if (megapixels >= 2) return 'detailed';
  465. if (megapixels >= 0.9) return 'basic';
  466. return 'minimal';
  467. }
  468. }
  469. /**
  470. * 获取质量等级
  471. */
  472. private getQualityLevel(score: number): 'low' | 'medium' | 'high' | 'ultra' {
  473. if (score >= 90) return 'ultra';
  474. if (score >= 75) return 'high';
  475. if (score >= 60) return 'medium';
  476. return 'low';
  477. }
  478. /**
  479. * 计算宽高比
  480. */
  481. private calculateAspectRatio(width: number, height: number): string {
  482. const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
  483. const divisor = gcd(width, height);
  484. return `${width / divisor}:${height / divisor}`;
  485. }
  486. /**
  487. * 🔥 确定建议的阶段分类(优化版:更精准的判断逻辑)
  488. */
  489. private determineSuggestedStage(
  490. content: ImageAnalysisResult['content'],
  491. quality: ImageAnalysisResult['quality']
  492. ): 'white_model' | 'soft_decor' | 'rendering' | 'post_process' {
  493. // 如果AI已经识别出明确类别且置信度高
  494. if (content.confidence > 75 && content.category !== 'unknown') {
  495. return content.category as any;
  496. }
  497. // 🔥 综合判断:像素密度 + 内容精细度 + 质量分数 + 特征
  498. const megapixels = quality.pixelDensity;
  499. const detailLevel = quality.detailLevel;
  500. const qualityScore = quality.score;
  501. const textureQuality = quality.textureQuality;
  502. console.log('🎯 阶段判断依据:', {
  503. 像素密度: megapixels,
  504. 精细程度: detailLevel,
  505. 质量分数: qualityScore,
  506. 纹理质量: textureQuality,
  507. 有家具: content.hasFurniture,
  508. 有灯光: content.hasLighting,
  509. 有色彩: content.hasColor,
  510. 有纹理: content.hasTexture
  511. });
  512. // 🔥 白模阶段:放宽条件,更准确识别白色/灰色无渲染的图片
  513. // 修复:默认值应该是false(无色彩/无纹理),而不是true
  514. const hasColor = content.hasColor === true; // 🔥 修复:只有明确有色彩才为true
  515. const hasTexture = content.hasTexture === true; // 🔥 修复:只有明确有纹理才为true
  516. // 🔥 白模判断:无装饰 + 无灯光 + 低质量 (放宽色彩和纹理条件)
  517. if (!content.hasFurniture &&
  518. !content.hasLighting &&
  519. qualityScore < 65 && // 🔥 放宽质量要求(原60提升到65)
  520. !hasColor) { // 🔥 主要看是否有色彩,纹理可以忽略
  521. console.log('✅ 判定为白模阶段:无装饰 + 无灯光 + 无色彩 + 低质量');
  522. return 'white_model';
  523. }
  524. // 🔥 如果质量极低且无装饰,也判定为白模
  525. if (qualityScore < 50 &&
  526. !content.hasFurniture &&
  527. !content.hasLighting) {
  528. console.log('✅ 判定为白模阶段:极低质量 + 无装饰');
  529. return 'white_model';
  530. }
  531. // 🔥 如果有明显色彩或灯光,绝对不是白模
  532. if (hasColor || content.hasLighting) {
  533. console.log('✅ 有色彩或灯光,不是白模,继续判断其他阶段');
  534. }
  535. // 🔥 软装阶段:有家具 + 无灯光 + 中等质量
  536. if (content.hasFurniture && !content.hasLighting &&
  537. qualityScore >= 60 && qualityScore < 80) {
  538. console.log('✅ 判定为软装阶段:有家具 + 无灯光');
  539. return 'soft_decor';
  540. }
  541. // 🔥 渲染阶段:有灯光 + 高质量 + 详细精细度
  542. if (content.hasLighting &&
  543. (detailLevel === 'detailed' || detailLevel === 'ultra_detailed') &&
  544. qualityScore >= 75 && qualityScore < 90) {
  545. console.log('✅ 判定为渲染阶段:有灯光 + 高质量 + 详细精细度');
  546. return 'rendering';
  547. }
  548. // 🔥 后期处理阶段:超高质量 + 超精细 + 高纹理质量
  549. if (qualityScore >= 90 &&
  550. detailLevel === 'ultra_detailed' &&
  551. textureQuality >= 85 &&
  552. (megapixels === 'ultra_high' || megapixels === 'high')) {
  553. console.log('✅ 判定为后期处理阶段:超高质量 + 超精细');
  554. return 'post_process';
  555. }
  556. // 🔥 渲染阶段:有灯光效果,即使质量不是最高
  557. if (content.hasLighting && qualityScore >= 70) {
  558. console.log('✅ 判定为渲染阶段:有灯光效果');
  559. return 'rendering';
  560. }
  561. // 🔥 软装阶段:有家具但质量一般
  562. if (content.hasFurniture && qualityScore >= 60) {
  563. console.log('✅ 判定为软装阶段:有家具');
  564. return 'soft_decor';
  565. }
  566. // 🔥 默认:根据质量分数判断(优先渲染,避免误判为白模)
  567. if (qualityScore >= 85) {
  568. console.log('✅ 默认判定为后期处理阶段:高质量');
  569. return 'post_process';
  570. } else if (qualityScore >= 65) {
  571. console.log('✅ 默认判定为渲染阶段:中高质量');
  572. return 'rendering';
  573. } else if (qualityScore >= 50) {
  574. console.log('✅ 默认判定为软装阶段:中等质量');
  575. return 'soft_decor';
  576. } else if (qualityScore >= 40) {
  577. console.log('✅ 默认判定为渲染阶段:低质量但有内容');
  578. return 'rendering'; // 🔥 即使质量低,也优先判定为渲染而非白模
  579. } else {
  580. console.log('⚠️ 默认判定为白模阶段:极低质量');
  581. return 'white_model';
  582. }
  583. }
  584. /**
  585. * 生成分类建议原因
  586. */
  587. private generateSuggestionReason(
  588. content: ImageAnalysisResult['content'],
  589. quality: ImageAnalysisResult['quality']
  590. ): string {
  591. const reasons = [];
  592. if (content.confidence > 70) {
  593. reasons.push(`AI识别置信度${content.confidence}%`);
  594. }
  595. if (quality.score >= 90) {
  596. reasons.push('图片质量极高,适合最终展示');
  597. } else if (quality.score >= 75) {
  598. reasons.push('图片质量良好');
  599. } else if (quality.score < 60) {
  600. reasons.push('图片质量较低,可能为初期阶段');
  601. }
  602. if (content.hasLighting) {
  603. reasons.push('包含灯光效果');
  604. }
  605. if (content.hasFurniture) {
  606. reasons.push('包含家具配置');
  607. }
  608. if (!content.hasFurniture && !content.hasLighting) {
  609. reasons.push('基础结构图,无装饰元素');
  610. }
  611. return reasons.join(',') || '基于图片特征综合判断';
  612. }
  613. /**
  614. * 批量分析图片
  615. */
  616. async analyzeImages(
  617. files: { file: File; url: string }[],
  618. onProgress?: (current: number, total: number, currentFileName: string) => void
  619. ): Promise<ImageAnalysisResult[]> {
  620. const results: ImageAnalysisResult[] = [];
  621. for (let i = 0; i < files.length; i++) {
  622. const { file, url } = files[i];
  623. onProgress?.(i + 1, files.length, file.name);
  624. try {
  625. const result = await this.analyzeImage(url, file);
  626. results.push(result);
  627. } catch (error) {
  628. console.error(`分析文件 ${file.name} 失败:`, error);
  629. // 创建一个基础的错误结果
  630. results.push(this.createErrorResult(file, error as Error));
  631. }
  632. }
  633. return results;
  634. }
  635. /**
  636. * 创建错误结果
  637. */
  638. private createErrorResult(file: File, error: Error): ImageAnalysisResult {
  639. return {
  640. fileName: file.name,
  641. fileSize: file.size,
  642. dimensions: { width: 0, height: 0 },
  643. quality: {
  644. score: 0,
  645. level: 'low',
  646. sharpness: 0,
  647. brightness: 0,
  648. contrast: 0,
  649. detailLevel: 'minimal',
  650. pixelDensity: 'low',
  651. textureQuality: 0,
  652. colorDepth: 0
  653. },
  654. content: {
  655. category: 'unknown',
  656. confidence: 0,
  657. description: `分析失败: ${error.message}`,
  658. tags: [],
  659. isArchitectural: false,
  660. hasInterior: false,
  661. hasFurniture: false,
  662. hasLighting: false
  663. },
  664. technical: {
  665. format: file.type,
  666. colorSpace: 'unknown',
  667. dpi: 0,
  668. aspectRatio: '0:0',
  669. megapixels: 0
  670. },
  671. suggestedStage: 'white_model',
  672. suggestedReason: '分析失败,默认分类',
  673. analysisTime: 0,
  674. analysisDate: new Date().toISOString()
  675. };
  676. }
  677. /**
  678. * 保存分析结果到数据库
  679. */
  680. async saveAnalysisResult(
  681. projectId: string,
  682. spaceId: string,
  683. stageType: string,
  684. analysisResult: ImageAnalysisResult
  685. ): Promise<void> {
  686. try {
  687. // 查询项目
  688. const projectQuery = new Parse.Query('Project');
  689. const project = await projectQuery.get(projectId);
  690. if (!project) {
  691. throw new Error('项目不存在');
  692. }
  693. // 获取现有的date数据
  694. const dateData = project.get('date') || {};
  695. // 初始化图片分析数据结构
  696. if (!dateData.imageAnalysis) {
  697. dateData.imageAnalysis = {};
  698. }
  699. if (!dateData.imageAnalysis[spaceId]) {
  700. dateData.imageAnalysis[spaceId] = {};
  701. }
  702. if (!dateData.imageAnalysis[spaceId][stageType]) {
  703. dateData.imageAnalysis[spaceId][stageType] = [];
  704. }
  705. // 添加分析结果
  706. dateData.imageAnalysis[spaceId][stageType].push(analysisResult);
  707. // 保存到项目
  708. project.set('date', dateData);
  709. await project.save();
  710. console.log('图片分析结果已保存到项目数据库');
  711. } catch (error) {
  712. console.error('保存分析结果失败:', error);
  713. throw error;
  714. }
  715. }
  716. /**
  717. * 🔥 快速生成模拟分析结果(用于开发测试)
  718. * 根据文件名和空间名称快速生成分析结果,无需调用AI
  719. */
  720. generateMockAnalysisResult(
  721. file: File,
  722. spaceName?: string,
  723. stageName?: string
  724. ): ImageAnalysisResult {
  725. const fileName = file.name.toLowerCase();
  726. const fileSize = file.size;
  727. let suggestedStage: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' = 'white_model';
  728. let category: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' | 'unknown' = 'white_model';
  729. let confidence = 75;
  730. let analysisReason = '基于文件名和特征分析';
  731. // 增强的关键词匹配
  732. const stageKeywords = {
  733. white_model: ['白模', 'white', 'model', '毛坯', '空间', '结构', '框架', '基础'],
  734. soft_decor: ['软装', 'soft', 'decor', '家具', 'furniture', '装饰', '饰品', '布艺'],
  735. rendering: ['渲染', 'render', '效果', 'effect', '光照', '材质', '质感'],
  736. post_process: ['后期', 'post', 'final', '最终', '完成', '成品', '精修', '调色']
  737. };
  738. // 分析文件名匹配度
  739. let maxMatchScore = 0;
  740. let bestStage = 'white_model';
  741. Object.entries(stageKeywords).forEach(([stage, keywords]) => {
  742. const matchScore = keywords.reduce((score, keyword) => {
  743. if (fileName.includes(keyword)) {
  744. return score + (keyword.length > 2 ? 2 : 1); // 长关键词权重更高
  745. }
  746. return score;
  747. }, 0);
  748. if (matchScore > maxMatchScore) {
  749. maxMatchScore = matchScore;
  750. bestStage = stage as typeof suggestedStage;
  751. }
  752. });
  753. if (maxMatchScore > 0) {
  754. suggestedStage = bestStage as 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
  755. category = bestStage as 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
  756. confidence = Math.min(75 + maxMatchScore * 5, 95);
  757. analysisReason = `文件名包含${this.getStageName(bestStage)}相关关键词`;
  758. }
  759. // 文件大小分析
  760. if (fileSize > 8 * 1024 * 1024) { // 大于8MB
  761. if (suggestedStage === 'white_model') {
  762. suggestedStage = 'rendering';
  763. category = 'rendering';
  764. confidence = Math.min(confidence + 10, 95);
  765. analysisReason += ',大文件更可能是高质量渲染图';
  766. }
  767. } else if (fileSize < 500 * 1024) { // 小于500KB
  768. if (suggestedStage === 'post_process') {
  769. suggestedStage = 'white_model';
  770. category = 'white_model';
  771. confidence = Math.max(confidence - 10, 60);
  772. analysisReason += ',小文件更可能是简单的白模图';
  773. }
  774. }
  775. // 根据目标阶段调整
  776. if (stageName) {
  777. const targetStageMap: Record<string, typeof suggestedStage> = {
  778. '白模': 'white_model',
  779. '软装': 'soft_decor',
  780. '渲染': 'rendering',
  781. '后期': 'post_process'
  782. };
  783. const targetStage = targetStageMap[stageName];
  784. if (targetStage && targetStage === suggestedStage) {
  785. confidence = Math.min(confidence + 15, 98);
  786. analysisReason += `,与目标阶段一致`;
  787. }
  788. }
  789. const qualityScoreMap = {
  790. 'white_model': 75,
  791. 'soft_decor': 82,
  792. 'rendering': 88,
  793. 'post_process': 95
  794. };
  795. const score = qualityScoreMap[suggestedStage];
  796. // 生成模拟分析结果
  797. const result: ImageAnalysisResult = {
  798. fileName: file.name,
  799. fileSize: file.size,
  800. dimensions: {
  801. width: 1920 + Math.floor(Math.random() * 400), // 模拟不同尺寸
  802. height: 1080 + Math.floor(Math.random() * 300)
  803. },
  804. quality: {
  805. score: score,
  806. level: this.getQualityLevel(score),
  807. sharpness: Math.min(score + Math.floor(Math.random() * 10), 100),
  808. brightness: Math.max(score - Math.floor(Math.random() * 10), 50),
  809. contrast: Math.min(score + Math.floor(Math.random() * 8), 100),
  810. detailLevel: score >= 90 ? 'ultra_detailed' : score >= 75 ? 'detailed' : score >= 60 ? 'basic' : 'minimal',
  811. pixelDensity: score >= 90 ? 'ultra_high' : score >= 75 ? 'high' : score >= 60 ? 'medium' : 'low',
  812. textureQuality: Math.min(score + Math.floor(Math.random() * 5), 100),
  813. colorDepth: Math.min(score + Math.floor(Math.random() * 5), 100)
  814. },
  815. content: {
  816. category: category,
  817. confidence: confidence,
  818. description: `${spaceName || '室内空间'}${this.getStageName(suggestedStage)}图`,
  819. tags: this.generateTags(suggestedStage, spaceName),
  820. isArchitectural: true,
  821. hasInterior: true,
  822. hasFurniture: suggestedStage !== 'white_model',
  823. hasLighting: suggestedStage === 'rendering' || suggestedStage === 'post_process'
  824. },
  825. technical: {
  826. format: file.type,
  827. colorSpace: Math.random() > 0.8 ? 'Adobe RGB' : 'sRGB',
  828. dpi: Math.random() > 0.5 ? 300 : 72,
  829. aspectRatio: this.calculateAspectRatio(1920, 1080),
  830. megapixels: Math.round(((1920 * 1080) / 1000000) * 100) / 100
  831. },
  832. suggestedStage: suggestedStage,
  833. suggestedReason: analysisReason,
  834. analysisTime: 50 + Math.floor(Math.random() * 100),
  835. analysisDate: new Date().toISOString()
  836. };
  837. console.log(`🚀 增强模拟分析结果: ${file.name} -> ${this.getStageName(suggestedStage)}`, {
  838. confidence: confidence,
  839. reason: analysisReason,
  840. fileSize: `${(fileSize / 1024 / 1024).toFixed(1)}MB`
  841. });
  842. return result;
  843. }
  844. /**
  845. * 生成标签
  846. */
  847. private generateTags(stage: string, spaceName?: string): string[] {
  848. const baseTags = [this.getStageName(stage), spaceName || '室内', '设计'];
  849. const stageSpecificTags: Record<string, string[]> = {
  850. 'white_model': ['建筑', '结构', '空间布局'],
  851. 'soft_decor': ['家具', '装饰', '色彩搭配'],
  852. 'rendering': ['渲染', '光影', '材质'],
  853. 'post_process': ['后期', '色彩调整', '成品']
  854. };
  855. return [...baseTags, ...(stageSpecificTags[stage] || [])];
  856. }
  857. /**
  858. * 获取阶段名称
  859. */
  860. private getStageName(stageType: string): string {
  861. const stageMap: { [key: string]: string } = {
  862. 'white_model': '白模',
  863. 'soft_decor': '软装',
  864. 'rendering': '渲染',
  865. 'post_process': '后期'
  866. };
  867. return stageMap[stageType] || stageType;
  868. }
  869. }