save code
This commit is contained in:
Binary file not shown.
+147
@@ -131,6 +131,66 @@ def change_hair():
|
||||
'message': f'处理图片失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/change-hair-color', methods=['POST'])
|
||||
def change_hair_color():
|
||||
"""
|
||||
更换头发颜色
|
||||
接收用户上传的图片、颜色值和更换比例,调用远程API处理
|
||||
"""
|
||||
try:
|
||||
# 检查是否有文件上传
|
||||
if 'image' not in request.files:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请上传图片'
|
||||
}), 400
|
||||
|
||||
# 获取图片文件、颜色值和更换比例
|
||||
image_file = request.files['image']
|
||||
rgb = request.form.get('rgb')
|
||||
ratio = request.form.get('ratio')
|
||||
|
||||
if not rgb:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请选择颜色'
|
||||
}), 400
|
||||
|
||||
if not ratio:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请设置更换比例'
|
||||
}), 400
|
||||
|
||||
# 验证文件类型
|
||||
if not allowed_file(image_file.filename):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '不支持的文件类型,请上传图片文件'
|
||||
}), 400
|
||||
|
||||
# 读取图片数据
|
||||
image_data = image_file.read()
|
||||
|
||||
# 调用远程API处理图片
|
||||
result_image = call_remote_hair_color_api(image_data, rgb, ratio)
|
||||
|
||||
# 将处理后的图片转换为base64格式返回
|
||||
result_base64 = base64.b64encode(result_image).decode('utf-8')
|
||||
result_data_url = f'data:image/jpeg;base64,{result_base64}'
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'result_image': result_data_url
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'处理图片失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
def allowed_file(filename):
|
||||
"""
|
||||
验证文件类型是否允许
|
||||
@@ -226,6 +286,93 @@ def call_remote_api(image_data, hairstyle_id):
|
||||
# 如果远程API调用失败,返回原始图片
|
||||
return image_data
|
||||
|
||||
def call_remote_hair_color_api(image_data, rgb, ratio):
|
||||
"""
|
||||
调用远程头发颜色更换API
|
||||
根据用户提供的接口文档实现
|
||||
"""
|
||||
try:
|
||||
# 将图片数据转换为base64格式
|
||||
encoded_string = base64.b64encode(image_data).decode('utf-8')
|
||||
img64_str = f"data:image/jpeg;base64,{encoded_string}"
|
||||
|
||||
# 解析rgb参数
|
||||
rgb_list = list(map(int, rgb.strip('[]').split(',')))
|
||||
|
||||
# 构建请求数据(根据用户提供的格式)
|
||||
data = {
|
||||
"img": img64_str,
|
||||
"userId": "18701620166",
|
||||
"rgb": rgb_list,
|
||||
"ratio": float(ratio),
|
||||
"output_format": "base64"
|
||||
}
|
||||
|
||||
# 打印传入的参数(只打印部分信息,避免日志过长)
|
||||
print("=== 调用远程头发颜色更换API参数 ===")
|
||||
print(f"userId: {data['userId']}")
|
||||
print(f"rgb: {data['rgb']}")
|
||||
print(f"ratio: {data['ratio']}")
|
||||
print(f"output_format: {data['output_format']}")
|
||||
print(f"img 长度: {len(img64_str)}")
|
||||
print(f"img 前缀: {img64_str[:50]}...") # 只打印前缀
|
||||
print("====================")
|
||||
|
||||
# 设置请求头(根据用户提供的格式)
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# 发送请求到远程API
|
||||
print(f"发送请求到: {app.config['REMOTE_HAIR_COLOR_API_URL']}")
|
||||
response = requests.post(
|
||||
app.config['REMOTE_HAIR_COLOR_API_URL'],
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=app.config['API_TIMEOUT']
|
||||
)
|
||||
|
||||
# 打印响应状态
|
||||
print(f"响应状态码: {response.status_code}")
|
||||
print(f"响应内容长度: {len(response.text)}")
|
||||
# 只打印响应内容的前100个字符,避免base64数据过长
|
||||
response_text = response.text
|
||||
if len(response_text) > 100:
|
||||
print(f"响应内容: {response_text[:100]}...")
|
||||
else:
|
||||
print(f"响应内容: {response_text}")
|
||||
|
||||
# 解析响应数据
|
||||
response_data = response.json()
|
||||
|
||||
# 检查响应状态
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"远程API返回错误: {response_data.get('msg', '未知错误')}")
|
||||
|
||||
# 检查是否有数据返回(根据日志,远程API返回的是'result'字段而不是'data'字段)
|
||||
result_base64 = response_data.get('result', '')
|
||||
|
||||
if not result_base64:
|
||||
# 如果没有'result'字段,检查是否有其他错误信息
|
||||
error_msg = response_data.get('msg', '未知错误')
|
||||
raise Exception(f"远程API返回错误: {error_msg}")
|
||||
|
||||
# 打印成功信息
|
||||
success_msg = response_data.get('msg', '成功')
|
||||
print(f"远程API调用成功: {success_msg}")
|
||||
|
||||
# 移除可能的前缀
|
||||
if result_base64.startswith("data:image"):
|
||||
result_base64 = result_base64.split(",", 1)[1]
|
||||
|
||||
# 解码base64数据为图片
|
||||
result_image = base64.b64decode(result_base64)
|
||||
|
||||
print("远程API调用成功,返回处理后的图片")
|
||||
return result_image
|
||||
except Exception as e:
|
||||
print(f'调用远程API失败: {str(e)}')
|
||||
# 如果远程API调用失败,返回原始图片
|
||||
return image_data
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,9 @@ class Config:
|
||||
# 远程发型更换API地址
|
||||
REMOTE_API_URL = "http://xiangsilian.com:18801/api/swapHair/v1"
|
||||
|
||||
# 远程头发颜色更换API地址
|
||||
REMOTE_HAIR_COLOR_API_URL = "http://xiangsilian.com:18801/hairColor/v2"
|
||||
|
||||
# 远程API请求超时时间(秒)
|
||||
API_TIMEOUT = 120 # 延长超时时间,因为发型更换可能需要60秒
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ body {
|
||||
.image-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* 下一步按钮 */
|
||||
@@ -255,8 +256,8 @@ body {
|
||||
|
||||
.result-preview {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 500px;
|
||||
max-width: 400px;
|
||||
height: 400px;
|
||||
margin: 0 auto;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
@@ -270,7 +271,8 @@ body {
|
||||
.result-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
@@ -372,7 +374,7 @@ body {
|
||||
}
|
||||
|
||||
.result-preview {
|
||||
height: 400px;
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
-4
@@ -30,10 +30,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下一步按钮 -->
|
||||
<div class="next-section">
|
||||
<!-- 功能选择按钮 -->
|
||||
<div class="next-section" style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
|
||||
<button id="nextBtn" class="next-btn">
|
||||
<span>下一步选择发型</span>
|
||||
<span>更换发型</span>
|
||||
</button>
|
||||
<button id="changeColorBtn" class="next-btn" style="background-color: #50E3C2;">
|
||||
<span>更换头发颜色</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -56,11 +59,49 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 头发颜色选择页 -->
|
||||
<section id="hairColorPage" class="page">
|
||||
<!-- 颜色选择区 -->
|
||||
<div class="hairstyle-list">
|
||||
<h2>选择头发颜色</h2>
|
||||
<div style="max-width: 400px; margin: 0 auto; padding: 2rem; background-color: #fafafa; border-radius: 8px;">
|
||||
<!-- 颜色选择器 -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">选择颜色</label>
|
||||
<input type="color" id="colorPicker" style="width: 100%; height: 60px; border: none; border-radius: 4px;">
|
||||
</div>
|
||||
|
||||
<!-- 颜色预览 -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">颜色预览</label>
|
||||
<div id="colorPreview" style="width: 100%; height: 100px; background-color: #ff0000; border-radius: 4px; border: 2px solid #eee;"></div>
|
||||
</div>
|
||||
|
||||
<!-- RGB值显示 -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">RGB值</label>
|
||||
<div id="rgbValue" style="padding: 0.5rem; background-color: #f0f0f0; border-radius: 4px; font-family: monospace;">[255, 0, 0]</div>
|
||||
</div>
|
||||
|
||||
<!-- 颜色强度调整 -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">颜色强度: <span id="ratioValue">0.9</span></label>
|
||||
<input type="range" id="ratioSlider" min="0" max="1" step="0.1" value="0.9" style="width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<div class="confirm-section">
|
||||
<button id="confirmColorBtn" class="confirm-btn" style="background-color: #50E3C2;">确认更换颜色</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 结果预览页 -->
|
||||
<section id="resultPage" class="page">
|
||||
<!-- 效果展示 -->
|
||||
<div class="result-section">
|
||||
<h2>发型更换效果</h2>
|
||||
<h2 id="resultTitle">发型更换效果</h2>
|
||||
<div id="resultPreview" class="result-preview">
|
||||
<!-- 结果图片将通过JavaScript动态添加 -->
|
||||
</div>
|
||||
@@ -72,6 +113,10 @@
|
||||
<span class="action-icon">✂️</span>
|
||||
<span>重新选择发型</span>
|
||||
</button>
|
||||
<button id="reselectColorBtn" class="action-btn" style="display: none;">
|
||||
<span class="action-icon">🎨</span>
|
||||
<span>重新选择颜色</span>
|
||||
</button>
|
||||
<button id="reuploadImageBtn" class="action-btn">
|
||||
<span class="action-icon">📷</span>
|
||||
<span>重新上传照片</span>
|
||||
|
||||
+153
-4
@@ -27,10 +27,32 @@ async function changeHair(imageFile, hairstyleId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function changeHairColor(imageFile, rgb, ratio) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
formData.append('rgb', rgb);
|
||||
formData.append('ratio', ratio);
|
||||
|
||||
const response = await axios.post('/api/change-hair-color', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('更换头发颜色失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局变量
|
||||
let uploadedImage = null;
|
||||
let selectedHairstyle = null;
|
||||
let hairstylesData = [];
|
||||
let selectedColor = null;
|
||||
let selectedRatio = 0.9;
|
||||
let currentFunction = null; // 记录当前执行的功能:'hairstyle' 或 'color'
|
||||
|
||||
// DOM元素
|
||||
const elements = {
|
||||
@@ -39,16 +61,28 @@ const elements = {
|
||||
imagePreview: document.getElementById('imagePreview'),
|
||||
imageUpload: document.getElementById('imageUpload'),
|
||||
nextBtn: document.getElementById('nextBtn'),
|
||||
changeColorBtn: document.getElementById('changeColorBtn'),
|
||||
|
||||
// 发型选择页元素
|
||||
hairstylePage: document.getElementById('hairstylePage'),
|
||||
hairstyleGrid: document.getElementById('hairstyleGrid'),
|
||||
confirmBtn: document.getElementById('confirmBtn'),
|
||||
|
||||
// 头发颜色选择页元素
|
||||
hairColorPage: document.getElementById('hairColorPage'),
|
||||
colorPicker: document.getElementById('colorPicker'),
|
||||
colorPreview: document.getElementById('colorPreview'),
|
||||
rgbValue: document.getElementById('rgbValue'),
|
||||
ratioSlider: document.getElementById('ratioSlider'),
|
||||
ratioValue: document.getElementById('ratioValue'),
|
||||
confirmColorBtn: document.getElementById('confirmColorBtn'),
|
||||
|
||||
// 结果预览页元素
|
||||
resultPage: document.getElementById('resultPage'),
|
||||
resultTitle: document.getElementById('resultTitle'),
|
||||
resultPreview: document.getElementById('resultPreview'),
|
||||
reselectHairstyleBtn: document.getElementById('reselectHairstyleBtn'),
|
||||
reselectColorBtn: document.getElementById('reselectColorBtn'),
|
||||
reuploadImageBtn: document.getElementById('reuploadImageBtn'),
|
||||
saveResultBtn: document.getElementById('saveResultBtn'),
|
||||
|
||||
@@ -71,17 +105,24 @@ function bindEventListeners() {
|
||||
// 图片上传
|
||||
elements.imageUpload.addEventListener('change', handleImageUpload);
|
||||
|
||||
// 下一步按钮
|
||||
// 功能选择按钮
|
||||
elements.nextBtn.addEventListener('click', navigateToHairstylePage);
|
||||
elements.changeColorBtn.addEventListener('click', navigateToHairColorPage);
|
||||
|
||||
// 确认按钮
|
||||
// 发型确认按钮
|
||||
elements.confirmBtn.addEventListener('click', handleConfirmHairstyle);
|
||||
|
||||
// 头发颜色相关事件
|
||||
elements.colorPicker.addEventListener('input', handleColorChange);
|
||||
elements.ratioSlider.addEventListener('input', handleRatioChange);
|
||||
elements.confirmColorBtn.addEventListener('click', handleConfirmColor);
|
||||
|
||||
// 返回按钮
|
||||
elements.backBtn.addEventListener('click', handleBack);
|
||||
|
||||
// 结果页操作按钮
|
||||
elements.reselectHairstyleBtn.addEventListener('click', handleReselectHairstyle);
|
||||
elements.reselectColorBtn.addEventListener('click', handleReselectColor);
|
||||
elements.reuploadImageBtn.addEventListener('click', handleReuploadImage);
|
||||
elements.saveResultBtn.addEventListener('click', handleSaveResult);
|
||||
}
|
||||
@@ -127,11 +168,26 @@ function navigateToHairstylePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
currentFunction = 'hairstyle';
|
||||
renderHairstyles();
|
||||
showPage(elements.hairstylePage);
|
||||
showBackBtn();
|
||||
}
|
||||
|
||||
// 导航到头发颜色选择页
|
||||
function navigateToHairColorPage() {
|
||||
if (!uploadedImage) {
|
||||
alert('请先上传图片');
|
||||
return;
|
||||
}
|
||||
|
||||
currentFunction = 'color';
|
||||
// 初始化颜色选择器
|
||||
handleColorChange({ target: elements.colorPicker });
|
||||
showPage(elements.hairColorPage);
|
||||
showBackBtn();
|
||||
}
|
||||
|
||||
// 渲染发型列表
|
||||
function renderHairstyles() {
|
||||
elements.hairstyleGrid.innerHTML = '';
|
||||
@@ -169,6 +225,82 @@ function selectHairstyle(hairstyleId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理颜色变化
|
||||
function handleColorChange(e) {
|
||||
const color = e.target.value;
|
||||
selectedColor = color;
|
||||
|
||||
// 更新颜色预览
|
||||
elements.colorPreview.style.backgroundColor = color;
|
||||
|
||||
// 转换为RGB值
|
||||
const rgb = hexToRgb(color);
|
||||
elements.rgbValue.textContent = `[${rgb.r}, ${rgb.g}, ${rgb.b}]`;
|
||||
}
|
||||
|
||||
// 处理比例变化
|
||||
function handleRatioChange(e) {
|
||||
const ratio = e.target.value;
|
||||
selectedRatio = parseFloat(ratio);
|
||||
elements.ratioValue.textContent = ratio;
|
||||
}
|
||||
|
||||
// 处理确认颜色选择
|
||||
async function handleConfirmColor() {
|
||||
if (!uploadedImage || !selectedColor) {
|
||||
alert('请上传图片并选择颜色');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载状态
|
||||
elements.loading.innerHTML = `
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在处理头发颜色更换,请稍候...</p>
|
||||
<p style="font-size: 0.9rem; color: #666;">此过程可能需要10-60秒</p>
|
||||
`;
|
||||
showLoading();
|
||||
|
||||
// 转换为RGB值
|
||||
const rgb = hexToRgb(selectedColor);
|
||||
const rgbString = `[${rgb.r}, ${rgb.g}, ${rgb.b}]`;
|
||||
|
||||
// 发起头发颜色更换请求
|
||||
const response = await changeHairColor(uploadedImage, rgbString, selectedRatio);
|
||||
|
||||
if (response.success) {
|
||||
// 显示结果
|
||||
elements.resultTitle.textContent = '头发颜色更换效果';
|
||||
showResult(response.data.result_image);
|
||||
|
||||
// 显示/隐藏相应的按钮
|
||||
elements.reselectHairstyleBtn.style.display = 'none';
|
||||
elements.reselectColorBtn.style.display = 'block';
|
||||
} else {
|
||||
alert(response.message || '处理失败,请重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理失败:', error);
|
||||
alert('处理失败,请重试');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 十六进制颜色转换为RGB
|
||||
function hexToRgb(hex) {
|
||||
// 移除#号
|
||||
hex = hex.replace(/^#/, '');
|
||||
|
||||
// 解析RGB值
|
||||
const bigint = parseInt(hex, 16);
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
// 处理确认发型选择
|
||||
async function handleConfirmHairstyle() {
|
||||
if (!uploadedImage || !selectedHairstyle) {
|
||||
@@ -190,7 +322,12 @@ async function handleConfirmHairstyle() {
|
||||
|
||||
if (response.success) {
|
||||
// 显示结果
|
||||
elements.resultTitle.textContent = '发型更换效果';
|
||||
showResult(response.data.result_image);
|
||||
|
||||
// 显示/隐藏相应的按钮
|
||||
elements.reselectHairstyleBtn.style.display = 'block';
|
||||
elements.reselectColorBtn.style.display = 'none';
|
||||
} else {
|
||||
alert(response.message || '处理失败,请重试');
|
||||
}
|
||||
@@ -216,6 +353,11 @@ function handleReselectHairstyle() {
|
||||
showPage(elements.hairstylePage);
|
||||
}
|
||||
|
||||
// 处理重新选择颜色
|
||||
function handleReselectColor() {
|
||||
showPage(elements.hairColorPage);
|
||||
}
|
||||
|
||||
// 处理重新上传图片
|
||||
function handleReuploadImage() {
|
||||
// 重置图片上传
|
||||
@@ -258,17 +400,24 @@ function handleSaveResult() {
|
||||
// 处理返回按钮
|
||||
function handleBack() {
|
||||
if (elements.resultPage.classList.contains('active')) {
|
||||
showPage(elements.hairstylePage);
|
||||
if (currentFunction === 'hairstyle') {
|
||||
showPage(elements.hairstylePage);
|
||||
} else if (currentFunction === 'color') {
|
||||
showPage(elements.hairColorPage);
|
||||
}
|
||||
} else if (elements.hairstylePage.classList.contains('active')) {
|
||||
showPage(elements.homePage);
|
||||
hideBackBtn();
|
||||
} else if (elements.hairColorPage.classList.contains('active')) {
|
||||
showPage(elements.homePage);
|
||||
hideBackBtn();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示页面
|
||||
function showPage(page) {
|
||||
// 隐藏所有页面
|
||||
[elements.homePage, elements.hairstylePage, elements.resultPage].forEach(p => {
|
||||
[elements.homePage, elements.hairstylePage, elements.hairColorPage, elements.resultPage].forEach(p => {
|
||||
p.classList.remove('active');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user