前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Seam Carving demo

Seam Carving demo

原创
作者头像
4cos90
修改2021-05-28 10:02:17
2.2K0
修改2021-05-28 10:02:17
举报
文章被收录于专栏:随想随想

最近看到个很有意思的网站:

https://trekhleb.dev/js-image-carver/

Seam Carving是一种图片压缩算法。简单来说就是优先删除图片中颜色与周围像素接近的像素点。即大片相同的颜色(如背景)将会被优先删除。最后将会留下主要元素的轮廓。

这个网站不但提供了一张图片供试验,也可以在线上上传图片。大家可以体验一下,效果很好。

幸运的是作者提供了源码和算法原理的讲解。算法原理很简单,简单浏览一下就可以明白。

从github上clone了源码,作者原来是用React写的,我把他改成了angular,同样实现了最基本的功能。

下面写一下改写的过程:

首先明确下我们需要实现的最基本的功能:

1.图片上传。

2.根据输入的长宽缩放比例,对图片进行压缩。

那么开始:

首先在github上clone了作者的源码,简单的阅读了一下源码,找到实现基本功能的文件。其中ImageResizer.tsx包含Resize时canvas相关的代码。utils中是实现了的Seam Carving算法。contentAwareSimplified.ts是包含了注释的版本。

源码结构
源码结构

首先新建一个新的组件,引入算法文件。

angular目录结构
angular目录结构

那么先实现第一个需求,图片上传:

代码语言:javascript
复制
        <div class="ButtonMr Button">CHOOSE IMAGE
            <input class="FileInput" type="file" (change)="onChange($event)" accept="image/png,image/jpeg" name="file" multiple="false" />
        </div>
        <div *ngIf="SelectImg">
            <div class="InputArea">
                <b class="Text">
                    Original image
                </b>
            </div>
            <img #imgRef id='myImg' [src]="imageSrc" alt="Original" />
        </div>
代码语言:javascript
复制
    imageSrc: SafeUrl = "";
    
    onChange = (event) => {
        const files = event.target.files
        this.onFileSelect(files);
    };
    
    onFileSelect = (files: FileList | null): void => {
        if (!files || !files.length) {
            return;
        }
        console.log(files);
        this.onReset();
        const imageURL = this.sanitizer.bypassSecurityTrustUrl(window.URL.createObjectURL(files[0]));//URL.createObjectURL(files[0]);
        this.imageSrc = imageURL;
        this.SelectImg = true;
    };

通过<input type="file"> 实现上传文件,通过imageSrc绑定<img>的Src,值得注意的是URL.createObjectURL(files[0])可能会导致跨域问题,因此需要使用SafeUrl声明这个链接是安全的,才能正常显示图片。

然后是第二个需求,实现Resize的功能:

代码语言:javascript
复制
 <div class="Button" (click)="Resize()">
        RESIZE
 </div>
 <div class="InputArea">
  <b class="Text"> Resized image </b>
 </div>
 <canvas #canvasRef id='myCanvas'> </canvas>

Resize需要用到canvas,下面是React和Angular的一些区别:

获取Img与Canvas元素

React:通过ref

代码语言:javascript
复制
    <img src={imageSrc} alt="Original" ref={imgRef} style={{ margin: 0 }} />
    <canvas ref={canvasRef} />
    
    const imgRef = useRef<HTMLImageElement>(null);
    const canvasRef = useRef<HTMLCanvasElement>(null);
    
    const srcImg: HTMLImageElement | null = imgRef.current;
    const canvas: HTMLCanvasElement | null = canvasRef.current;

Angular:通过ViewChild与#声明

代码语言:javascript
复制
<img #imgRef id='myImg' [src]="imageSrc" alt="Original" />
<canvas #canvasRef id='myCanvas'> </canvas>

@ViewChild('imgRef', { static: false })
imgRef: ElementRef;
@ViewChild('canvasRef', { static: false })
canvasRef: ElementRef;

const srcImg: HTMLImageElement | null = this.imgRef.nativeElement;
const canvas: HTMLCanvasElement | null = this.canvasRef.nativeElement;

原生:通过document

代码语言:javascript
复制
const srcImg: any = document.getElementById('myImg');
const canvas: any = document.getElementById('myCanvas');

单向绑定

React:通过useState

代码语言:javascript
复制
const [imageSrc, setImageSrc] = useState<string>(defaultImgSrc);
setImageSrc(imageURL);

Angular:通过[src]

代码语言:javascript
复制
this.imageSrc = imageURL;
<img #imgRef id='myImg' [src]="imageSrc" alt="Original" />

另外作者原本提供了Mask的功能,这里为了简单实现,没有实现Mask和图片缩放时删除像素的特效,也没有提供Higher quality

的选项(即使用img.naturalWidth和img.Width的区别)。

将Resize方法按上述方式修改。那么我们基本上就已经大功告成了。

组件完整代码:

html:

代码语言:javascript
复制
<div class="Base">
    <div class="Title">
        <div class="ButtonArea">
            <div class="ButtonMr Button">
                CHOOSE IMAGE
                <input class="FileInput" type="file" (change)="onChange($event)" accept="image/png,image/jpeg" name="file" multiple="false" />
            </div>
            <div class="Button" (click)="Resize()">
                RESIZE
            </div>
        </div>
        <div class="InputArea">
            <div class="Text mr-1">
                Width
            </div>
            <div class="InputArea">
                <input class="Input" type="number" [min]="minScale" [max]="maxScale" [(ngModel)]="WidthChange" oninput="value = (value > 100 ? 100 : (value < 1 ? 1 : value))" />
            </div>
            <div class="Text ml-1 mr-4">
                %
            </div>
            <div class="Text mr-1">
                Height
            </div>
            <div class="InputArea">
                <input class="Input" type="number" [min]="minScale" [max]="maxScale" [(ngModel)]="HeightChage" oninput="value = (value > 100 ? 100 : (value < 1 ? 1 : value))" />
            </div>
            <div class="Text ml-1">
                %
            </div>
        </div>
        <div>
            <div class="InputArea">
                <b class="Text">
                    Resized image
                </b>
            </div>
            <canvas #canvasRef id='myCanvas'> </canvas>
        </div>
        <div *ngIf="SelectImg">
            <div class="InputArea">
                <b class="Text">
                    Original image
                </b>
            </div>
            <img #imgRef id='myImg' [src]="imageSrc" alt="Original" />
        </div>
    </div>

</div>
ts:
代码语言:javascript
复制
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { EnergyMap, ImageSize, MAX_HEIGHT_LIMIT, MAX_WIDTH_LIMIT, OnIterationArgs, resizeImage, Seam } from './function'


const maxWidthLimit = MAX_WIDTH_LIMIT;
const maxHeightLimit = MAX_HEIGHT_LIMIT;


@Component({
    selector: 'app-SeamCarving',
    templateUrl: './SeamCarving.component.html',
    styleUrls: ['./SeamCarving.component.css']
})
export class SeamCarvingComponent implements OnInit {

    minScale: number = 1;
    maxScale: number = 100;

    WidthChange: string = "100";
    HeightChage: string = "100";

    isResizing: boolean = false;

    imageSrc: SafeUrl = "";

    SelectImg: boolean = false;

    @ViewChild('imgRef', { static: false })
    imgRef: ElementRef;

    @ViewChild('canvasRef', { static: false })
    canvasRef: ElementRef;

    workingImgSize: ImageSize;
    originalImgViewSize: ImageSize;
    resizedImgSrc: SafeUrl = "";
    energyMap: EnergyMap = null;
    seams: Seam[] = null;
    progress: number = 0;
    constructor(
        private sanitizer: DomSanitizer
    ) { }

    ngOnInit() {

    }

    Resize() {
        const srcImg: HTMLImageElement | null = this.imgRef.nativeElement;
        //const srcImg: any = document.getElementById('myImg');
        if (!srcImg) {
            return;
        }

        const canvas: HTMLCanvasElement | null = this.canvasRef.nativeElement;
        //const canvas: any = document.getElementById('myCanvas');
        if (!canvas) {
            return;
        }

        this.onReset();
        this.isResizing = true;

        let w = srcImg.naturalWidth; //srcImg.width;
        let h = srcImg.naturalHeight;//srcImg.height;
        const ratio = w / h;

        this.originalImgViewSize = {
            w: srcImg.width,
            h: srcImg.height,
        };

        if (w > maxWidthLimit) {
            w = maxWidthLimit;
            h = Math.floor(w / ratio);
        }

        if (h > maxHeightLimit) {
            h = maxHeightLimit;
            w = Math.floor(h * ratio);
        }

        canvas.width = w;
        canvas.height = h;

        const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d');
        if (!ctx) {
            return;
        }

        ctx.drawImage(srcImg, 0, 0, w, h);

        const img: ImageData = ctx.getImageData(0, 0, w, h);

        // this.applyMask(img);

        const toWidth = Math.floor((this.onWidthSizeChange(this.WidthChange) * w) / 100);
        const toHeight = Math.floor((this.onHeightSizeChange(this.HeightChage) * h) / 100);

        const onIteration = this.onIteration.bind(this);

        resizeImage({
            img,
            toWidth,
            toHeight,
            onIteration,
        }).then(() => {
            this.onFinish();
        });
    };

    onWidthSizeChange = (size: string | undefined): number => {
        const radix = 10;
        const scale = Math.max(Math.min(parseInt(size || '0', radix), this.maxScale), this.minScale);
        return scale;
    };

    onHeightSizeChange = (size: string | undefined): number => {
        const radix = 10;
        const scale = Math.max(Math.min(parseInt(size || '0', radix), this.maxScale), this.minScale);
        return scale;
    };

    onFileSelect = (files: FileList | null): void => {
        if (!files || !files.length) {
            return;
        }
        console.log(files);
        this.onReset();
        const imageURL = this.sanitizer.bypassSecurityTrustUrl(window.URL.createObjectURL(files[0]));//URL.createObjectURL(files[0]);
        this.imageSrc = imageURL;
        this.SelectImg = true;
    };

    onReset = (): void => {
        this.resizedImgSrc = null;
        this.workingImgSize = null;
        this.energyMap = null;
        this.originalImgViewSize = null;
    };

    onChange = (event) => {
        const files = event.target.files
        this.onFileSelect(files);
    };

    onIteration = async (args: OnIterationArgs): Promise<void> => {
        const {
            seam,
            img,
            energyMap: nrgMap,
            size: { w, h },
            step,
            steps,
        } = args;

        const canvas: HTMLCanvasElement | null = this.canvasRef.nativeElement;
        //const canvas: any = document.getElementById('myCanvas');
        if (!canvas) {
            return;
        }

        canvas.width = w;
        canvas.height = h;

        const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d');
        if (!ctx) {
            return;
        }

        ctx.putImageData(img, 0, 0, 0, 0, w, h);

        this.energyMap = nrgMap;
        this.seams = [seam];
        this.workingImgSize = { w, h };
        this.progress = step / steps;
    };

    onFinish = (): void => {
        const canvas: any = this.canvasRef.nativeElement;//document.getElementById('myCanvas');
        if (!canvas) {
            return;
        }
        const imageType = 'image/png';
        canvas.toBlob((blob: Blob | null): void => {
            if (!blob) {
                return;
            }
            const imgUrl = URL.createObjectURL(blob);
            this.resizedImgSrc = imgUrl;
            this.isResizing = false;
        }, imageType);
    };
}

css:

代码语言:javascript
复制
.Base {
    height: 100%;
    width: 100%;
}

.Title {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    margin-bottom: 0.25rem;
}

.ButtonArea {
    display: flex;
    flex-direction: row;
    margin-bottom: 0.75rem;
}

.ButtonMr {
    margin-right: 0.5rem;
}

.Button {
    background: black;
    color: white;
    cursor: pointer;
    width: 150px;
    height: 30px;
    font-size: 18px;
    line-height: 30px;
    text-align: center;
}

.InputArea {
    display: flex;
    flex-direction: row;
}

.Text {
    font-size: 0.75rem;
    line-height: 1rem;
}

.mr-1 {
    margin-right: 0.25rem;
}

.InputArea {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
}

.mr-4 {
    margin-right: 1rem;
}

.ml-1 {
    margin-left: 0.25rem;
}

.FileInput {
    width: 100%;
    height: 100%;
    position: relative;
    left: 0px;
    top: -35px;
    opacity: 0%;
    cursor: pointer;
}

最后看下实现的效果对比:

实现
实现

可以看出缩放后的图像还是比较一致的。

那么一个简单的Seam Carving的demo就完成了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
图片处理
图片处理(Image Processing,IP)是由腾讯云数据万象提供的丰富的图片处理服务,广泛应用于腾讯内部各产品。支持对腾讯云对象存储 COS 或第三方源的图片进行处理,提供基础处理能力(图片裁剪、转格式、缩放、打水印等)、图片瘦身能力(Guetzli 压缩、AVIF 转码压缩)、盲水印版权保护能力,同时支持先进的图像 AI 功能(图像增强、图像标签、图像评分、图像修复、商品抠图等),满足多种业务场景下的图片处理需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档