最近看到个很有意思的网站:
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是包含了注释的版本。
首先新建一个新的组件,引入算法文件。
那么先实现第一个需求,图片上传:
<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>
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的功能:
<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
<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与#声明
<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
const srcImg: any = document.getElementById('myImg');
const canvas: any = document.getElementById('myCanvas');
单向绑定
React:通过useState
const [imageSrc, setImageSrc] = useState<string>(defaultImgSrc);
setImageSrc(imageURL);
Angular:通过[src]
this.imageSrc = imageURL;
<img #imgRef id='myImg' [src]="imageSrc" alt="Original" />
另外作者原本提供了Mask的功能,这里为了简单实现,没有实现Mask和图片缩放时删除像素的特效,也没有提供Higher quality
的选项(即使用img.naturalWidth和img.Width的区别)。
将Resize方法按上述方式修改。那么我们基本上就已经大功告成了。
组件完整代码:
html:
<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:
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:
.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 删除。