变化建筑物半自动标注

1.重新开一个终端进入label-studio的conda容器:

conda activate label-studio(labelstudio的基础环境安装步骤在我前面的一篇博客:https://blog.csdn.net/m0_73240431/article/details/150945462?spm=1011.2124.3001.6209

2.安装本次变化建筑分析所需环境:pip install label-studio-ml torch opencv-python pillow

pip install uvicorn

3.到违建变化的labelstudio后端目录下:

cd labelstudio/label-studio-ml

将其他项目中初始化项目生成的_wsgi.py文件(label-studio-ml init my_sta_backend)移动到label-studio-ml文件夹下

由于默认的_wsgi.py代码和变化分析的代码有所不同 所以要进行修改:

import os
import argparse
import logging
import logging.config
import json # 补充json导入(原代码遗漏)

# 日志配置
logging.config.dictConfig({
"version": 1,
"formatters": {
"standard": {
"format": "[%(asctime)s] [%(levelname)s] [%(name)s::%(funcName)s::%(lineno)d] %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"stream": "ext://sys.stdout",
"formatter": "standard"
}
},
"root": {
"level": "ERROR",
"handlers": [
"console"
],
"propagate": True
}
})

from label_studio_ml.api import init_app
# 导入自定义模型类(替换默认的DummyModel)
from model import CDModelBackend

_DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config.json')

def get_kwargs_from_config(config_path=_DEFAULT_CONFIG_PATH):
if not os.path.exists(config_path):
return dict()
with open(config_path) as f:
config = json.load(f)
assert isinstance(config, dict)
return config

if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Label studio ML backend')
parser.add_argument(
'-p', '--port', dest='port', type=int, default=9090,
help='Server port')
parser.add_argument(
'--host', dest='host', type=str, default='0.0.0.0',
help='Server host')
parser.add_argument(
'--kwargs', '--with', dest='kwargs', metavar='KEY=VAL', nargs='+', type=lambda kv: kv.split('='),
help='Additional model initialization kwargs')
parser.add_argument(
'-d', '--debug', dest='debug', action='store_true',
help='Switch debug mode')
parser.add_argument(
'--log-level', dest='log_level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], default=None,
help='Logging level')
parser.add_argument(
'--model-dir', dest='model_dir', default=os.path.dirname(__file__),
help='Directory where models are stored')
parser.add_argument(
'--check', dest='check', action='store_true',
help='Validate model instance before launching server')

args = parser.parse_args()

# 设置日志级别
if args.log_level:
logging.root.setLevel(args.log_level)

def isfloat(value):
try:
float(value)
return True
except ValueError:
return False

def parse_kwargs():
param = dict()
for k, v in args.kwargs:
if v.isdigit():
param[k] = int(v)
elif v.lower() in ('true', 'false'):
param[k] = v.lower() == 'true'
elif isfloat(v):
param[k] = float(v)
else:
param[k] = v
return param

kwargs = get_kwargs_from_config()

if args.kwargs:
kwargs.update(parse_kwargs())

# 检查模型实例创建是否成功
if args.check:
print(f'检查 "{CDModelBackend.__name__}" 模型实例创建...')
model = CDModelBackend(** kwargs)
print(f'模型实例创建成功: {model}')

# 初始化应用,使用自定义模型类
app = init_app(
model_class=CDModelBackend, # 替换为自定义模型类
model_dir=os.environ.get('MODEL_DIR', args.model_dir),
redis_queue=os.environ.get('RQ_QUEUE_NAME', 'default'),
redis_host=os.environ.get('REDIS_HOST', 'localhost'),
redis_port=int(os.environ.get('REDIS_PORT', 6379)),
**kwargs
)

# 启动服务
app.run(host=args.host, port=args.port, debug=args.debug)

else:
# 用于uWSGI部署
app = init_app(
model_class=CDModelBackend, # 替换为自定义模型类
model_dir=os.environ.get('MODEL_DIR', os.path.dirname(__file__)),
redis_queue=os.environ.get('RQ_QUEUE_NAME', 'default'),
redis_host=os.environ.get('REDIS_HOST', 'localhost'),
redis_port=int(os.environ.get('REDIS_PORT', 6379))
)

4.运行后端:cd ./labelstudio/ (这里写项目路径)

label-studio-ml start my_sta_backend

其中依赖的model.py为:

import os
import sys
import torch
import cv2
import numpy as np
from PIL import Image
import torch.nn.functional as F
from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.utils import get_image_local_path
from options.test_options import TestOptions
from models import create_model
from util.util import tensor2im

# 全局变量:保存模型和配置
model = None
opt = None

class CDModelBackend(LabelStudioMLBase):
def __init__(self, **kwargs):
super().__init__(** kwargs)
self.project_dir = os.path.abspath(os.path.dirname(__file__))
# 创建掩码保存目录
self.raw_mask_dir = os.path.join(self.project_dir, "raw_masks")
self.resized_mask_dir = os.path.join(self.project_dir, "resized_masks")
os.makedirs(self.raw_mask_dir, exist_ok=True)
os.makedirs(self.resized_mask_dir, exist_ok=True)
print(f"原始掩码保存至:{self.raw_mask_dir}")
print(f"原图尺寸掩码保存至:{self.resized_mask_dir}")
global model, opt
if model is None:
self.init_model()

self.labels = ["变化区域"]
# 预处理参数(需与训练时一致)
self.mean = [0.5, 0.5, 0.5] # 根据训练配置修改
self.std = [0.5, 0.5, 0.5] # 根据训练配置修改

def init_model(self):
"""模型初始化:严格检查权重文件"""
global model, opt
original_argv = sys.argv.copy()
try:
sys.argv = [sys.argv[0]]
necessary_args = [
'--input_nc', '3',
'--output_nc', '1',
'--f_c', '64',
'--gpu_ids', '0', # 无GPU改为'-1'
'--load_iter', '0',
'--istest', 'True',
'--max_dataset_size', '1000000',
'--phase', 'val',
'--dataroot', os.path.join(self.project_dir, 'tmp'),
'--dataset_mode', 'changedetection',
'--n_class', '1',
'--SA_mode', 'PAM',
'--arch', 'mynet3',
'--model', 'CDF0',
'--name', 'LEVIR-CDF0',
'--results_dir', os.path.join(self.project_dir, 'results'),
'--checkpoints_dir', os.path.join(self.project_dir, 'checkpoints'),
'--epoch', 'best',
'--num_test', '1000000',
'--load_size', '1024',
'--preprocess', 'resize'
]
sys.argv.extend(necessary_args)
opt = TestOptions().parse()
opt = self.make_val_opt(opt)
# 检查权重文件是否存在
weight_path = os.path.join(opt.checkpoints_dir, opt.name, f"{opt.epoch}.pth")
if not os.path.exists(weight_path):
raise FileNotFoundError(f"模型权重文件不存在:{weight_path}")
model = create_model(opt)
model.setup(opt)
model.eval()
print(f"模型加载成功:{opt.name} (epoch: {opt.epoch}),权重路径:{weight_path}")
except Exception as e:
print(f"模型初始化失败:{str(e)}")
raise
finally:
sys.argv = original_argv

def make_val_opt(self, opt):
"""标准化验证参数"""
opt.num_threads = 0
opt.batch_size = 1
opt.serial_batches = True
opt.no_flip = True
opt.no_flip2 = True
opt.display_id = -1
opt.phase = 'val'
opt.isTrain = False
opt.aspect_ratio = 1
opt.eval = True
opt.verbose = False
if not hasattr(opt, 'preprocess'):
opt.preprocess = 'resize'
return opt

def preprocess_image(self, img_path):
"""确保输入为FloatTensor"""
global opt
try:
img = Image.open(img_path).convert('RGB')
target_size = (opt.load_size, opt.load_size)
img = img.resize(target_size, Image.BILINEAR)
img = np.array(img).astype(np.float32) / 255.0
img = (img - self.mean) / self.std
img = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).float()
return img
except Exception as e:
print(f"图片预处理失败 ({img_path}):{str(e)}")
return None

def mask_to_polygons(self, mask, original_size, threshold=0.7, min_area=100):
"""处理单通道掩码,过滤小区域"""
try:
# 确保掩码为单通道
if len(mask.shape) == 3:
mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY) # 转为灰度单通道
mask_binary = (mask > threshold * 255).astype(np.uint8) # 适应[0,255]范围
contours, _ = cv2.findContours(mask_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
polygons = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area:
continue
polygon = cnt.squeeze().tolist()
if isinstance(polygon[0], (int, float)):
polygon = [polygon]
polygons.append(polygon)
print(f"过滤后保留的变化区域数量:{len(polygons)}")
return polygons
except Exception as e:
print(f"掩码转多边形失败:{str(e)}")
return []

def save_mask(self, mask, t1_path, original_size):
"""修正掩码通道和占比计算"""
try:
t1_filename = os.path.splitext(os.path.basename(t1_path))[0]
# 1. 确保掩码为单通道(关键修复)
if len(mask.shape) == 3 and mask.shape[2] == 3:
mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY) # 转为单通道灰度图
# 2. 正确计算变化像素占比(单通道)
total_pixels = mask.shape[0] * mask.shape[1]
change_pixels = (mask > 127).sum() # 阈值127对应0.5(因掩码是[0,255])
change_ratio = 100 * change_pixels / total_pixels
print(f"掩码值范围:[{mask.min():.3f}, {mask.max():.3f}],平均值:{mask.mean():.3f}")
print(f"变化像素占比:{change_ratio:.2f}%") # 现在占比会≤100%
# 3. 保存原始掩码(单通道)
raw_mask_path = os.path.join(self.raw_mask_dir, f"raw_mask_{t1_filename}.png")
cv2.imwrite(raw_mask_path, mask) # 直接保存单通道图
print(f"原始掩码(单通道)已保存:{raw_mask_path}")
# 4. 保存原图尺寸掩码
resized_mask = cv2.resize(mask, original_size, interpolation=cv2.INTER_NEAREST)
resized_mask_path = os.path.join(self.resized_mask_dir, f"resized_mask_{t1_filename}.png")
cv2.imwrite(resized_mask_path, resized_mask)
print(f"原图尺寸掩码已保存:{resized_mask_path}")
return resized_mask
except Exception as e:
print(f"保存掩码失败:{str(e)}")
return mask

def predict(self, tasks, **kwargs):
"""推理主函数:确保掩码处理正确"""
global model, opt
predictions = []
for task in tasks:
try:
# 1. 获取图片路径
t1_url = task["data"].get("image_t1")
t2_url = task["data"].get("image_t2")
if not t1_url or not t2_url:
print("任务数据缺少图片路径")
predictions.append({"result": []})
continue

# 2. 获取本地路径
t1_path = get_image_local_path(t1_url, project_dir=self.project_dir)
t2_path = get_image_local_path(t2_url, project_dir=self.project_dir)
if not os.path.exists(t1_path) or not os.path.exists(t2_path):
print(f"图片不存在:{t1_path} 或 {t2_path}")
predictions.append({"result": []})
continue
print(f"处理图片对:{os.path.basename(t1_path)} 和 {os.path.basename(t2_path)}")

# 3. 获取原始尺寸
with Image.open(t1_path) as img:
original_size = (img.width, img.height)
print(f"原始图片尺寸:{original_size}")

# 4. 预处理输入图片
t1_tensor = self.preprocess_image(t1_path)
t2_tensor = self.preprocess_image(t2_path)
if t1_tensor is None or t2_tensor is None:
predictions.append({"result": []})
continue

# 5. 模型推理
with torch.no_grad():
t1_tensor = t1_tensor.to(model.device, dtype=torch.float32)
t2_tensor = t2_tensor.to(model.device, dtype=torch.float32)
data = {
'A': t1_tensor,
'B': t2_tensor,
'A_paths': [t1_path],
'B_paths': [t2_path]
}
model.set_input(data)
model.L = torch.zeros(
(1, 1, opt.load_size, opt.load_size),
device=model.device,
dtype=torch.float32
)
model.test(val=True)
visuals = model.get_current_visuals()

if 'pred_L_show' not in visuals:
print(f"模型输出键名:{list(visuals.keys())},未找到'pred_L_show'")
predictions.append({"result": []})
continue

# 处理模型输出为单通道
pred_tensor = visuals['pred_L_show'].float()
if pred_tensor.max() > 1.0:
pred_tensor = F.sigmoid(pred_tensor)
pred_mask = tensor2im(pred_tensor) # 此时可能为3通道,后续会转为单通道
print(f"模型输出掩码尺寸(原始):{pred_mask.shape}")

# 6. 保存掩码并生成多边形(内部会转为单通道)
resized_mask = self.save_mask(pred_mask, t1_path, original_size)
polygons = self.mask_to_polygons(resized_mask, original_size)

# 7. 构造标注结果
prediction = {"result": [], "score": 0.8}
for polygon in polygons:
pred_item = {
"from_name": "change_type",
"to_name": "img_t1,img_t2",
"type": "polygon",
"value": {"points": polygon, "label": self.labels[0]}
}
prediction["result"].append(pred_item)

predictions.append(prediction)

except Exception as e:
print(f"处理任务时出错:{str(e)}")
predictions.append({"result": []})

return predictions

if __name__ == "__main__":
import uvicorn
from label_studio_ml.api import app
from label_studio_ml.model import get_model_server

app.state.model_server = get_model_server(CDModelBackend)
print("启动Label Studio ML后端服务...")
uvicorn.run(app, host="0.0.0.0", port=9090)

5.运行成功:

运行后会在./labelstudio/checkpoints/路径下获取模型 需要将.pth模型保存到此处

注意:运行成功的前提条件是需要有gpu 此模型必须要有gpu

6.打开网页端labelstudio:label-studio start

7.在labelstudio界面创建一个项目

8.将模型服务url设置到labelstudio的Model中。

9.点击设置,将输入图像的label代码设置一下

<View>
  <!-- 双时相图片(name需与toName匹配) -->
  <Image name="img_t1" value="$image_t1" width="50%" zoom="true" />
  <Image name="img_t2" value="$image_t2" width="50%" zoom="true" />
  
  <!-- 变化区域标注(PolygonLabels的name需与fromName匹配) -->
  <PolygonLabels name="change_type" toName="img_t1,img_t2" label="变化类型">
    <Label value="变化目标" background="green" />
  </PolygonLabels>
  
  <!-- 变化描述 -->
  <TextArea name="change_desc" toName="img_t1,img_t2" label="变化描述" />
</View>

10.这里需要同时输入2个图像目标,我用的json格式进行输入,由于labelstudio支持的是在线的图像 所以需要将需要传入的图像转换为在线的格式

现在就可以进行双时相半自动标注了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值