GD库实现自动裁剪PNG图片多余透明区域

        AI抠图可以自动将图片中主体扣取出来,但是图片尺寸不变,就导致主体周边存在多余的透明图层:

现在需要将多余透明图层自动删除掉。

之前只知道imagecrop(),按理说顺序应该是检测确定图片主体的x,y坐标及裁剪宽度和高度。

首先确定起始坐标值和位置,可以通过一张四个角有不同颜色的图片来确定:

测试代码:

//获取图片指定位置rgba值
$im = ImageCreateFromPng("./img/02.png");
$imgIndex = ImageColorAt($im, 0, 0); //坐标从0开始
$colorInfo = imagecolorsforindex($im, $imgIndex);
$imgIndex2 = ImageColorAt($im, 699, 437);
$colorInfo2 = imagecolorsforindex($im, $imgIndex2);
var_dump($colorInfo, $colorInfo2);

通过简单调整上面代码的参数和打印出来的结果就可以确定,坐标值是从0开始的,且坐标方向是从左上角到右下角。完全透明时colorInfo数组中的‘alpha’值为127。

因为AI抠图返回的数据是base64格式(也可以返回路径,这里需求是要返回内容),所以要写的自动裁剪方法接收两个参数,一个是图片base64数据,一个是保存图片的路径,方法如下

//获取最小非透明区域图片
function getMinAreaImg($imgStr,$saveFile)
{
    if(empty($imgStr)){
        return false;
    }
    $data = base64_decode($imgStr);
    $im = imagecreatefromstring($data);

    if($im !== false){
        $size = getimagesizefromstring($data);
        $width = $size[0];
        $height = $size[1];
        $area = [
            'left' => [0, 0],
            'right' => ['width' => $width, 'height' => $height]
        ];
        $lw = -1;//左裁剪点w坐标
        $lh = -1;//左裁剪点h坐标
        $firstPoint = [0, 0]; //第一个发现点,没用
        //查找左裁剪点
        for ($w=0; $w < $width; $w++) { 
            for ($h=0; $h < $height; $h++) { 
                $imgIndex = ImageColorAt($im, $w, $h);
                $colorInfo = imagecolorsforindex($im, $imgIndex);
                if($colorInfo['alpha'] != 127){
                    $firstPoint = [$w, $h];
                    $lw = $w;
                    $lh = $h;
                    if($lh == 0){
                        break 2;
                    }
                    //已确定左w,下面确定左h(右上角区域最小h)
                    for ($ow=$lw+1; $ow < $width; $ow++) { 
                        for ($oh=$lh-1; $oh >= 0; $oh--) { 
                            $ooindex = ImageColorAt($im, $ow, $oh);
                            $ooinfo = imagecolorsforindex($im, $ooindex);
                            if($ooinfo['alpha'] != 127){
                                $lh = $oh;
                                if($lh == 0){
                                    break 4;
                                }
                            }
                        }
                    }
                    break 2;
                }
            }
        }

        if($lw == -1){//纯透明图层
            return false;
        }

        $rw = $width-1;//右裁剪点w坐标
        $rh = $height-1;//右裁剪点h坐标
        //右下角到左裁剪点查找右裁剪点
        for ($sw=$width-1; $sw >= $lw; $sw--) { //这里条件使用>=$lw或>=$firstPoint[0]都行(第一个发现点是采用宽度优先得来的)
            for ($sh=$height-1; $sh >= $lh; $sh--) { //这里条件需要使用>=$lh,使用高度最小值(而不是第一个发现点,发现点是宽度优先得来的)才能在下面的查找中找到最大的宽度值
                $sgIndex = ImageColorAt($im, $sw, $sh);
                $sgColorInfo = imagecolorsforindex($im, $sgIndex);
                if($sgColorInfo['alpha'] != 127){
                    $rw = $sw;
                    $rh = $sh;
                    if($rh == $height-1){
                        break 2;
                    }
                    for ($sow=$rw-1; $sow >= $lw; $sow--) { 
                        for ($soh=$rh+1; $soh <= $height-1; $soh++) { 
                            $sogIndex = ImageColorAt($im, $sow, $soh);
                            $sogColorInfo = imagecolorsforindex($im, $sogIndex);
                            if($sogColorInfo['alpha'] != 127){
                                $rh = $soh;
                                if($rh == $height-1){
                                    break 4;
                                }
                            }
                        }
                    }
                    break 2;
                }
            }
        }
        //根据左右裁剪点,裁剪图片
        $cropWidth = $rw - $lw + 1;
        $cropHeight = $rh - $lh + 1;
        // var_dump($lw,$lh,$cropWidth,$cropHeight);exit;
        $im2 = imagecrop($im, ['x' => $lw, 'y' => $lh, 'width' => $cropWidth, 'height' => $cropHeight]);
        if ($im2 !== FALSE) {
            imagesavealpha($im2, true); //保存图像时是否保留完整的 alpha 通道信息
            imagepng($im2, $saveFile);
            imagedestroy($im2);
        }
        imagedestroy($im);
        return true;
    }

    return false;
}

整体思路是从左上角开始,以w优先,遍历h,先确定最左边(最小)w,然后再遍历右上角区域确定最小h,示例图(椭圆是主体)如下:

这样就确定了左上角起始坐标了,然后确定右下角坐标,从图片右下角开始,相同的道理,如下紫色区域(结束位置即上面确定的起始坐标位置)是计算需要考虑的范围。

这样又得出了最小裁剪目标的右下角坐标。

剩下的就是计算裁剪宽高和保存图片了,具体看上述代码。

测试一下:

$img = "./img/01.png";
$saveFile = './img/01_crop.png';
$imgStr = base64_encode(file_get_contents($img));
$result = getMinAreaImg($imgStr,$saveFile);
var_dump($result);

结果符合预期。

以为到这里结束了?其实不然。

在我保存裁剪图片后,发现图片透明图片丢失了,于是各种调试并查找官方文档,解决方案就是上述代码中imagesavealpha方法。
但令我崩溃是的在翻阅的过程中发现了imagecropauto()方法。auto?这明摆着就是可以自动裁剪吗,

PHP: imagecropauto - Manual

点进去一看,还真是。imagecropauto两个参数,第一个参数基本明确,主要是第二个参数存在几种类型【IMG_CROP_DEFAULT,IMG_CROP_TRANSPARENT,IMG_CROP_BLACK,IMG_CROP_WHITE,IMG_CROP_SIDES,IMG_CROP_THRESHOLD】
于是又写了一个小方法,

//自动裁剪
function cropAutoImg($imgStr,$saveFile)
{
    if(empty($imgStr)){
        return false;
    }
    $data = base64_decode($imgStr);
    $im = imagecreatefromstring($data);

    if($im !== false){
        $cropped = imagecropauto($im, IMG_CROP_SIDES);
        if ($cropped !== false) { 
            imagesavealpha($cropped, true); //保存图像时是否保留完整的 alpha 通道信息
            imagepng($cropped, $saveFile);
            imagedestroy($cropped);
        }
        imagedestroy($im);
        return true;
    }
    return false;
}

大概测试了一下,乍一看似乎是IMG_CROP_TRANSPARENT,但事与愿违,根据我提供的图片的裁剪基本确定IMG_CROP_SIDES才是符合我预期的,IMG_CROP_WHITE适合白色背景的图片,IMG_CROP_BLACK应该是适合黑色背景的吧(没测,感兴趣的可以测一下),IMG_CROP_THRESHOLD似乎跟颜色阈值有关没有深究。至于IMG_CROP_TRANSPARENT可能是我理解错了,不想费时间去探索了,有知道怎么使用的大佬希望提供下示例。

有时候对文档的熟练掌握真的能够事半功倍,但这次尝试自己编写自动裁剪也是个不错的体验吧,也希望这篇文章对大家有用。

=============新增===============

下面是根据新发现补充的新内容,在一次智能抠图后,选择自动裁剪多余透明区域,然而发现裁剪后的图片上部还是存在很大一部分透明区域
原图:

切换上面写的两个自动裁剪方法,看效果
cropAutoImg:

getMinAreaImg:

可以看出两个都存在问题,cropAutoImg是通过php的自动裁剪方法实现的咱们不方便调试和修改,只能从getMinAreaImg入手了
大概率猜到的原因应该是原图上部分区域存在肉眼无法察觉的非透明像素,先将图片上部分多余的透明区域图片通过PS取下来保存新图(下面的图可能看不见,点击一下):


然后写demo检测:

$file = './img/doubt.png';
$imgInfo = getimagesize($file);
$im = ImageCreateFromPng($file);
for ($w=0; $w < $imgInfo[0]; $w++) { 
    for ($h=0; $h < $imgInfo[1]; $h++) { 
        $imgIndex = ImageColorAt($im, $w, $h); //坐标从0开始
        $colorInfo = imagecolorsforindex($im, $imgIndex);
        if($colorInfo['alpha'] != 127){
            var_dump($w, $h, $colorInfo);exit;
        }
    }
}

结果:

跟预期的一样,确实存在非完全透明区域,只是alpha是126很接近完全透明了,根据需求咱们对于这些像素也选择抛弃,于是稍作调整,新的方案:

function getMinAreaImg($imgStr,$saveFile = '', $alphaEdge = 127)
{
    if(empty($imgStr)){
        return false;
    }
    $alphaEdge = ($alphaEdge > 127 || $alphaEdge < 0) ? 127 : $alphaEdge;
    $data = base64_decode($imgStr);
    $im = imagecreatefromstring($data);

    if($im !== false){
        $size = getimagesizefromstring($data);
        $width = $size[0];
        $height = $size[1];
        $area = [
            'left' => [0, 0],
            'right' => ['width' => $width, 'height' => $height]
        ];
        $lw = -1;//左裁剪点w坐标
        $lh = -1;//左裁剪点h坐标
        $firstPoint = [0, 0]; //第一个发现点,没用
        //查找左裁剪点
        for ($w=0; $w < $width; $w++) { 
            for ($h=0; $h < $height; $h++) { 
                $imgIndex = ImageColorAt($im, $w, $h);
                $colorInfo = imagecolorsforindex($im, $imgIndex);
                if($colorInfo['alpha'] < $alphaEdge){
                    $firstPoint = [$w, $h];
                    $lw = $w;
                    $lh = $h;
                    if($lh == 0){
                        break 2;
                    }
                    //已确定左w,下面确定左h(右上角区域最小h)
                    for ($ow=$lw+1; $ow < $width; $ow++) { 
                        for ($oh=$lh-1; $oh >= 0; $oh--) { 
                            $ooindex = ImageColorAt($im, $ow, $oh);
                            $ooinfo = imagecolorsforindex($im, $ooindex);
                            if($ooinfo['alpha'] < $alphaEdge){
                                $lh = $oh;
                                if($lh == 0){
                                    break 4;
                                }
                            }
                        }
                    }
                    break 2;
                }
            }
        }

        if($lw == -1){//纯透明图层
            return false;
        }

        $rw = $width-1;//右裁剪点w坐标
        $rh = $height-1;//右裁剪点h坐标
        //右下角到左裁剪点查找右裁剪点
        for ($sw=$width-1; $sw >= $lw; $sw--) { //这里条件使用>=$lw或>=$firstPoint[0]都行(第一个发现点是采用宽度优先得来的)
            for ($sh=$height-1; $sh >= $lh; $sh--) { //这里条件需要使用>=$lh,使用高度最小值(而不是第一个发现点,发现点是宽度优先得来的)才能在下面的查找中找到最大的宽度值
                $sgIndex = ImageColorAt($im, $sw, $sh);
                $sgColorInfo = imagecolorsforindex($im, $sgIndex);
                if($sgColorInfo['alpha'] < $alphaEdge){
                    $rw = $sw;
                    $rh = $sh;
                    if($rh == $height-1){
                        break 2;
                    }
                    for ($sow=$rw-1; $sow >= $lw; $sow--) { 
                        for ($soh=$rh+1; $soh <= $height-1; $soh++) { 
                            $sogIndex = ImageColorAt($im, $sow, $soh);
                            $sogColorInfo = imagecolorsforindex($im, $sogIndex);
                            if($sogColorInfo['alpha'] < $alphaEdge){
                                $rh = $soh;
                                if($rh == $height-1){
                                    break 4;
                                }
                            }
                        }
                    }
                    break 2;
                }
            }
        }
        //根据左右裁剪点,裁剪图片
        $cropWidth = $rw - $lw + 1;
        $cropHeight = $rh - $lh + 1;
        // var_dump($lw,$lh,$cropWidth,$cropHeight);exit;
        $im2 = imagecrop($im, ['x' => $lw, 'y' => $lh, 'width' => $cropWidth, 'height' => $cropHeight]);
        if ($im2 !== FALSE) {
            imagesavealpha($im2, true); //保存图像时是否保留完整的 alpha 通道信息
            if (!empty($saveFile)){
                imagepng($im2, $saveFile);
            }else{
                ob_start();
                imagepng($im2);
                $imgData = ob_get_contents();
                ob_end_clean();
                imagedestroy($im2);
                imagedestroy($im);
                return base64_encode($imgData);
            }
            imagedestroy($im2);
        }
        imagedestroy($im);
        return true;
    }

    return false;
}

主要是增加了$alphaEdge参数,根据用户需求自行判断需要舍弃的透明像素边界值,完全透明是127(默认值),
以及考虑到咱们需要图片的base64格式数据于是添加了$saveFile = ''的情况直接返回base64数据。这时候我们通过把$alphaEdge值设置为125试一下,效果如下:

效果还不错,可以说完美解决了上面出现的新问题!

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,我们可以使用PIL(Python Imaging Library)或OpenCV来批量中心裁剪图像。 首先,我们需要安装相关。对于PIL,可以使用以下命令来安装: ``` pip install Pillow ``` 对于OpenCV,可以使用以下命令进行安装: ``` pip install opencv-python ``` 接下来,我们需要创建一个函数来进行图像的中心裁剪。以下是一个示例函数: ```python from PIL import Image def center_crop_image(image, new_width, new_height): width, height = image.size left = (width - new_width) // 2 top = (height - new_height) // 2 right = (width + new_width) // 2 bottom = (height + new_height) // 2 cropped_image = image.crop((left, top, right, bottom)) return cropped_image ``` 然后,我们可以编写一个批量中心裁剪图像的函数。以下是一个示例函数: ```python import os def batch_center_crop_images(input_dir, output_dir, new_width, new_height): if not os.path.exists(output_dir): os.makedirs(output_dir) for filename in os.listdir(input_dir): if filename.endswith(".jpg") or filename.endswith(".png"): image_path = os.path.join(input_dir, filename) image = Image.open(image_path) cropped_image = center_crop_image(image, new_width, new_height) output_path = os.path.join(output_dir, filename) cropped_image.save(output_path) ``` 在上述函数中,我们首先检查输出目录是否存在,如果不存在则创建。然后,我们遍历输入目录中的所有图像文件,并使用`center_crop_image`函数对每个图像进行中心裁剪。最后,我们将裁剪后的图像保存到输出目录中。 最后,我们调用批量中心裁剪图像的函数,并传入输入目录、输出目录以及所需的裁剪尺寸。以下是一个示例调用: ```python input_directory = "input" output_directory = "output" new_width = 200 new_height = 200 batch_center_crop_images(input_directory, output_directory, new_width, new_height) ``` 上述示例将从名为"input"的文件夹中读取所有图像文件,并将裁剪后的图像保存到名为"output"的文件夹中。裁剪尺寸为200x200。 希望以上解答能对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值