项目文件夹

文件
wehub-resource-sync e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

285 行
11 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This code is refer from:
https://github.com/WenmuZhou/DBNet.pytorch/blob/master/models/losses/basic_loss.py
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
import paddle
from paddle import nn
import paddle.nn.functional as F
class BalanceLoss(nn.Layer):
def __init__(
self,
balance_loss=True,
main_loss_type="DiceLoss",
negative_ratio=3,
return_origin=False,
eps=1e-6,
**kwargs,
):
"""
The BalanceLoss for Differentiable Binarization text detection
args:
balance_loss (bool): whether balance loss or not, default is True
main_loss_type (str): can only be one of ['CrossEntropy','DiceLoss',
'Euclidean','BCELoss', 'MaskL1Loss'], default is 'DiceLoss'.
negative_ratio (int|float): float, default is 3.
return_origin (bool): whether return unbalanced loss or not, default is False.
eps (float): default is 1e-6.
"""
super(BalanceLoss, self).__init__()
self.balance_loss = balance_loss
self.main_loss_type = main_loss_type
self.negative_ratio = negative_ratio
self.return_origin = return_origin
self.eps = eps
if self.main_loss_type == "CrossEntropy":
self.loss = nn.CrossEntropyLoss()
elif self.main_loss_type == "Euclidean":
self.loss = nn.MSELoss()
elif self.main_loss_type == "DiceLoss":
self.loss = DiceLoss(self.eps)
elif self.main_loss_type == "BCELoss":
self.loss = BCELoss(reduction="none")
elif self.main_loss_type == "MaskL1Loss":
self.loss = MaskL1Loss(self.eps)
else:
loss_type = [
"CrossEntropy",
"DiceLoss",
"Euclidean",
"BCELoss",
"MaskL1Loss",
]
raise Exception(
"main_loss_type in BalanceLoss() can only be one of {}".format(
loss_type
)
)
def forward(self, pred, gt, mask=None):
"""
The BalanceLoss for Differentiable Binarization text detection
args:
pred (variable): predicted feature maps.
gt (variable): ground truth feature maps.
mask (variable): masked maps.
return: (variable) balanced loss
"""
positive = gt * mask
negative = (1 - gt) * mask
positive_count = int(positive.sum())
negative_count = int(min(negative.sum(), positive_count * self.negative_ratio))
loss = self.loss(pred, gt, mask=mask)
if not self.balance_loss:
return loss
positive_loss = positive * loss
negative_loss = negative * loss
negative_loss = paddle.reshape(negative_loss, shape=[-1])
if negative_count > 0:
sort_loss = negative_loss.sort(descending=True)
negative_loss = sort_loss[:negative_count]
# negative_loss, _ = paddle.topk(negative_loss, k=negative_count_int)
balance_loss = (positive_loss.sum() + negative_loss.sum()) / (
positive_count + negative_count + self.eps
)
else:
balance_loss = positive_loss.sum() / (positive_count + self.eps)
if self.return_origin:
return balance_loss, loss
return balance_loss
class DiceLoss(nn.Layer):
def __init__(self, eps=1e-6):
super(DiceLoss, self).__init__()
self.eps = eps
def forward(self, pred, gt, mask, weights=None):
"""
DiceLoss function.
"""
assert pred.shape == gt.shape
assert pred.shape == mask.shape
if weights is not None:
assert weights.shape == mask.shape
mask = weights * mask
intersection = paddle.sum(pred * gt * mask)
union = paddle.sum(pred * mask) + paddle.sum(gt * mask) + self.eps
loss = 1 - 2.0 * intersection / union
assert loss <= 1
return loss
class MaskL1Loss(nn.Layer):
def __init__(self, eps=1e-6):
super(MaskL1Loss, self).__init__()
self.eps = eps
def forward(self, pred, gt, mask):
"""
Mask L1 Loss
"""
loss = (paddle.abs(pred - gt) * mask).sum() / (mask.sum() + self.eps)
loss = paddle.mean(loss)
return loss
class BCELoss(nn.Layer):
def __init__(self, reduction="mean"):
super(BCELoss, self).__init__()
self.reduction = reduction
def forward(self, input, label, mask=None, weight=None, name=None):
loss = F.binary_cross_entropy(input, label, reduction=self.reduction)
return loss
class MaskedFocalLoss(nn.Layer):
"""
Binary Focal Loss with mask support, designed for text segmentation tasks.
Focal Loss addresses class imbalance by down-weighting easy examples and
focusing training on hard examples:
FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)
Compared to OHEM (which hard-selects a fixed ratio of negatives), Focal Loss
applies a continuous per-pixel weight that gracefully scales with difficulty,
making it a strictly superior drop-in for the OHEM + DiceLoss pattern when
DiceLoss returns a scalar and OHEM has no discriminating effect.
Args:
alpha (float): Balancing factor for the positive (text) class.
Since text pixels are a small minority, alpha > 0.5 gives them
higher weight. Default: 0.75.
gamma (float): Focusing parameter. gamma=0 reduces to masked BCE.
gamma=2 is the standard value from the original Focal Loss paper.
Default: 2.0.
eps (float): Small constant for numerical stability. Default: 1e-6.
"""
def __init__(self, alpha=0.25, gamma=2.0, eps=1e-6):
super(MaskedFocalLoss, self).__init__()
self.alpha = alpha
self.gamma = gamma
self.eps = eps
def forward(self, pred, gt, mask):
"""
Args:
pred (Tensor): Predicted probability map, shape (B, H, W), in [0, 1].
(i.e. after sigmoid — the direct output of DBHead.binarize)
gt (Tensor): Binary ground-truth map, shape (B, H, W), values 0 or 1.
mask (Tensor): Valid-pixel mask, shape (B, H, W), values 0 or 1.
Pixels with mask=0 are ignored regions (e.g. too-small text).
Returns:
Tensor: Scalar focal loss averaged over valid (mask=1) pixels.
"""
# F.sigmoid_focal_loss expects a logit (pre-sigmoid) input and applies
# sigmoid internally using the numerically stable log-sum-exp form:
# log(σ(x)) = -softplus(-x), log(1-σ(x)) = -softplus(x)
# This avoids the log(0) issue of the manual implementation.
# Since pred is already a probability (post-sigmoid from DBHead), we
# invert it: logit = log(p / (1-p)). The round-trip is numerically safe
# after clamping, and the stable path inside paddle takes over from there.
pred = paddle.clip(pred, self.eps, 1.0 - self.eps)
logit = paddle.log(pred / (1.0 - pred))
# Per-pixel focal loss, shape (B, H, W)
# reduction='none' so we can apply the mask ourselves
loss = F.sigmoid_focal_loss(
logit,
gt,
normalizer=None,
alpha=self.alpha,
gamma=self.gamma,
reduction="none",
)
# Average over valid (mask=1) pixels only
return (loss * mask).sum() / (mask.sum() + self.eps)
class DiceFocalLoss(nn.Layer):
"""
Combined DiceLoss + MaskedFocalLoss for binary text segmentation.
Rationale for the combination:
- DiceLoss optimizes the global F1 / region overlap between prediction and GT.
It is naturally robust to class imbalance (text vs background) because it
normalizes by the sum of both sets, not by pixel count.
- MaskedFocalLoss provides per-pixel supervision with adaptive hard-example
weighting. It compensates for DiceLoss being a global metric that cannot
distinguish which specific pixels are mispredicted.
Together they provide complementary supervision: DiceLoss for global shape
quality, FocalLoss for pixel-level precision on ambiguous boundaries.
This design follows the Dice + Focal combination used in mmsegmentation and
segmentation_models_pytorch for binary segmentation with class imbalance.
This class is a drop-in replacement for BalanceLoss when main_loss_type is
'DiceLoss' — both share the same forward(pred, gt, mask) signature and
return a scalar.
Args:
dice_weight (float): Weight for the DiceLoss term. Default: 1.0.
focal_weight (float): Weight for the MaskedFocalLoss term. Default: 1.0.
focal_alpha (float): Positive-class balancing factor for FocalLoss.
Default: 0.75.
focal_gamma (float): Focusing exponent for FocalLoss. Default: 2.0.
eps (float): Numerical stability constant. Default: 1e-6.
"""
def __init__(
self,
dice_weight=1.0,
focal_weight=1.0,
focal_alpha=0.75,
focal_gamma=2.0,
eps=1e-6,
):
super(DiceFocalLoss, self).__init__()
self.dice_weight = dice_weight
self.focal_weight = focal_weight
self.dice_loss = DiceLoss(eps=eps)
self.focal_loss = MaskedFocalLoss(alpha=focal_alpha, gamma=focal_gamma, eps=eps)
def forward(self, pred, gt, mask=None):
"""
Args:
pred (Tensor): Predicted probability map, shape (B, H, W), in [0, 1].
gt (Tensor): Binary ground-truth shrink map, shape (B, H, W).
mask (Tensor): Valid-pixel mask, shape (B, H, W).
Returns:
Tensor: Scalar combined loss.
"""
loss_dice = self.dice_loss(pred, gt, mask)
loss_focal = self.focal_loss(pred, gt, mask)
return self.dice_weight * loss_dice + self.focal_weight * loss_focal